Parcourir la source

feat(webworker): add selectable preview fixtures

imccyu il y a 2 semaines
Parent
commit
e883dc2354
43 fichiers modifiés avec 2057 ajouts et 108 suppressions
  1. 9 7
      apps/web/src/preview.ts
  2. 191 44
      apps/web/tests/preview-boot.e2e.ts
  3. 15 0
      apps/web/tests/snapshots/preview-boot/source-chooser.expected.md
  4. 2 1
      knip.json
  5. 1 1
      packages/experimental/webworker-packer/package.json
  6. 38 6
      packages/experimental/webworker-packer/src/bin.ts
  7. 4 3
      packages/experimental/webworker-packer/src/index.ts
  8. 58 8
      packages/experimental/webworker-packer/src/pack.ts
  9. 33 1
      packages/experimental/webworker-packer/src/repository.ts
  10. 28 2
      packages/experimental/webworker-packer/tests/image-loadable.spec.ts
  11. 3 0
      packages/experimental/webworker-runtime/package.json
  12. 3 2
      packages/experimental/webworker-runtime/src/client/client.ts
  13. 70 6
      packages/experimental/webworker-runtime/src/client/index.ts
  14. 178 0
      packages/experimental/webworker-runtime/src/client/source-chooser.ts
  15. 67 0
      packages/experimental/webworker-runtime/src/fixture-manifest.ts
  16. 9 3
      packages/experimental/webworker-runtime/src/image-layout.ts
  17. 6 2
      packages/experimental/webworker-runtime/src/index.ts
  18. 1 0
      packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs-watch.ts
  19. 2 0
      packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/stream.ts
  20. 11 7
      packages/experimental/webworker-runtime/src/shell/fs-access.ts
  21. 36 0
      packages/experimental/webworker-runtime/src/storage/memory.ts
  22. 7 3
      packages/experimental/webworker-runtime/src/transport/frames.ts
  23. 9 3
      packages/experimental/webworker-runtime/src/worker-host.ts
  24. 7 3
      packages/experimental/webworker-runtime/src/worker.ts
  25. 150 0
      packages/experimental/webworker-runtime/tests/client/source-chooser.spec.ts
  26. 48 0
      packages/experimental/webworker-runtime/tests/fixture-manifest.spec.ts
  27. 178 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl
  28. 8 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl
  29. 173 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.jsonl
  30. 24 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json
  31. 8 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/.agents/skills/preview-tour/SKILL.md
  32. 9 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/PREVIEW.md
  33. 17 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/data/tasks.json
  34. 3 0
      packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/src/preview.ts
  35. 3 4
      packages/experimental/webworker-runtime/tests/node/child-process.spec.ts
  36. 18 1
      packages/experimental/webworker-runtime/tests/storage/tar.spec.ts
  37. 22 0
      packages/experimental/webworker-runtime/tests/transport/frames.spec.ts
  38. 21 0
      packages/experimental/webworker-runtime/tests/transport/tunnel-client.spec.ts
  39. 118 0
      packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts
  40. 432 0
      packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts
  41. 9 0
      pnpm-lock.yaml
  42. 13 1
      scripts/session-fixture-layout.spec.ts
  43. 15 0
      scripts/session-fixture-layout.ts

+ 9 - 7
apps/web/src/preview.ts

@@ -1,12 +1,14 @@
 /**
  * Worker-preview bootstrap: the one module preview.html adds ahead of the
- * stock entry tag. Connecting the worker host installs the boot globals and
- * settles `__DSH_BOOT_READY__`, where the stock entry's pre-boot await holds,
- * so everything after this module is the served startup chain verbatim. A
- * failed handshake rejects the deferred into the boot page's failure
- * rendering; this module owns no page painting.
+ * stock entry tag. The runtime's optional source stage owns the pre-Cordis
+ * chooser; the unchanged Host connector then owns the Worker handshake.
+ * Everything after those calls is the served startup chain verbatim.
  */
 import DshWorker from '@deepseek-ai/dsh-experimental-webworker-runtime/worker?worker'
-import { connectWorkerHost, IMAGE_FILE_NAME } from '@deepseek-ai/dsh-experimental-webworker-runtime/client'
+import {
+  chooseWorkerHostSource, connectWorkerHost, IMAGE_FILE_NAME,
+} from '@deepseek-ai/dsh-experimental-webworker-runtime/client'
 
-await connectWorkerHost(new DshWorker({ name: 'dsh-host' }), { image: `preview/${IMAGE_FILE_NAME}` })
+const image = `preview/${IMAGE_FILE_NAME}`
+const source = await chooseWorkerHostSource({ image })
+await connectWorkerHost(new DshWorker({ name: 'dsh-host' }), { image, overlays: source.overlays })

+ 191 - 44
apps/web/tests/preview-boot.e2e.ts

@@ -8,27 +8,33 @@
  * Two milestones prove that happened — the host's `tree active` boot line,
  * whose lowering contract must be the one this checkout's packer emits, and the
  * workspace hero, which paints only after the client tree comes up over the
- * tunnel. The same page then creates a Workspace and Session, lists skills,
- * and writes through the settings and credentials providers, exercising the
- * upstream Chokidar instances over the Worker filesystem implementation.
+ * tunnel. The same page opens the seeded Workspace and showcase Session,
+ * verifies its tool/subagent/history examples, then writes through the
+ * settings and credentials providers. That keeps the upstream Chokidar
+ * instances exercised over the Worker filesystem implementation.
  *
  * The site is served the way a static host serves it: bytes from `dist/` with
  * no rewrite rules, so a missing file is a 404 rather than the index page.
  */
-import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
+import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
 import { readFile } from 'node:fs/promises'
 import { createServer } from 'node:http'
 import type { IncomingMessage, ServerResponse } from 'node:http'
 import { tmpdir } from 'node:os'
-import { extname, join, normalize } from 'node:path'
+import { dirname, extname, join, normalize } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { chromium } from 'playwright'
 import type { Browser } from 'playwright'
 import { expect, it } from 'vitest'
 import {
-  composeProfile, configTrees, indexWorkspacePackages, packVfsImage, WRAPPER_CONTRACT,
+  composeProfile, configTrees, indexWorkspacePackages, packVfsImage, packVfsOverlay,
+  previewFixtures, WRAPPER_CONTRACT,
 } from '@deepseek-ai/dsh-experimental-webworker-packer'
-import { IMAGE_FILE_NAME } from '@deepseek-ai/dsh-experimental-webworker-runtime'
+import {
+  IMAGE_FILE_NAME, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
+  type PreviewFixtureManifest,
+} from '@deepseek-ai/dsh-experimental-webworker-runtime'
+import { captureStableAria, compareOrRefreshGolden, webSnapshotMode } from './scaffold.ts'
 import { newEnglishPage, REPO_ROOT, saveFailureShot } from './support.ts'
 
 const DIST_ROOT = fileURLToPath(new URL('../dist', import.meta.url))
@@ -36,9 +42,22 @@ const DIST_ROOT = fileURLToPath(new URL('../dist', import.meta.url))
 /** Where the client looks for the image: the runtime's own name, beside the page. */
 const IMAGE_FILE = join(DIST_ROOT, 'preview', IMAGE_FILE_NAME)
 
+/** Built-in source catalog read by the pre-boot chooser. */
+const FIXTURE_MANIFEST_FILE = join(DIST_ROOT, 'preview', PREVIEW_FIXTURE_MANIFEST_FILE)
+
+/** Keyless browser golden for the pre-Worker source chooser. */
+const SOURCE_CHOOSER_EXPECTED = fileURLToPath(new URL('./snapshots/preview-boot/source-chooser.expected.md', import.meta.url))
+
+const SNAPSHOT_MODE = webSnapshotMode()
+
 /** Profile the preview deployment composes; `build:preview` packs the same one. */
 const PROFILE = 'web'
 
+/** Stable labels authored by the deterministic VFS example fixture. */
+const SHOWCASE_TITLE = 'WebWorker Preview Showcase'
+const SHOWCASE_TAIL = 'Preview tour complete'
+const SHOWCASE_OLDEST = 'History checkpoint 01: verify deterministic preview state.'
+
 /** Pages the preview needs; the Vite build emits both. */
 const PAGES = ['index.html', 'preview.html']
 
@@ -77,6 +96,12 @@ interface Site {
   close(): Promise<void>
 }
 
+interface PreviewAssets {
+  /** Static-host-relative path to a generated file outside `dist/`. */
+  readonly overrides: ReadonlyMap<string, string>
+  cleanup(): void
+}
+
 /**
  * Fail before the browser opens a page the build never produced.
  * @throws When either preview page is missing from `dist/`.
@@ -89,20 +114,25 @@ function requirePreviewPages(): void {
 }
 
 /**
- * The image file to serve, packed here when `dist/` carries none: `pnpm run
- * build` emits the pages but only `build:preview` packs, so this lane packs
- * for itself rather than skipping the deployment it is here to accept. An
- * image already in place is used as it stands — the worker refuses one lowered
- * against another wrapper contract, and that refusal names the rebuild. A
- * self-packed image lands in a temp directory, never in `dist/`: the
+ * The base image, fixture manifest, and overlays to serve, packed here when
+ * `dist/` does not carry the complete set: `pnpm run build` emits the pages but
+ * only `build:preview` packs these files, so this lane packs for itself rather
+ * than skipping the deployment it accepts. A complete built set is used as it
+ * stands — the worker refuses a base lowered against another wrapper contract.
+ * Self-packed files land in a temp directory, never in `dist/`: the
  * client-artifact digest record treats `dist/` as build-owned, so a test write
  * there fails the record check for every later consumer.
- * @returns The file to answer `preview/<image>` with, and its teardown.
+ * @returns Static-path overrides and their teardown.
  * @throws When the closure leaves dependencies unresolved, which would pack an
  * incomplete image the tree fails on later and further from the cause.
  */
-function requireVfsImage(): { path: string; cleanup(): void } {
-  if (existsSync(IMAGE_FILE)) return { path: IMAGE_FILE, cleanup: () => {} }
+function requireVfsAssets(): PreviewAssets {
+  const fixtureDefinitions = previewFixtures(REPO_ROOT)
+  const fixtureFiles = fixtureDefinitions.map(fixture =>
+    join(DIST_ROOT, 'preview', 'fixtures', `${fixture.id}.tar.gz`))
+  if ([IMAGE_FILE, FIXTURE_MANIFEST_FILE, ...fixtureFiles].every(existsSync)) {
+    return { overrides: new Map(), cleanup: () => {} }
+  }
   const packed = packVfsImage({
     config: composeProfile(REPO_ROOT, PROFILE),
     profile: PROFILE,
@@ -114,23 +144,48 @@ function requireVfsImage(): { path: string; cleanup(): void } {
     throw new Error(`preview boot: ${String(packed.missing.length)} dependencies did not resolve: ${packed.missing.join(', ')}`)
   }
   const directory = mkdtempSync(join(tmpdir(), 'dsh-preview-boot-'))
-  const path = join(directory, IMAGE_FILE_NAME)
-  writeFileSync(path, packed.image)
-  return { path, cleanup: () => { rmSync(directory, { recursive: true, force: true }) } }
+  const overrides = new Map<string, string>()
+  const writeAsset = (relativePath: string, bytes: Uint8Array | string): void => {
+    const path = join(directory, relativePath)
+    mkdirSync(dirname(path), { recursive: true })
+    writeFileSync(path, bytes)
+    overrides.set(relativePath, path)
+  }
+  writeAsset(`preview/${IMAGE_FILE_NAME}`, packed.image)
+  const fixtures = fixtureDefinitions.map((fixture) => {
+    const relativePath = `preview/fixtures/${fixture.id}.tar.gz`
+    writeAsset(relativePath, packVfsOverlay(fixture.trees).image)
+    return {
+      id: fixture.id,
+      label: fixture.label,
+      description: fixture.description,
+      overlays: [`fixtures/${fixture.id}.tar.gz`],
+    }
+  })
+  const manifest: PreviewFixtureManifest = {
+    version: PREVIEW_FIXTURE_MANIFEST_VERSION,
+    defaultFixture: fixtures[0]?.id ?? null,
+    fixtures,
+  }
+  writeAsset(`preview/${PREVIEW_FIXTURE_MANIFEST_FILE}`, `${JSON.stringify(manifest, null, 2)}\n`)
+  return { overrides, cleanup: () => { rmSync(directory, { recursive: true, force: true }) } }
 }
 
 /**
- * Answer one request with the file it names under `dist/`; the image path
- * answers from wherever {@link requireVfsImage} put the file.
+ * Answer one request with its generated override or the file under `dist/`.
  * @param request - Incoming request; only its path is read.
  * @param response - Response to write the bytes or the 404 to.
- * @param imagePath - File behind `preview/<image>`.
+ * @param overrides - Generated deployment files used when `dist/` has none.
  */
-async function respond(request: IncomingMessage, response: ServerResponse, imagePath: string): Promise<void> {
+async function respond(
+  request: IncomingMessage,
+  response: ServerResponse,
+  overrides: ReadonlyMap<string, string>,
+): Promise<void> {
   const path = new URL(request.url ?? '/', 'http://127.0.0.1').pathname
   const relative = normalize(decodeURIComponent(path)).replace(/^\/+/, '')
   try {
-    const body = await readFile(relative === `preview/${IMAGE_FILE_NAME}` ? imagePath : join(DIST_ROOT, relative))
+    const body = await readFile(overrides.get(relative) ?? join(DIST_ROOT, relative))
     response.writeHead(200, { 'content-type': MIME[extname(relative)] ?? 'application/octet-stream' })
     response.end(body)
   } catch {
@@ -144,11 +199,11 @@ async function respond(request: IncomingMessage, response: ServerResponse, image
 
 /**
  * Serve `dist/` over loopback with static-host semantics.
- * @param imagePath - File behind `preview/<image>`.
+ * @param overrides - Generated deployment files used when `dist/` has none.
  * @returns The origin to navigate, and its teardown.
  */
-async function serveDist(imagePath: string): Promise<Site> {
-  const server = createServer((request, response) => { void respond(request, response, imagePath) })
+async function serveDist(overrides: ReadonlyMap<string, string>): Promise<Site> {
+  const server = createServer((request, response) => { void respond(request, response, overrides) })
   await new Promise<void>((listening) => { server.listen(0, '127.0.0.1', listening) })
   const address = server.address()
   if (address === null || typeof address === 'string') throw new Error('preview boot: the static server bound no port')
@@ -188,12 +243,13 @@ async function within<T>(work: Promise<T>, ms: number, stalled: string): Promise
 
 it('boots the packed worker deployment to an interactive page', async () => {
   requirePreviewPages()
-  const image = requireVfsImage()
+  const assets = requireVfsAssets()
   try {
-    const site = await serveDist(image.path)
+    const site = await serveDist(assets.overrides)
     try {
       const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] })
       try {
+        await bootEmptyPreview(site.origin, browser)
         await bootPreview(site.origin, browser)
       } finally {
         await browser.close()
@@ -202,7 +258,7 @@ it('boots the packed worker deployment to an interactive page', async () => {
       await site.close()
     }
   } finally {
-    image.cleanup()
+    assets.cleanup()
   }
 }, 600_000)
 
@@ -227,26 +283,34 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
   })
   try {
     await page.goto(`${origin}/preview.html`, { waitUntil: 'domcontentloaded' })
+    await page.getByRole('heading', { name: '选择 Preview 数据源' }).waitFor()
+    expect(await page.locator('input[name="preview-source"][value="vfs-example"]').isChecked()).toBe(true)
+    expect(await page.getByText('空白环境', { exact: true }).count()).toBe(1)
+    expect(await page.getByText('WebFS 目录', { exact: true }).count()).toBe(1)
+    expect(await page.locator('input[name="preview-source"][value="webfs"]').isDisabled()).toBe(true)
+    expect(await page.getByRole('textbox', { name: 'Choose workspace' }).count()).toBe(0)
+    await compareOrRefreshGolden(
+      SOURCE_CHOOSER_EXPECTED,
+      await captureStableAria(page, '[data-preview-source-card]', '/__preview_no_workspace__'),
+      SNAPSHOT_MODE,
+    )
+    await page.getByRole('button', { name: '启动 Preview' }).click()
     const bootLine = await within(treeActive, BOOT_TIMEOUT_MS, `preview boot: the worker never reported "${TREE_ACTIVE}"`)
     // The activated tree ran bodies lowered against the contract this
     // checkout's packer emits; a dist built before a contract change would
     // report the older one.
     expect(bootLine).toContain(`image lowering=${WRAPPER_CONTRACT}`)
+    expect(bootLine).toContain('data overlays=1')
     // The hero's workspace picker is the client tree's first interactive
     // surface, so it appears only once the startup chain completed over the
     // tunnel.
     await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: HERO_TIMEOUT_MS })
     const continueButton = page.getByRole('button', { name: 'Continue' })
-    if (await continueButton.isVisible()) await continueButton.click()
-    await page.getByRole('button', { name: 'Configure later' }).click()
-    await page.getByRole('textbox', { name: 'Choose workspace' }).click()
-    const dialog = page.getByRole('dialog', { name: 'Select Workspace Directory' })
-    await dialog.waitFor({ timeout: 10_000 })
-    await dialog.getByRole('button', { name: 'Edit path' }).click()
-    const pathInput = dialog.getByRole('textbox', { name: 'Edit path' })
-    await pathInput.fill('/dsh/workspace')
-    await pathInput.press('Enter')
-    await dialog.getByRole('button', { name: 'Open', exact: true }).click()
+    await continueButton.waitFor({ timeout: 30_000 })
+    await continueButton.click()
+    const configureLater = page.getByRole('button', { name: 'Configure later' })
+    await configureLater.waitFor({ timeout: 30_000 })
+    await configureLater.click()
     await page.locator('textarea:enabled[placeholder="Describe what you want to build"]')
       .waitFor({ timeout: 30_000 })
 
@@ -296,9 +360,7 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
         const refreshed = await api.skills.list({ sessionId })
         if (!refreshed.result.ok) throw new Error(`skill.list refresh failed: ${refreshed.result.error.message}`)
       }
-      await createDirectory('/dsh/workspace', '.agents')
-      await createDirectory('/dsh/workspace/.agents', 'skills')
-      await createDirectory('/dsh/workspace/.agents/skills', 'placeholder')
+      await createDirectory('/dsh/workspace/.agents/skills', 'runtime-created')
       const settings = await api.settings.describe({})
       if (!settings.result.ok) throw new Error(`settings.describe failed: ${settings.result.error.message}`)
       const shell = settings.result.value.namespaces.find(namespace => namespace.ns === 'shell')
@@ -317,8 +379,31 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
         credentialConfigured: credentials.result.value.credentials.PREVIEW_TEST_SECRET?.configured,
       }
     })
-    expect(exercised.skillCount).toBeGreaterThanOrEqual(0)
+    expect(exercised.skillCount).toBeGreaterThan(0)
     expect(exercised.credentialConfigured).toBe(true)
+
+    const sessions = page.getByRole('tree', { name: 'Sessions' })
+    const showcase = sessions.getByRole('treeitem').filter({ hasText: SHOWCASE_TITLE })
+    await expect.poll(() => showcase.count(), { timeout: 15_000 }).toBe(1)
+    await showcase.click()
+    await page.getByText(SHOWCASE_TAIL, { exact: true }).waitFor({ timeout: 30_000 })
+
+    expect(await page.getByText(SHOWCASE_OLDEST, { exact: true }).count()).toBe(0)
+    await page.getByText('PREVIEW.md', { exact: true }).waitFor()
+    await page.getByText('src/preview.ts', { exact: true }).waitFor()
+    await page.getByText('Update to-do list', { exact: true }).waitFor()
+    await page.getByText('Error: ENOENT: no such file, open missing.txt', { exact: true }).waitFor()
+
+    const subagents = page.getByRole('button', { name: '2 subagents' })
+    await subagents.waitFor({ timeout: 15_000 })
+    await subagents.hover()
+    const catalog = page.getByRole('tree', { name: 'Subagent sessions' })
+    await catalog.getByRole('treeitem', { name: /Review preview architecture/ }).waitFor()
+    await catalog.getByRole('treeitem', { name: /Continue preview verification/ }).waitFor()
+    await catalog.press('Escape')
+
+    await page.getByRole('button', { name: 'Load earlier', exact: true }).click()
+    await page.getByText(SHOWCASE_OLDEST, { exact: true }).waitFor({ timeout: 15_000 })
     expect(pageErrors.map(error => error.message)).toEqual([])
     expect(consoleErrors.filter(line =>
       /watchFile|failed to watch|node-addon-landlock-run\.probe|sandbox backend is usable|SANDBOX_UNAVAILABLE/i.test(line))).toEqual([])
@@ -329,3 +414,65 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
       : new AggregateError([error, ...pageErrors], 'preview boot failed, with uncaught page errors')
   }
 }
+
+/** Verify the chooser can boot the untouched base image and reach first-run UI. */
+async function bootEmptyPreview(origin: string, browser: Browser): Promise<void> {
+  const page = await newEnglishPage(browser)
+  const pageErrors: Error[] = []
+  const consoleErrors: string[] = []
+  const failedResponses: string[] = []
+  page.on('pageerror', (error) => { pageErrors.push(error) })
+  page.on('response', (response) => {
+    if (response.status() >= 400) failedResponses.push(new URL(response.url()).pathname)
+  })
+  const treeActive = new Promise<string>((reported) => {
+    page.on('console', (message) => {
+      const text = message.text()
+      if (text.includes(TREE_ACTIVE)) reported(text)
+      if (message.type() === 'error' || message.type() === 'warning') consoleErrors.push(text)
+    })
+  })
+  try {
+    await page.goto(`${origin}/preview.html?preview-fixture=none`, { waitUntil: 'domcontentloaded' })
+    expect(await page.getByRole('heading', { name: '选择 Preview 数据源' }).count()).toBe(0)
+    const bootLine = await within(
+      treeActive,
+      BOOT_TIMEOUT_MS,
+      `empty preview boot: the worker never reported "${TREE_ACTIVE}"`,
+    )
+    expect(bootLine).toContain(`image lowering=${WRAPPER_CONTRACT}`)
+    expect(bootLine).toContain('data overlays=0')
+    await page.getByRole('textbox', { name: 'Choose workspace' }).waitFor({ timeout: HERO_TIMEOUT_MS })
+    const sessionCount = await page.evaluate(async () => {
+      const transport = (globalThis as typeof globalThis & {
+        __DSH_TRANSPORT__?: { fetch(input: string, init: RequestInit): Promise<Response> }
+      }).__DSH_TRANSPORT__
+      if (transport === undefined) throw new Error('empty preview transport is absent after boot')
+      const response = await transport.fetch('/api/session/list', {
+        method: 'POST',
+        headers: { 'content-type': 'application/json' },
+        body: JSON.stringify({
+          type: 'client-request', rpcId: 'empty-preview-session-list', method: 'session/list',
+          payload: { args: { _request: {} } },
+        }),
+      })
+      const body = await response.json() as {
+        result: { ok: true; value: { items: unknown[] } } | { ok: false; error: { message: string } }
+      }
+      if (!body.result.ok) throw new Error(`empty session/list failed: ${body.result.error.message}`)
+      return body.result.value.items.length
+    })
+    expect(sessionCount).toBe(0)
+    expect(pageErrors.map(error => error.message)).toEqual([])
+    expect(failedResponses).toEqual(['/plugins/events'])
+    expect(consoleErrors.filter(line => !line.includes('Failed to load resource: the server responded with a status of 404')))
+      .toEqual([])
+  } catch (error) {
+    await saveFailureShot(page, 'preview-boot-empty')
+    throw pageErrors.length === 0
+      ? error
+      : new AggregateError([error, ...pageErrors], 'empty preview boot failed, with uncaught page errors')
+  } finally {
+    await page.close()
+  }
+}

+ 15 - 0
apps/web/tests/snapshots/preview-boot/source-chooser.expected.md

@@ -0,0 +1,15 @@
+- form "选择 Preview 数据源":
+  - heading "选择 Preview 数据源" [level=1]
+  - paragraph: 数据会在 Worker 和应用启动前挂载;刷新页面可重新选择。
+  - group "文件系统来源":
+    - text: 文件系统来源
+    - radio "空白环境 只加载基础运行时,用于验证首次启动与新建 Workspace。"
+    - strong: 空白环境
+    - text: 只加载基础运行时,用于验证首次启动与新建 Workspace。
+    - radio "内置综合示例 示例 Workspace、工具卡、子代理与分页会话。" [checked]
+    - strong: 内置综合示例
+    - text: 示例 Workspace、工具卡、子代理与分页会话。
+    - radio "WebFS 目录 需要用户授权的目录来源,将在 WebFS provider 接入后开放。" [disabled]
+    - strong: WebFS 目录
+    - text: 需要用户授权的目录来源,将在 WebFS provider 接入后开放。
+  - button "启动 Preview"

+ 2 - 1
knip.json

@@ -241,7 +241,8 @@
     "packages/experimental/webworker-runtime": {
       "entry": [
         "tests/**/*.spec.ts",
-        "tests/compile/transform-corpus-check.ts"
+        "tests/compile/transform-corpus-check.ts",
+        "tests/fixtures/vfs-example/workspace/src/preview.ts"
       ],
       "project": [
         "src/**/*.ts",

+ 1 - 1
packages/experimental/webworker-packer/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@deepseek-ai/dsh-experimental-webworker-packer",
-  "description": "Build-time packer for the browser runtime's VFS image: materializes a profile's package closure into one gzip-compressed tar the worker mounts, with every module body pre-transformed",
+  "description": "Build-time packer for the browser runtime's base VFS image and ordered data-overlay archives",
   "version": "0.1.1-rc.2",
   "private": true,
   "repository": {

+ 38 - 6
packages/experimental/webworker-packer/src/bin.ts

@@ -1,17 +1,23 @@
 #!/usr/bin/env node
 /**
- * Pack a VFS image from this repository: compose the profile, materialize the
- * closure, lower every module body, write the gzip-compressed tar.
+ * Pack a Preview deployment from this repository: compose and lower the base
+ * image, then write each named fixture overlay and their manifest.
  *
  * Usage: dsh-pack-vfs-image --out <file> [--profile web] [--root /dsh]
  *        node --import tsx/esm src/bin.ts --out ../../apps/web/dist/preview/vfs-image.tar.gz
  * @module @deepseek-ai/dsh-experimental-webworker-packer/src/bin
  */
 import { mkdirSync, writeFileSync } from 'node:fs'
-import { dirname, isAbsolute, resolve } from 'node:path'
+import { dirname, isAbsolute, join, resolve } from 'node:path'
 import { fileURLToPath } from 'node:url'
-import { packVfsImage } from './pack.ts'
-import { composeProfile, configTrees, describePack, indexWorkspacePackages } from './repository.ts'
+import {
+  PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
+  type PreviewFixtureManifest,
+} from '@deepseek-ai/dsh-experimental-webworker-runtime'
+import { packVfsImage, packVfsOverlay } from './pack.ts'
+import {
+  composeProfile, configTrees, describePack, indexWorkspacePackages, previewFixtures,
+} from './repository.ts'
 
 /**
  * Read one `--flag value` pair.
@@ -54,4 +60,30 @@ if (result.missing.length > 0) {
 
 mkdirSync(dirname(outputFile), { recursive: true })
 writeFileSync(outputFile, result.image)
-process.stdout.write(describePack(result, repoRoot, outputFile).join('\n'))
+
+const fixtureDefinitions = previewFixtures(repoRoot)
+const fixtureDirectory = join(dirname(outputFile), 'fixtures')
+mkdirSync(fixtureDirectory, { recursive: true })
+const fixtureLines: string[] = []
+const fixtures = fixtureDefinitions.map((fixture) => {
+  const packed = packVfsOverlay(fixture.trees)
+  const file = `fixtures/${fixture.id}.tar.gz`
+  writeFileSync(join(dirname(outputFile), file), packed.image)
+  fixtureLines.push(`  fixture overlay     ${fixture.id} (${String(packed.image.byteLength)} B compressed)`)
+  return {
+    id: fixture.id,
+    label: fixture.label,
+    description: fixture.description,
+    overlays: [file],
+  }
+})
+const manifest: PreviewFixtureManifest = {
+  version: PREVIEW_FIXTURE_MANIFEST_VERSION,
+  defaultFixture: fixtures[0]?.id ?? null,
+  fixtures,
+}
+writeFileSync(
+  join(dirname(outputFile), PREVIEW_FIXTURE_MANIFEST_FILE),
+  `${JSON.stringify(manifest, null, 2)}\n`,
+)
+process.stdout.write([...describePack(result, repoRoot, outputFile), ...fixtureLines, ''].join('\n'))

+ 4 - 3
packages/experimental/webworker-packer/src/index.ts

@@ -7,9 +7,10 @@ export {
   type ImageFiles, type TransformOutcome,
 } from './transform-image.ts'
 export {
-  CONFIG_PATH, DEFAULT_ROOT, MANIFEST_PATH, packVfsImage,
-  type ConfigTree, type PackOptions, type PackResult,
+  CONFIG_PATH, DEFAULT_ROOT, MANIFEST_PATH, packVfsImage, packVfsOverlay,
+  type ConfigTree, type ImageTree, type PackOptions, type PackOverlayResult, type PackResult,
 } from './pack.ts'
 export {
-  composeProfile, configTrees, describePack, indexWorkspacePackages,
+  composeProfile, configTrees, describePack, indexWorkspacePackages, previewFixtures,
+  type PreviewFixture,
 } from './repository.ts'

+ 58 - 8
packages/experimental/webworker-packer/src/pack.ts

@@ -19,6 +19,7 @@ import { gzipSync } from 'node:zlib'
 import {
   lowerModuleSource, MemoryVfs, packTar, WorkerModuleLoader,
   DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_MANIFEST_PATH,
+  IMAGE_OVERLAY_DIRECTORIES,
 } from '@deepseek-ai/dsh-experimental-webworker-runtime'
 import picomatch from 'picomatch'
 import yaml from 'js-yaml'
@@ -52,12 +53,16 @@ const workspaceExcluded = picomatch([...EXCLUDE, ...EXCLUDE_WORKSPACE], { dot: t
 /** Page-asset matcher over image paths ({@link PAGE_ASSETS}). */
 const pageAsset = picomatch([...PAGE_ASSETS], { dot: true })
 
-/** One directory tree to copy in verbatim beside the composition. */
-export interface ConfigTree {
+/** One directory tree to copy into the image at a caller-selected mount. */
+export interface ImageTree {
   /** Image path to mount it at, relative to the virtual root. */
   readonly mount: string
   /** Absolute source directory. */
   readonly directory: string
+}
+
+/** One configuration tree whose plugin rows may extend the package roster. */
+export interface ConfigTree extends ImageTree {
   /**
    * Whether plugin names inside its `.yml` files join the materialization closure.
    * An agent preset mounts plugins the base composition never lists, and creating a
@@ -119,6 +124,14 @@ export interface PackResult {
   readonly contract: string
 }
 
+/** One deterministic data-overlay archive and its uncompressed entries. */
+export interface PackOverlayResult {
+  /** Gzip-compressed ustar bytes consumed by the Worker host. */
+  readonly image: Uint8Array
+  /** Every path in the overlay before compression. */
+  readonly files: ImageFiles
+}
+
 const readJson = (file: string): Record<string, unknown> =>
   JSON.parse(readFileSync(file, 'utf8')) as Record<string, unknown>
 
@@ -204,20 +217,28 @@ function resolveDependency(fromDirectory: string, name: string): string | undefi
 
 /**
  * Collect files under one directory. Traversal mechanics live here — nested
- * `node_modules` never mounts (the image is flat) and dot directories are
- * tooling residue at any depth — while every judgement call comes in through
- * `keep` (the {@link EXCLUDE} tables and the npm publish view).
+ * package/config collection flattens nested `node_modules` and prunes dot
+ * directories, while seed collection preserves every directory. Every file
+ * judgement comes in through `keep` (the {@link EXCLUDE} tables and the npm
+ * publish view, or an unconditional seed predicate).
  * @param root - Source directory.
  * @param into - Image entries to add to.
  * @param prefix - Image path prefix.
  * @param keep - Filter over root-relative paths.
+ * @param preserveDirectories - Whether dot directories and nested `node_modules`
+ *   are ordinary fixture content rather than package-manager residue.
  */
-function collectTree(root: string, into: ImageFiles, prefix: string, keep: (relativePath: string) => boolean): void {
+function collectTree(
+  root: string,
+  into: ImageFiles,
+  prefix: string,
+  keep: (relativePath: string) => boolean,
+  preserveDirectories = false,
+): void {
   const walk = (directory: string): void => {
     for (const entry of readdirSync(directory, { withFileTypes: true })) {
       if (entry.isDirectory()) {
-        if (entry.name === 'node_modules') continue
-        if (entry.name.startsWith('.')) continue
+        if (!preserveDirectories && (entry.name === 'node_modules' || entry.name.startsWith('.'))) continue
         walk(join(directory, entry.name))
         continue
       }
@@ -621,3 +642,32 @@ export function packVfsImage(options: PackOptions): PackResult {
     contract: WRAPPER_CONTRACT,
   }
 }
+
+/**
+ * Pack opaque data trees into one ordered VFS overlay.
+ *
+ * Overlay mounts are restricted to the runtime-owned data directories, so an
+ * overlay cannot replace configuration, the lowering manifest, or modules.
+ * Files bypass package excludes and module reachability processing; later
+ * trees replace earlier files at the same path.
+ * @param trees - Absolute source directories and their data-directory mounts.
+ * @returns Deterministic compressed archive plus its uncompressed entries.
+ */
+export function packVfsOverlay(trees: readonly ImageTree[]): PackOverlayResult {
+  const files: ImageFiles = {}
+  for (const tree of trees) {
+    if (!existsSync(tree.directory)) {
+      throw new Error(`vfs overlay: tree ${tree.mount} is missing at ${tree.directory}`)
+    }
+    const mount = tree.mount.replace(/^\.\//, '').replace(/\/$/, '')
+    const first = mount.split('/')[0]
+    if (mount === '' || first === undefined || !IMAGE_OVERLAY_DIRECTORIES.includes(first)
+      || mount.split('/').some(segment => segment === '' || segment === '.' || segment === '..')) {
+      throw new Error(
+        `vfs overlay: mount ${JSON.stringify(tree.mount)} must stay under ${IMAGE_OVERLAY_DIRECTORIES.join(' or ')}`,
+      )
+    }
+    collectTree(tree.directory, files, mount, () => true, true)
+  }
+  return { image: compressImage(packTar(files)), files }
+}

+ 33 - 1
packages/experimental/webworker-packer/src/repository.ts

@@ -12,7 +12,7 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from 'node
 import { tmpdir } from 'node:os'
 import { join, relative } from 'node:path'
 import { DSH_HOME_ENV } from '@deepseek-ai/dsh-home-paths'
-import type { ConfigTree, PackResult } from './pack.ts'
+import type { ConfigTree, ImageTree, PackResult } from './pack.ts'
 
 /**
  * Repository directories scanned for workspace and vendored packages. The
@@ -28,6 +28,21 @@ const CLI_PACKAGE = 'apps/cli'
 /** Composition entry point: the `dsh` CLI, run from source. */
 const CLI_ENTRY = `${CLI_PACKAGE}/src/bin.ts`
 
+/** Repository-owned deterministic filesystem content offered by the preview. */
+const PREVIEW_EXAMPLE_ROOT = 'packages/experimental/webworker-runtime/tests/fixtures/vfs-example'
+
+/** One built-in Preview source and the trees packed into its overlay. */
+export interface PreviewFixture {
+  /** URL/query-safe identifier. */
+  readonly id: string
+  /** User-facing chooser label. */
+  readonly label: string
+  /** User-facing chooser detail. */
+  readonly description: string
+  /** Opaque trees packed into this fixture's overlay archive. */
+  readonly trees: readonly ImageTree[]
+}
+
 /**
  * Index every workspace and vendored package by name.
  * @param repoRoot - Absolute repository root.
@@ -130,6 +145,23 @@ export function configTrees(repoRoot: string): ConfigTree[] {
   })
 }
 
+/**
+ * Built-in filesystem fixtures offered by the repository preview.
+ * Session and Workspace semantics remain opaque here; the owning runtime tests
+ * validate those files through their production readers.
+ * @param repoRoot - Absolute repository root.
+ * @returns Named chooser entries and their overlay trees.
+ */
+export function previewFixtures(repoRoot: string): PreviewFixture[] {
+  const root = join(repoRoot, PREVIEW_EXAMPLE_ROOT)
+  return [{
+    id: 'vfs-example',
+    label: '内置综合示例',
+    description: '示例 Workspace、工具卡、子代理与分页会话。',
+    trees: ['home', 'workspace'].map(mount => ({ mount, directory: join(root, mount) })),
+  }]
+}
+
 /**
  * Render one pack as the lines a build log should carry.
  *

+ 28 - 2
packages/experimental/webworker-packer/tests/image-loadable.spec.ts

@@ -26,8 +26,8 @@ import {
 } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/module-system/module-loader.ts'
 import { inflateImage } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/image-gzip.ts'
 import { loadVfsImage } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/memory.ts'
-import { indexWorkspacePackages } from '../src/repository.ts'
-import { DEFAULT_ROOT, MANIFEST_PATH, packVfsImage } from '../src/pack.ts'
+import { indexWorkspacePackages, previewFixtures } from '../src/repository.ts'
+import { DEFAULT_ROOT, MANIFEST_PATH, packVfsImage, packVfsOverlay } from '../src/pack.ts'
 
 const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url))
 
@@ -37,6 +37,32 @@ const LANDLOCK = '@deepseek-ai/node-addon-landlock-run'
 
 const workspaces = indexWorkspacePackages(repoRoot)
 
+describe('preview example overlays', () => {
+  it('packs source-looking paths and dot directories into a separate overlay', () => {
+    const fixture = previewFixtures(repoRoot)[0]
+    expect(fixture?.id).toBe('vfs-example')
+    const result = packVfsOverlay(fixture?.trees ?? [])
+    expect(new TextDecoder().decode(result.files['workspace/src/preview.ts']))
+      .toContain("previewStatus = 'ready'")
+    expect(new TextDecoder().decode(result.files['workspace/.agents/skills/preview-tour/SKILL.md']))
+      .toContain('name: preview-tour')
+    expect(Object.keys(result.files).filter(path => path.endsWith('/session.jsonl'))).toHaveLength(3)
+  })
+
+  it('fails loud when a declared seed tree is absent', () => {
+    expect(() => packVfsOverlay([
+      { mount: 'workspace', directory: join(repoRoot, 'missing-preview-seed') },
+    ])).toThrow(/tree workspace is missing/)
+  })
+
+  it('refuses overlays that could replace runtime files', () => {
+    const fixture = previewFixtures(repoRoot)[0]
+    expect(() => packVfsOverlay([
+      { mount: 'config', directory: fixture?.trees[0]?.directory ?? repoRoot },
+    ])).toThrow(/must stay under home or workspace/)
+  })
+})
+
 /**
  * The pack consumes built `lib/` output. An unbuilt checkout (the unit
  * coverage lane runs before any build) self-skips; the built lanes and every

+ 3 - 0
packages/experimental/webworker-runtime/package.json

@@ -57,6 +57,9 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-sandbox-local": "workspace:^",
     "@deepseek-ai/dsh-sandbox-policy": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
+    "@deepseek-ai/dsh-subagent": "workspace:^",
     "@deepseek-ai/dsh-subprocess-local": "workspace:^",
     "@deepseek-ai/node-addon-landlock-run": "workspace:^",
     "@types/picomatch": "^3.0.2",

+ 3 - 2
packages/experimental/webworker-runtime/src/client/client.ts

@@ -156,9 +156,10 @@ export class WorkerTunnel {
   /**
    * Open the tunnel: the worker assembles its host from this frame.
    * @param image - VFS image URL the worker fetches.
+   * @param overlays - Ordered data overlay URLs applied before boot.
    */
-  init(image: string): void {
-    this.worker.postMessage({ t: 'init', image })
+  init(image: string, overlays: readonly string[] = []): void {
+    this.worker.postMessage({ t: 'init', image, overlays })
   }
 
   /** Fetch-shaped entry: one request frame, one Response (streamed when the worker streams). */

+ 70 - 6
packages/experimental/webworker-runtime/src/client/index.ts

@@ -9,14 +9,20 @@
  * @module @deepseek-ai/dsh-experimental-webworker-runtime/client
  */
 import { IMAGE_FILE_NAME } from '../image-layout.ts'
+import { PREVIEW_FIXTURE_MANIFEST_FILE } from '../fixture-manifest.ts'
 import { WorkerApiClient } from './api-client.ts'
 import { WorkerTunnel, type TunnelFetch } from './client.ts'
 import { applyIndexInjections } from './apply-injections.ts'
+import { choosePreviewSource } from './source-chooser.ts'
 
 export { WorkerApiClient } from './api-client.ts'
 export { WorkerTunnel, type TunnelFetch } from './client.ts'
 export { applyIndexInjections } from './apply-injections.ts'
 export { IMAGE_FILE_NAME } from '../image-layout.ts'
+export {
+  parsePreviewFixtureManifest, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
+  type PreviewFixtureManifest, type PreviewFixtureManifestEntry,
+} from '../fixture-manifest.ts'
 
 /** Transport global the connection plugin reads instead of building an HTTP carrier. */
 interface ClientTransportGlobal {
@@ -35,9 +41,25 @@ export interface WorkerHostConnectOptions {
   /**
    * VFS image URL, the one deployment-shaped input. Defaults to
    * {@link IMAGE_FILE_NAME} beside the page; a deployment that packs the
-   * image elsewhere passes its own URL.
+   * image elsewhere passes its own URL. Data overlays are independent.
    */
   readonly image?: string | URL
+  /** Ordered data overlay URLs, resolved against the page like the base image. */
+  readonly overlays?: readonly (string | URL)[]
+}
+
+/** Inputs for the optional pre-boot filesystem-source chooser. */
+export interface WorkerHostSourceOptions {
+  /** Base VFS image URL; defaults to {@link IMAGE_FILE_NAME} beside the page. */
+  readonly image?: string | URL
+  /** Fixture catalog URL; defaults to {@link PREVIEW_FIXTURE_MANIFEST_FILE} beside the image. */
+  readonly fixtureManifest?: string | URL
+}
+
+/** Filesystem inputs selected before {@link connectWorkerHost}. */
+export interface WorkerHostSource {
+  /** Ordered data overlays to pass through unchanged to the Host connection. */
+  readonly overlays: readonly URL[]
 }
 
 /** A page connected to a worker-hosted harness, ready to run a shell entry. */
@@ -53,12 +75,51 @@ interface BootReadyGlobal {
   __DSH_BOOT_READY__?: PromiseWithResolvers<void>
 }
 
+function bootReadyGate(): PromiseWithResolvers<void> {
+  return (globalThis as BootReadyGlobal).__DSH_BOOT_READY__ ??= Promise.withResolvers<void>()
+}
+
+/**
+ * Install the page boot barrier before an asynchronous source chooser waits
+ * for user input. The later {@link connectWorkerHost} call settles the same
+ * barrier.
+ */
+function holdWorkerHostBoot(): void {
+  const ready = bootReadyGate()
+  // A chooser may remain open indefinitely; if a later connection fails before
+  // the stock entry subscribes, retain the rejection without browser noise.
+  void ready.promise.catch(() => {})
+}
+
+/**
+ * Run the optional pre-boot source-selection stage. Calling this stage holds
+ * the stock shell until the caller passes its result to {@link connectWorkerHost};
+ * callers that need no chooser call `connectWorkerHost` directly and receive
+ * the base image with an empty overlay list.
+ * @param options - Base image and optional fixture-catalog locations.
+ * @returns The ordered overlays selected by the user.
+ */
+export async function chooseWorkerHostSource(
+  options: WorkerHostSourceOptions = {},
+): Promise<WorkerHostSource> {
+  holdWorkerHostBoot()
+  const image = new URL(options.image ?? IMAGE_FILE_NAME, document.baseURI)
+  const manifest = new URL(options.fixtureManifest ?? PREVIEW_FIXTURE_MANIFEST_FILE, image)
+  try {
+    const overlays = await choosePreviewSource(manifest)
+    return { overlays }
+  } catch (reason) {
+    bootReadyGate().reject(reason)
+    throw reason
+  }
+}
+
 /**
  * Connect a spawned host worker and complete the pre-Cordis handshake.
  *
  * The caller constructs the Worker so its bundler resolves the bundle URL
- * statically; the opening `init` frame then carries the image location, the
- * only input the worker takes from outside.
+ * statically; the opening `init` frame then carries the base image and ordered
+ * overlay locations.
  *
  * Order is fixed by the web boot protocol: the transport global must exist
  * before any bundle executes; the injection table then reproduces the served
@@ -70,17 +131,20 @@ interface BootReadyGlobal {
  * row has taken effect, and surfaces a failed handshake instead of
  * proceeding on missing globals.
  * @param worker - The host worker.
- * @param options - Image location override.
+ * @param options - Base-image and overlay location overrides.
  * @returns The connection; hand `loadBundle` to the shell entry's boot seam.
  */
 export async function connectWorkerHost(worker: Worker, options?: WorkerHostConnectOptions): Promise<WorkerHostConnection> {
-  const ready = (globalThis as BootReadyGlobal).__DSH_BOOT_READY__ ??= Promise.withResolvers<void>()
+  const ready = bootReadyGate()
   // The handshake may fail before any entry awaits the promise; this no-op
   // subscription keeps that from surfacing as an unhandled rejection.
   void ready.promise.catch(() => {})
   try {
     const tunnel = new WorkerTunnel(worker)
-    tunnel.init(new URL(options?.image ?? IMAGE_FILE_NAME, document.baseURI).href)
+    tunnel.init(
+      new URL(options?.image ?? IMAGE_FILE_NAME, document.baseURI).href,
+      (options?.overlays ?? []).map(overlay => new URL(overlay, document.baseURI).href),
+    )
     const payload = await tunnel.bootPayload()
     ;(globalThis as ClientTransportGlobal).__DSH_TRANSPORT__ = {
       createApiClient: () => new WorkerApiClient(tunnel),

+ 178 - 0
packages/experimental/webworker-runtime/src/client/source-chooser.ts

@@ -0,0 +1,178 @@
+/** Pre-boot filesystem-source chooser for static WebWorker previews. */
+
+import {
+  parsePreviewFixtureManifest, type PreviewFixtureManifestEntry,
+} from '../fixture-manifest.ts'
+
+const EMPTY_SOURCE = 'none'
+const WEBFS_SOURCE = 'webfs'
+const PREVIEW_FIXTURE_QUERY = 'preview-fixture'
+
+interface PreviewSourceChoice {
+  readonly id: string
+  readonly label: string
+  readonly description: string
+  readonly overlays: readonly URL[]
+  readonly disabled?: boolean
+}
+
+const CHOOSER_STYLE = `
+  :root { color-scheme: light dark; }
+  body { margin: 0; }
+  [data-preview-source-chooser] {
+    min-height: 100vh;
+    display: grid;
+    place-items: center;
+    padding: 24px;
+    box-sizing: border-box;
+    color: #171717;
+    background: radial-gradient(circle at 50% 35%, #eef4ff 0, #f8fafc 42%, #f3f4f6 100%);
+    font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+  }
+  [data-preview-source-card] {
+    width: min(560px, 100%);
+    box-sizing: border-box;
+    padding: 28px;
+    border: 1px solid #d8dee9;
+    border-radius: 20px;
+    background: rgba(255, 255, 255, 0.94);
+    box-shadow: 0 20px 60px rgba(15, 23, 42, 0.12);
+  }
+  [data-preview-source-card] h1 { margin: 0 0 6px; font-size: 24px; line-height: 1.25; }
+  [data-preview-source-card] > p { margin: 0 0 22px; color: #5b6472; }
+  [data-preview-source-card] fieldset { display: grid; gap: 10px; margin: 0; padding: 0; border: 0; }
+  [data-preview-source-card] legend { margin-bottom: 10px; font-weight: 650; }
+  [data-preview-source-option] {
+    display: grid;
+    grid-template-columns: auto 1fr;
+    gap: 2px 12px;
+    padding: 14px;
+    border: 1px solid #d8dee9;
+    border-radius: 12px;
+    cursor: pointer;
+  }
+  [data-preview-source-option]:has(input:checked) { border-color: #4777df; background: #edf3ff; }
+  [data-preview-source-option]:has(input:disabled) { cursor: not-allowed; opacity: 0.55; }
+  [data-preview-source-option] input { grid-row: 1 / span 2; margin: 4px 0 0; }
+  [data-preview-source-option] strong { font-size: 15px; }
+  [data-preview-source-option] span { color: #667085; }
+  [data-preview-source-submit] {
+    width: 100%;
+    margin-top: 20px;
+    padding: 11px 16px;
+    border: 0;
+    border-radius: 10px;
+    color: white;
+    background: #315fc7;
+    font: inherit;
+    font-weight: 650;
+    cursor: pointer;
+  }
+  [data-preview-source-submit]:disabled { cursor: not-allowed; opacity: 0.5; }
+  @media (prefers-color-scheme: dark) {
+    [data-preview-source-chooser] { color: #f4f4f5; background: radial-gradient(circle at 50% 35%, #172554 0, #111827 45%, #09090b 100%); }
+    [data-preview-source-card] { border-color: #374151; background: rgba(24, 24, 27, 0.96); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35); }
+    [data-preview-source-card] > p, [data-preview-source-option] span { color: #a1a1aa; }
+    [data-preview-source-option] { border-color: #3f3f46; }
+    [data-preview-source-option]:has(input:checked) { border-color: #7aa2ff; background: #172554; }
+  }
+`
+
+const ENTITIES: Readonly<Record<string, string>> = {
+  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
+}
+
+function escapeMarkup(value: string): string {
+  return value.replace(/[&<>"']/g, character => ENTITIES[character] ?? character)
+}
+
+function optionMarkup(choice: PreviewSourceChoice, selected: string): string {
+  return `<label data-preview-source-option>
+    <input type="radio" name="preview-source" value="${choice.id}"${choice.id === selected ? ' checked' : ''}${choice.disabled === true ? ' disabled' : ''}>
+    <strong>${escapeMarkup(choice.label)}</strong>
+    <span>${escapeMarkup(choice.description)}</span>
+  </label>`
+}
+
+function fixtureChoices(entries: readonly PreviewFixtureManifestEntry[], manifestUrl: URL): PreviewSourceChoice[] {
+  return entries.map(entry => ({
+    id: entry.id,
+    label: entry.label,
+    description: entry.description,
+    overlays: entry.overlays.map(overlay => new URL(overlay, manifestUrl)),
+  }))
+}
+
+/**
+ * Render the source chooser and wait for an enabled selection.
+ * @param manifestUrl - Built-in fixture catalog URL.
+ * @returns Ordered overlay URLs selected for the Worker mount.
+ */
+export async function choosePreviewSource(manifestUrl: URL): Promise<readonly URL[]> {
+  const requested = new URL(location.href).searchParams.get(PREVIEW_FIXTURE_QUERY)
+  if (requested === EMPTY_SOURCE) return []
+
+  const response = await fetch(manifestUrl)
+  if (!response.ok) {
+    throw new Error(`preview source chooser: fixture manifest returned ${String(response.status)}`)
+  }
+  const manifest = parsePreviewFixtureManifest(await response.json())
+  const choices: PreviewSourceChoice[] = [
+    {
+      id: EMPTY_SOURCE,
+      label: '空白环境',
+      description: '只加载基础运行时,用于验证首次启动与新建 Workspace。',
+      overlays: [],
+    },
+    ...fixtureChoices(manifest.fixtures, manifestUrl),
+    {
+      id: WEBFS_SOURCE,
+      label: 'WebFS 目录',
+      description: '需要用户授权的目录来源,将在 WebFS provider 接入后开放。',
+      overlays: [],
+      disabled: true,
+    },
+  ]
+  if (requested !== null) {
+    const requestedChoice = choices.find(choice => choice.id === requested && choice.disabled !== true)
+    if (requestedChoice === undefined) {
+      throw new Error(`preview source chooser: unknown or interactive source "${requested}"`)
+    }
+    return requestedChoice.overlays
+  }
+
+  const root = document.getElementById('root')
+  if (root === null) throw new Error('preview source chooser: missing #root')
+  const selected = manifest.defaultFixture ?? EMPTY_SOURCE
+  const style = document.createElement('style')
+  style.dataset.previewSourceStyle = ''
+  style.textContent = CHOOSER_STYLE
+  document.head.append(style)
+
+  root.innerHTML = `<main data-preview-source-chooser>
+    <form data-preview-source-card aria-labelledby="preview-source-title">
+      <h1 id="preview-source-title">选择 Preview 数据源</h1>
+      <p>数据会在 Worker 和应用启动前挂载;刷新页面可重新选择。</p>
+      <fieldset>
+        <legend>文件系统来源</legend>
+        ${choices.map(choice => optionMarkup(choice, selected)).join('')}
+      </fieldset>
+      <button data-preview-source-submit type="submit">启动 Preview</button>
+    </form>
+  </main>`
+  const form = root.querySelector<HTMLFormElement>('[data-preview-source-card]')
+  if (form === null) throw new Error('preview source chooser: form was not rendered')
+  const sourceId = await new Promise<string>((resolve, reject) => {
+    form.addEventListener('submit', (event) => {
+      event.preventDefault()
+      const value = new FormData(form).get('preview-source')
+      if (typeof value === 'string') resolve(value)
+      else reject(new Error('preview source chooser: no source selected'))
+    }, { once: true })
+  })
+  const choice = choices.find(candidate => candidate.id === sourceId && candidate.disabled !== true)
+  if (choice === undefined) throw new Error(`preview source chooser: unavailable source "${sourceId}"`)
+  root.replaceChildren()
+  style.remove()
+  return choice.overlays
+}

+ 67 - 0
packages/experimental/webworker-runtime/src/fixture-manifest.ts

@@ -0,0 +1,67 @@
+/** Browser-readable catalog of built-in Preview filesystem overlays. */
+
+/** Manifest format version emitted beside the base VFS image. */
+export const PREVIEW_FIXTURE_MANIFEST_VERSION = 1
+
+/** Leaf name resolved beside the base image. */
+export const PREVIEW_FIXTURE_MANIFEST_FILE = 'fixtures.json'
+
+/** One selectable built-in fixture and its ordered overlay archives. */
+export interface PreviewFixtureManifestEntry {
+  readonly id: string
+  readonly label: string
+  readonly description: string
+  readonly overlays: readonly string[]
+}
+
+/** Complete built-in fixture catalog consumed before Worker startup. */
+export interface PreviewFixtureManifest {
+  readonly version: number
+  readonly defaultFixture: string | null
+  readonly fixtures: readonly PreviewFixtureManifestEntry[]
+}
+
+function recordOf(value: unknown): Record<string, unknown> | undefined {
+  return typeof value === 'object' && value !== null && !Array.isArray(value)
+    ? value as Record<string, unknown>
+    : undefined
+}
+
+/**
+ * Validate the static fixture catalog before it controls Worker fetches.
+ * @param value - Parsed JSON response.
+ * @returns A detached manifest with unique ids and non-empty overlay lists.
+ */
+export function parsePreviewFixtureManifest(value: unknown): PreviewFixtureManifest {
+  const record = recordOf(value)
+  if (record?.version !== PREVIEW_FIXTURE_MANIFEST_VERSION || !Array.isArray(record.fixtures)) {
+    throw new Error(`preview fixture manifest must use version ${String(PREVIEW_FIXTURE_MANIFEST_VERSION)}`)
+  }
+  const fixtures: PreviewFixtureManifestEntry[] = []
+  const ids = new Set<string>()
+  for (const value of record.fixtures) {
+    const fixture = recordOf(value)
+    const id = fixture?.id
+    const label = fixture?.label
+    const description = fixture?.description
+    const overlays = fixture?.overlays
+    const overlayUrls = Array.isArray(overlays)
+      ? overlays.filter((overlay): overlay is string => typeof overlay === 'string' && overlay.length > 0)
+      : []
+    if (typeof id !== 'string' || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(id)
+      || id === 'none' || id === 'webfs'
+      || typeof label !== 'string' || label.length === 0
+      || typeof description !== 'string' || description.length === 0
+      || !Array.isArray(overlays) || overlays.length === 0 || overlayUrls.length !== overlays.length) {
+      throw new Error('preview fixture manifest contains an invalid fixture entry')
+    }
+    if (ids.has(id)) throw new Error(`preview fixture manifest repeats id "${id}"`)
+    ids.add(id)
+    fixtures.push({ id, label, description, overlays: overlayUrls })
+  }
+  const defaultFixture = record.defaultFixture
+  if (defaultFixture !== null && (typeof defaultFixture !== 'string' || !ids.has(defaultFixture))) {
+    throw new Error('preview fixture manifest defaultFixture does not name a fixture')
+  }
+  return { version: PREVIEW_FIXTURE_MANIFEST_VERSION, defaultFixture, fixtures }
+}

+ 9 - 3
packages/experimental/webworker-runtime/src/image-layout.ts

@@ -9,9 +9,9 @@
 export const DEFAULT_ROOT = '/dsh'
 
 /**
- * Leaf name of the packed image: one gzip member holding the ustar archive. The
- * app build writes it beside the page and the page's boot fetches it from there,
- * so the extension is part of what a deployment serves.
+ * Leaf name of the packed base image: one gzip member holding the ustar archive.
+ * The app build writes it beside the page and the page's boot fetches it from
+ * there, so the extension is part of what a deployment serves.
  */
 export const IMAGE_FILE_NAME = 'vfs-image.tar.gz'
 
@@ -27,6 +27,12 @@ export const IMAGE_HOME_DIRECTORY = 'home'
 /** Working directories the host tree expects to exist, empty. */
 export const IMAGE_EMPTY_DIRECTORIES: readonly string[] = ['home/', 'workspace/', 'tmp/']
 
+/**
+ * Top-level directories an overlay archive may populate. Runtime code,
+ * configuration, and the lowering manifest remain owned by the base image.
+ */
+export const IMAGE_OVERLAY_DIRECTORIES: readonly string[] = ['home', 'workspace']
+
 /**
  * Identity of the lowered code shape, recorded in the image manifest by the
  * packer and required by the worker host: an image lowered by an older transform

+ 6 - 2
packages/experimental/webworker-runtime/src/index.ts

@@ -34,9 +34,13 @@ export {
 } from './worker-host.ts'
 export {
   DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_FILE_NAME, IMAGE_HOME_DIRECTORY,
-  IMAGE_MANIFEST_PATH, LOWERING_VERSION, WRAPPER_PARAMS,
+  IMAGE_MANIFEST_PATH, IMAGE_OVERLAY_DIRECTORIES, LOWERING_VERSION, WRAPPER_PARAMS,
 } from './image-layout.ts'
-export { loadVfsImage, MemoryVfs } from './storage/memory.ts'
+export {
+  parsePreviewFixtureManifest, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION,
+  type PreviewFixtureManifest, type PreviewFixtureManifestEntry,
+} from './fixture-manifest.ts'
+export { loadVfsImage, loadVfsOverlay, MemoryVfs } from './storage/memory.ts'
 export { inflateImage, inflateImageStream } from './storage/image-gzip.ts'
 export { packTar, parseTar, type TarEntry } from './storage/tar.ts'
 export { requireActiveVfs, setActiveVfs } from './storage/active.ts'

+ 1 - 0
packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs-watch.ts

@@ -413,6 +413,7 @@ export function watchAsync(
     throw(reason?: unknown): Promise<IteratorResult<WatchEvent>> {
       close()
       // AsyncIterator.throw forwards the caller's exact reason, including non-Error values.
+      // oxlint-disable-next-line typescript/prefer-promise-reject-errors
       return Promise.reject(reason)
     },
   }

+ 2 - 0
packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/stream.ts

@@ -23,6 +23,7 @@ type StreamStatics = typeof import('node:stream').Stream & {
 
 const nodeStream = Stream as unknown as StreamRuntime
 
+/* oxlint-disable typescript/unbound-method -- readable-stream's namespace statics do not read `this`. */
 const {
   Duplex, PassThrough, Readable, Stream: StreamBase, Transform, Writable,
   addAbortSignal, compose, destroy, finished, isDisturbed, isErrored, isReadable, pipeline, promises,
@@ -31,6 +32,7 @@ const streamStatics = StreamBase as unknown as StreamStatics
 const {
   getDefaultHighWaterMark, isDestroyed, isWritable, setDefaultHighWaterMark,
 } = streamStatics
+/* oxlint-enable typescript/unbound-method */
 
 // readable-stream tracks Node 18's 16 KiB byte default; this repository runs
 // Node 22+, whose generic and file streams use 64 KiB.

+ 11 - 7
packages/experimental/webworker-runtime/src/shell/fs-access.ts

@@ -76,13 +76,13 @@ function statsOf(stats: VfsStats): ShellStats {
  */
 export function hostFileSystem(): ShellFileSystem {
   const vfs = (): ReturnType<typeof requireActiveVfs> => requireActiveVfs()
-  const stat = async (path: string): Promise<ShellStats | undefined> => {
+  const stat = (path: string): Promise<ShellStats | undefined> => {
     try {
-      return statsOf(vfs().statSync(path) as VfsStats)
+      return Promise.resolve(statsOf(vfs().statSync(path) as VfsStats))
     } catch {
       // Absence is the answer callers branch on; every other failure mode of
       // the in-memory backend is also "this path holds nothing readable".
-      return undefined
+      return Promise.resolve(undefined)
     }
   }
   // Several members take no await: the face is asynchronous because a process
@@ -101,18 +101,22 @@ export function hostFileSystem(): ShellFileSystem {
       if ((await stat(path))?.directory === true) throw filesystemError('EISDIR', 'read', path)
       return vfs().readFileSync(path, 'utf8') as string
     },
-    writeText: async (path: string, text: string, append = false): Promise<void> => {
+    writeText: (path: string, text: string, append = false): Promise<void> => {
       if (append) vfs().appendFileSync(path, text)
       else vfs().writeFileSync(path, text)
+      return Promise.resolve()
     },
-    mkdir: async (path: string, recursive: boolean): Promise<void> => {
+    mkdir: (path: string, recursive: boolean): Promise<void> => {
       vfs().mkdirSync(path, { recursive })
+      return Promise.resolve()
     },
-    remove: async (path: string, options: { recursive: boolean; force: boolean }): Promise<void> => {
+    remove: (path: string, options: { recursive: boolean; force: boolean }): Promise<void> => {
       vfs().rmSync(path, options)
+      return Promise.resolve()
     },
-    rename: async (from: string, to: string): Promise<void> => {
+    rename: (from: string, to: string): Promise<void> => {
       vfs().renameSync(from, to)
+      return Promise.resolve()
     },
   }
 }

+ 36 - 0
packages/experimental/webworker-runtime/src/storage/memory.ts

@@ -5,6 +5,7 @@
  * @module @deepseek-ai/dsh-experimental-webworker-runtime/src/storage/memory
  */
 import { dirname, join, normalize, resolve, SEP } from '../module-system/posix-path.ts'
+import { IMAGE_OVERLAY_DIRECTORIES } from '../image-layout.ts'
 import { parseTar } from './tar.ts'
 import type {
   Vfs, VfsBigIntStats, VfsDir, VfsDirent, VfsEncoding, VfsError, VfsFileHandle, VfsMutation,
@@ -773,3 +774,38 @@ export function loadVfsImage(image: Uint8Array, root = '/dsh', vfs = new MemoryV
   }
   return vfs
 }
+
+/**
+ * Apply one ordered data overlay to an already mounted base image.
+ *
+ * Overlay entries may replace files only under the layout's data directories;
+ * module code, configuration, and the lowering manifest cannot be shadowed.
+ * Paths containing traversal segments are refused before normalization. Later
+ * overlays win for files, while file/directory type conflicts fail loud.
+ * @param image - Uncompressed ustar overlay archive.
+ * @param root - Virtual root shared with the base image.
+ * @param vfs - Mounted filesystem to update.
+ * @returns The same filesystem after applying the overlay.
+ */
+export function loadVfsOverlay(image: Uint8Array, root: string, vfs: MemoryVfs): MemoryVfs {
+  for (const entry of parseTar(image)) {
+    const relativeName = entry.name.startsWith('./') ? entry.name.slice(2) : entry.name
+    const path = relativeName.endsWith('/') ? relativeName.slice(0, -1) : relativeName
+    const segments = path.split('/')
+    if (path === '' || relativeName.startsWith(SEP)
+      || segments.some(segment => segment === '' || segment === '.' || segment === '..')
+      || !IMAGE_OVERLAY_DIRECTORIES.includes(segments[0] ?? '')) {
+      throw new Error(`webworker vfs: overlay entry must stay under ${IMAGE_OVERLAY_DIRECTORIES.join('/ or ')}, received "${entry.name}"`)
+    }
+    const target = join(root, path)
+    if (entry.directory) {
+      vfs.seedDirectory(target, { mode: entry.mode })
+      continue
+    }
+    if (vfs.existsSync(target) && vfs.statSync(target).isDirectory()) {
+      throw new Error(`webworker vfs: overlay file cannot replace directory "${target}"`)
+    }
+    vfs.seed(target, entry.bytes, { mode: entry.mode })
+  }
+  return vfs
+}

+ 7 - 3
packages/experimental/webworker-runtime/src/transport/frames.ts

@@ -33,12 +33,13 @@ export interface TunnelAbortFrame {
 
 /** Frames the worker accepts. */
 /**
- * First inbound frame: the image URL, the one input the worker assembly
- * takes from outside.
+ * First inbound frame: the base image URL and ordered data overlays selected
+ * before the worker assembly starts.
  */
 export interface TunnelInitFrame {
   readonly t: 'init'
   readonly image: string
+  readonly overlays: readonly string[]
 }
 
 /** Every frame the page sends the worker. */
@@ -142,7 +143,10 @@ export function parseInboundFrame(data: unknown): TunnelInboundFrame {
     if (typeof frame.image !== 'string') {
       throw new Error('webworker tunnel: init frame needs a string image url')
     }
-    return { t: 'init', image: frame.image }
+    if (!Array.isArray(frame.overlays) || frame.overlays.some(overlay => typeof overlay !== 'string')) {
+      throw new Error('webworker tunnel: init frame needs an array of string overlay urls')
+    }
+    return { t: 'init', image: frame.image, overlays: frame.overlays as string[] }
   }
   const id = frame.id
   if (typeof id !== 'string' && typeof id !== 'number') {

+ 9 - 3
packages/experimental/webworker-runtime/src/worker-host.ts

@@ -29,7 +29,7 @@ import { installProcessGlobal } from './node/globals/process.ts'
 import type { RequestListener } from './transport/synthetic-http.ts'
 import { TunnelServer, type TunnelPort } from './transport/tunnel.ts'
 import { inflateImage, inflateImageStream } from './storage/image-gzip.ts'
-import { loadVfsImage, MemoryVfs } from './storage/memory.ts'
+import { loadVfsImage, loadVfsOverlay, MemoryVfs } from './storage/memory.ts'
 import { setActiveVfs } from './storage/active.ts'
 import {
   DEFAULT_ROOT, IMAGE_CONFIG_PATH, IMAGE_EMPTY_DIRECTORIES, IMAGE_HOME_DIRECTORY, IMAGE_MANIFEST_PATH,
@@ -87,6 +87,8 @@ export interface WorkerHostOptions {
   readonly requestListener: () => Promise<RequestListener>
   /** Image bytes, or the URL the worker fetches them from. */
   readonly image: Uint8Array | string
+  /** Ordered data overlays applied after the base image and before boot. */
+  readonly overlays?: readonly (Uint8Array | string)[]
   /** Virtual root; defaults to {@link DEFAULT_ROOT}. */
   readonly root?: string
   /** Composed configuration inside the image; defaults to `<root>/config/cordis.yml`. */
@@ -182,8 +184,12 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost {
       const home = join(root, IMAGE_HOME_DIRECTORY)
       installProcessGlobal({ cwd: root, env: { DSH_HOME: home, HOME: home, ...options.env } })
 
-      const bytes = await readImage(options.image)
+      const [bytes, overlays] = await Promise.all([
+        readImage(options.image),
+        Promise.all((options.overlays ?? []).map(readImage)),
+      ])
       const mounted = loadVfsImage(bytes, root)
+      for (const overlay of overlays) loadVfsOverlay(overlay, root, mounted)
       // Belt and braces over the image's own empty-directory entries: a hand
       // -built image without them still boots.
       for (const directory of IMAGE_EMPTY_DIRECTORIES) {
@@ -248,7 +254,7 @@ export function createWorkerHost(options: WorkerHostOptions): WorkerHost {
       const shared = ctx.get('connection') !== undefined
       const handler = directFetchHandler(ctx, toFetchHandler(apiProxy))
       const usage = loader.usage()
-      console.info(`webworker host: tree active (modules=${String(usage.modules)}, preset root overlay=${presetOverlay ? 'applied' : 'already in roster'}, direct lane=${shared ? 'connection.createSharedFetchHandler (interceptors kept)' : 'api surface only'}, als causality=${options.alsCausality === undefined ? 'inert' : 'snapshot/restore'}, image lowering=${LOWERING_VERSION})`)
+      console.info(`webworker host: tree active (modules=${String(usage.modules)}, data overlays=${String(overlays.length)}, preset root overlay=${presetOverlay ? 'applied' : 'already in roster'}, direct lane=${shared ? 'connection.createSharedFetchHandler (interceptors kept)' : 'api surface only'}, als causality=${options.alsCausality === undefined ? 'inert' : 'snapshot/restore'}, image lowering=${LOWERING_VERSION})`)
 
       tunnel.serve({
         directFetch: (request: Request) => handler.fetch(request),

+ 7 - 3
packages/experimental/webworker-runtime/src/worker.ts

@@ -4,9 +4,9 @@
  * listener; the assembly owns everything else (process global, VFS image,
  * Cordis tree, tunnel server).
  *
- * The assembly needs the image location before it can exist, and it arrives in
- * the tunnel's opening `init` frame — this bundle reads nothing from its own
- * URL, so the deployment decides where both the bundle and the image live.
+ * The assembly needs the base image and selected overlays before it can exist;
+ * they arrive in the tunnel's opening `init` frame. This bundle reads nothing
+ * from its own URL, so the deployment decides where every archive lives.
  * Messages before `init` queue here; requests during boot queue inside the
  * host, which attaches its handler before its first await.
  */
@@ -52,12 +52,16 @@ self.addEventListener('message', (event: MessageEvent) => {
     if (typeof data.image !== 'string') {
       throw new Error('webworker: init frame needs a string image url')
     }
+    if (!Array.isArray(data.overlays) || data.overlays.some(overlay => typeof overlay !== 'string')) {
+      throw new Error('webworker: init frame needs an array of string overlay urls')
+    }
     const created = createWorkerHost({
       staticModules: createNodeBuiltins(),
       staticModulePrefixes: REPLACED_PREFIXES,
       requestListener: whenRequestListener,
       alsCausality,
       image: data.image,
+      overlays: data.overlays as string[],
     })
     host = created
     for (const queued of pending) {

+ 150 - 0
packages/experimental/webworker-runtime/tests/client/source-chooser.spec.ts

@@ -0,0 +1,150 @@
+// @vitest-environment jsdom
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import type { PreviewFixtureManifest } from '../../src/fixture-manifest.ts'
+import { choosePreviewSource } from '../../src/client/source-chooser.ts'
+
+const MANIFEST_URL = new URL('https://preview.test/preview/fixtures.json')
+
+const MANIFEST: PreviewFixtureManifest = {
+  version: 1,
+  defaultFixture: 'example',
+  fixtures: [{
+    id: 'example',
+    label: 'Example & <demo> "quoted" \'single\'',
+    description: 'A deterministic example.',
+    overlays: ['fixtures/base.tar.gz', 'fixtures/tail.tar.gz'],
+  }],
+}
+
+function setLocation(search = ''): void {
+  history.replaceState({}, '', `/preview.html${search}`)
+}
+
+function installManifest(manifest: PreviewFixtureManifest = MANIFEST): ReturnType<typeof vi.fn> {
+  const fetch = vi.fn(async () => Response.json(manifest))
+  vi.stubGlobal('fetch', fetch)
+  return fetch
+}
+
+function submitChooser(): void {
+  const form = document.querySelector<HTMLFormElement>('[data-preview-source-card]')
+  if (form === null) throw new Error('test chooser form was not rendered')
+  form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }))
+}
+
+describe('Preview source chooser', () => {
+  beforeEach(() => {
+    document.head.replaceChildren()
+    document.body.innerHTML = '<div id="root"></div>'
+    setLocation()
+  })
+
+  afterEach(() => {
+    vi.restoreAllMocks()
+    vi.unstubAllGlobals()
+  })
+
+  it('bypasses the chooser and manifest for an explicit empty source', async () => {
+    setLocation('?preview-fixture=none')
+    const fetch = installManifest()
+
+    await expect(choosePreviewSource(MANIFEST_URL)).resolves.toEqual([])
+    expect(fetch).not.toHaveBeenCalled()
+    expect(document.querySelector('[data-preview-source-chooser]')).toBeNull()
+  })
+
+  it('bypasses the chooser and resolves an explicit built-in fixture', async () => {
+    document.body.replaceChildren()
+    setLocation('?preview-fixture=example')
+    const fetch = installManifest()
+
+    await expect(choosePreviewSource(MANIFEST_URL)).resolves.toEqual([
+      new URL('https://preview.test/preview/fixtures/base.tar.gz'),
+      new URL('https://preview.test/preview/fixtures/tail.tar.gz'),
+    ])
+    expect(fetch).toHaveBeenCalledOnce()
+    expect(document.querySelector('[data-preview-source-chooser]')).toBeNull()
+  })
+
+  it.each(['', 'missing', 'webfs'])(
+    'fails loud for the explicit unavailable source %j without opening the chooser',
+    async (source) => {
+      setLocation(`?preview-fixture=${source}`)
+      installManifest()
+
+      await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/unknown or interactive source/)
+      expect(document.querySelector('[data-preview-source-chooser]')).toBeNull()
+    },
+  )
+
+  it('shows the chooser only when the query is absent and returns its default selection', async () => {
+    installManifest()
+
+    const selected = choosePreviewSource(MANIFEST_URL)
+    await vi.waitFor(() => {
+      expect(document.querySelector('[data-preview-source-chooser]')).not.toBeNull()
+    })
+    expect(document.querySelector<HTMLInputElement>('input[value="example"]')?.checked).toBe(true)
+    expect(document.querySelector<HTMLInputElement>('input[value="webfs"]')?.disabled).toBe(true)
+    expect(document.querySelector('[data-preview-source-card]')?.textContent)
+      .toContain(MANIFEST.fixtures[0]?.label)
+    expect(document.querySelector('[data-preview-source-card] script')).toBeNull()
+
+    submitChooser()
+
+    await expect(selected).resolves.toEqual([
+      new URL('https://preview.test/preview/fixtures/base.tar.gz'),
+      new URL('https://preview.test/preview/fixtures/tail.tar.gz'),
+    ])
+    expect(document.getElementById('root')?.childElementCount).toBe(0)
+    expect(document.querySelector('[data-preview-source-style]')).toBeNull()
+  })
+
+  it('selects the empty source when the manifest has no default', async () => {
+    installManifest({ ...MANIFEST, defaultFixture: null })
+
+    const selected = choosePreviewSource(MANIFEST_URL)
+    await vi.waitFor(() => {
+      expect(document.querySelector<HTMLInputElement>('input[value="none"]')?.checked).toBe(true)
+    })
+    submitChooser()
+
+    await expect(selected).resolves.toEqual([])
+  })
+
+  it('reports manifest, mount, form, selection, and catalog failures', async () => {
+    vi.stubGlobal('fetch', vi.fn(async () => new Response('missing', { status: 404 })))
+    await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/returned 404/)
+
+    installManifest()
+    document.body.replaceChildren()
+    await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/missing #root/)
+
+    document.body.innerHTML = '<div id="root"></div>'
+    const root = document.getElementById('root')
+    if (root === null) throw new Error('test root is missing')
+    vi.spyOn(root, 'querySelector').mockReturnValueOnce(null)
+    await expect(choosePreviewSource(MANIFEST_URL)).rejects.toThrow(/form was not rendered/)
+
+    document.head.replaceChildren()
+    document.body.innerHTML = '<div id="root"></div>'
+    const missingSelection = choosePreviewSource(MANIFEST_URL)
+    await vi.waitFor(() => { expect(document.querySelector('form')).not.toBeNull() })
+    document.querySelectorAll('input[name="preview-source"]').forEach((input) => {
+      input.removeAttribute('name')
+    })
+    submitChooser()
+    await expect(missingSelection).rejects.toThrow(/no source selected/)
+
+    document.head.replaceChildren()
+    document.body.innerHTML = '<div id="root"></div>'
+    const unavailableSelection = choosePreviewSource(MANIFEST_URL)
+    await vi.waitFor(() => { expect(document.querySelector('form')).not.toBeNull() })
+    const selected = document.querySelector<HTMLInputElement>('input:checked')
+    if (selected === null) throw new Error('test selection is missing')
+    selected.value = 'missing'
+    submitChooser()
+    await expect(unavailableSelection).rejects.toThrow(/unavailable source/)
+  })
+})

+ 48 - 0
packages/experimental/webworker-runtime/tests/fixture-manifest.spec.ts

@@ -0,0 +1,48 @@
+import { describe, expect, it } from 'vitest'
+import {
+  parsePreviewFixtureManifest, PREVIEW_FIXTURE_MANIFEST_VERSION,
+} from '../src/fixture-manifest.ts'
+
+describe('Preview fixture manifest', () => {
+  it('accepts a unique named fixture with ordered overlays', () => {
+    expect(parsePreviewFixtureManifest({
+      version: PREVIEW_FIXTURE_MANIFEST_VERSION,
+      defaultFixture: 'example',
+      fixtures: [{
+        id: 'example',
+        label: 'Example',
+        description: 'A deterministic example.',
+        overlays: ['fixtures/base.tar.gz', 'fixtures/tail.tar.gz'],
+      }],
+    })).toEqual({
+      version: PREVIEW_FIXTURE_MANIFEST_VERSION,
+      defaultFixture: 'example',
+      fixtures: [{
+        id: 'example',
+        label: 'Example',
+        description: 'A deterministic example.',
+        overlays: ['fixtures/base.tar.gz', 'fixtures/tail.tar.gz'],
+      }],
+    })
+  })
+
+  it.each([
+    [{ version: 2, defaultFixture: null, fixtures: [] }, /must use version/],
+    [{ version: 1, defaultFixture: 'missing', fixtures: [] }, /defaultFixture/],
+    [{
+      version: 1,
+      defaultFixture: 'duplicate',
+      fixtures: [
+        { id: 'duplicate', label: 'One', description: 'First.', overlays: ['one.tar.gz'] },
+        { id: 'duplicate', label: 'Two', description: 'Second.', overlays: ['two.tar.gz'] },
+      ],
+    }, /repeats id/],
+    [{
+      version: 1,
+      defaultFixture: 'none',
+      fixtures: [{ id: 'none', label: 'None', description: 'Reserved.', overlays: ['none.tar.gz'] }],
+    }, /invalid fixture entry/],
+  ])('rejects malformed catalogs', (value, error) => {
+    expect(() => parsePreviewFixtureManifest(value)).toThrow(error)
+  })
+})

+ 178 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl

@@ -0,0 +1,178 @@
+{"type":"session","version":0,"id":"preview-architecture-review","createdAt":1787472100000,"cwd":"/dsh/workspace","parentSession":"preview-showcase","seedLength":169,"origin":"subagent","delegationDepth":1,"agentPreset":"standard"}
+{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472000000}
+{"type":"user/message","data":{"id":"preview-user-01","role":"user","content":[{"type":"text","text":"History checkpoint 01: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472000001}
+{"type":"session/title","data":{"title":"WebWorker Preview Showcase","messageSeqs":[],"source":{"kind":"user"}},"seq":2,"time":1787472000002}
+{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472000003}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-assistant-01","role":"assistant","content":[{"type":"text","text":"Checkpoint 01 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":4,"time":1787472000004}
+{"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472000005}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":6,"time":1787472000006}
+{"type":"turn/start","data":{"turn":2},"seq":7,"time":1787472000007}
+{"type":"user/message","data":{"id":"preview-user-02","role":"user","content":[{"type":"text","text":"History checkpoint 02: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":8,"time":1787472000008}
+{"type":"step/start","data":{"turn":2,"step":1},"seq":9,"time":1787472000009}
+{"type":"assistant/message","data":{"turn":2,"step":1,"message":{"id":"preview-assistant-02","role":"assistant","content":[{"type":"text","text":"Checkpoint 02 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":10,"time":1787472000010}
+{"type":"step/end","data":{"turn":2,"step":1},"seq":11,"time":1787472000011}
+{"type":"turn/end","data":{"turn":2,"reason":{"kind":"completed"}},"seq":12,"time":1787472000012}
+{"type":"turn/start","data":{"turn":3},"seq":13,"time":1787472000013}
+{"type":"user/message","data":{"id":"preview-user-03","role":"user","content":[{"type":"text","text":"History checkpoint 03: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":14,"time":1787472000014}
+{"type":"step/start","data":{"turn":3,"step":1},"seq":15,"time":1787472000015}
+{"type":"assistant/message","data":{"turn":3,"step":1,"message":{"id":"preview-assistant-03","role":"assistant","content":[{"type":"text","text":"Checkpoint 03 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":16,"time":1787472000016}
+{"type":"step/end","data":{"turn":3,"step":1},"seq":17,"time":1787472000017}
+{"type":"turn/end","data":{"turn":3,"reason":{"kind":"completed"}},"seq":18,"time":1787472000018}
+{"type":"turn/start","data":{"turn":4},"seq":19,"time":1787472000019}
+{"type":"user/message","data":{"id":"preview-user-04","role":"user","content":[{"type":"text","text":"History checkpoint 04: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":20,"time":1787472000020}
+{"type":"step/start","data":{"turn":4,"step":1},"seq":21,"time":1787472000021}
+{"type":"assistant/message","data":{"turn":4,"step":1,"message":{"id":"preview-assistant-04","role":"assistant","content":[{"type":"text","text":"Checkpoint 04 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":22,"time":1787472000022}
+{"type":"step/end","data":{"turn":4,"step":1},"seq":23,"time":1787472000023}
+{"type":"turn/end","data":{"turn":4,"reason":{"kind":"completed"}},"seq":24,"time":1787472000024}
+{"type":"turn/start","data":{"turn":5},"seq":25,"time":1787472000025}
+{"type":"user/message","data":{"id":"preview-user-05","role":"user","content":[{"type":"text","text":"History checkpoint 05: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":26,"time":1787472000026}
+{"type":"step/start","data":{"turn":5,"step":1},"seq":27,"time":1787472000027}
+{"type":"assistant/message","data":{"turn":5,"step":1,"message":{"id":"preview-assistant-05","role":"assistant","content":[{"type":"text","text":"Checkpoint 05 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":28,"time":1787472000028}
+{"type":"step/end","data":{"turn":5,"step":1},"seq":29,"time":1787472000029}
+{"type":"turn/end","data":{"turn":5,"reason":{"kind":"completed"}},"seq":30,"time":1787472000030}
+{"type":"turn/start","data":{"turn":6},"seq":31,"time":1787472000031}
+{"type":"user/message","data":{"id":"preview-user-06","role":"user","content":[{"type":"text","text":"History checkpoint 06: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":32,"time":1787472000032}
+{"type":"step/start","data":{"turn":6,"step":1},"seq":33,"time":1787472000033}
+{"type":"assistant/message","data":{"turn":6,"step":1,"message":{"id":"preview-assistant-06","role":"assistant","content":[{"type":"text","text":"Checkpoint 06 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":34,"time":1787472000034}
+{"type":"step/end","data":{"turn":6,"step":1},"seq":35,"time":1787472000035}
+{"type":"turn/end","data":{"turn":6,"reason":{"kind":"completed"}},"seq":36,"time":1787472000036}
+{"type":"turn/start","data":{"turn":7},"seq":37,"time":1787472000037}
+{"type":"user/message","data":{"id":"preview-user-07","role":"user","content":[{"type":"text","text":"History checkpoint 07: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":38,"time":1787472000038}
+{"type":"step/start","data":{"turn":7,"step":1},"seq":39,"time":1787472000039}
+{"type":"assistant/message","data":{"turn":7,"step":1,"message":{"id":"preview-assistant-07","role":"assistant","content":[{"type":"text","text":"Checkpoint 07 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":40,"time":1787472000040}
+{"type":"step/end","data":{"turn":7,"step":1},"seq":41,"time":1787472000041}
+{"type":"turn/end","data":{"turn":7,"reason":{"kind":"completed"}},"seq":42,"time":1787472000042}
+{"type":"turn/start","data":{"turn":8},"seq":43,"time":1787472000043}
+{"type":"user/message","data":{"id":"preview-user-08","role":"user","content":[{"type":"text","text":"History checkpoint 08: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":44,"time":1787472000044}
+{"type":"step/start","data":{"turn":8,"step":1},"seq":45,"time":1787472000045}
+{"type":"assistant/message","data":{"turn":8,"step":1,"message":{"id":"preview-assistant-08","role":"assistant","content":[{"type":"text","text":"Checkpoint 08 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":46,"time":1787472000046}
+{"type":"step/end","data":{"turn":8,"step":1},"seq":47,"time":1787472000047}
+{"type":"turn/end","data":{"turn":8,"reason":{"kind":"completed"}},"seq":48,"time":1787472000048}
+{"type":"turn/start","data":{"turn":9},"seq":49,"time":1787472000049}
+{"type":"user/message","data":{"id":"preview-user-09","role":"user","content":[{"type":"text","text":"History checkpoint 09: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":50,"time":1787472000050}
+{"type":"step/start","data":{"turn":9,"step":1},"seq":51,"time":1787472000051}
+{"type":"assistant/message","data":{"turn":9,"step":1,"message":{"id":"preview-assistant-09","role":"assistant","content":[{"type":"text","text":"Checkpoint 09 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":52,"time":1787472000052}
+{"type":"step/end","data":{"turn":9,"step":1},"seq":53,"time":1787472000053}
+{"type":"turn/end","data":{"turn":9,"reason":{"kind":"completed"}},"seq":54,"time":1787472000054}
+{"type":"turn/start","data":{"turn":10},"seq":55,"time":1787472000055}
+{"type":"user/message","data":{"id":"preview-user-10","role":"user","content":[{"type":"text","text":"History checkpoint 10: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":56,"time":1787472000056}
+{"type":"step/start","data":{"turn":10,"step":1},"seq":57,"time":1787472000057}
+{"type":"assistant/message","data":{"turn":10,"step":1,"message":{"id":"preview-assistant-10","role":"assistant","content":[{"type":"text","text":"Checkpoint 10 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":58,"time":1787472000058}
+{"type":"step/end","data":{"turn":10,"step":1},"seq":59,"time":1787472000059}
+{"type":"turn/end","data":{"turn":10,"reason":{"kind":"completed"}},"seq":60,"time":1787472000060}
+{"type":"turn/start","data":{"turn":11},"seq":61,"time":1787472000061}
+{"type":"user/message","data":{"id":"preview-user-11","role":"user","content":[{"type":"text","text":"History checkpoint 11: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":62,"time":1787472000062}
+{"type":"step/start","data":{"turn":11,"step":1},"seq":63,"time":1787472000063}
+{"type":"assistant/message","data":{"turn":11,"step":1,"message":{"id":"preview-assistant-11","role":"assistant","content":[{"type":"text","text":"Checkpoint 11 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":64,"time":1787472000064}
+{"type":"step/end","data":{"turn":11,"step":1},"seq":65,"time":1787472000065}
+{"type":"turn/end","data":{"turn":11,"reason":{"kind":"completed"}},"seq":66,"time":1787472000066}
+{"type":"turn/start","data":{"turn":12},"seq":67,"time":1787472000067}
+{"type":"user/message","data":{"id":"preview-user-12","role":"user","content":[{"type":"text","text":"History checkpoint 12: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":68,"time":1787472000068}
+{"type":"step/start","data":{"turn":12,"step":1},"seq":69,"time":1787472000069}
+{"type":"assistant/message","data":{"turn":12,"step":1,"message":{"id":"preview-assistant-12","role":"assistant","content":[{"type":"text","text":"Checkpoint 12 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":70,"time":1787472000070}
+{"type":"step/end","data":{"turn":12,"step":1},"seq":71,"time":1787472000071}
+{"type":"turn/end","data":{"turn":12,"reason":{"kind":"completed"}},"seq":72,"time":1787472000072}
+{"type":"turn/start","data":{"turn":13},"seq":73,"time":1787472000073}
+{"type":"user/message","data":{"id":"preview-user-13","role":"user","content":[{"type":"text","text":"History checkpoint 13: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":74,"time":1787472000074}
+{"type":"step/start","data":{"turn":13,"step":1},"seq":75,"time":1787472000075}
+{"type":"assistant/message","data":{"turn":13,"step":1,"message":{"id":"preview-assistant-13","role":"assistant","content":[{"type":"text","text":"Checkpoint 13 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":76,"time":1787472000076}
+{"type":"step/end","data":{"turn":13,"step":1},"seq":77,"time":1787472000077}
+{"type":"turn/end","data":{"turn":13,"reason":{"kind":"completed"}},"seq":78,"time":1787472000078}
+{"type":"turn/start","data":{"turn":14},"seq":79,"time":1787472000079}
+{"type":"user/message","data":{"id":"preview-user-14","role":"user","content":[{"type":"text","text":"History checkpoint 14: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":80,"time":1787472000080}
+{"type":"step/start","data":{"turn":14,"step":1},"seq":81,"time":1787472000081}
+{"type":"assistant/message","data":{"turn":14,"step":1,"message":{"id":"preview-assistant-14","role":"assistant","content":[{"type":"text","text":"Checkpoint 14 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":82,"time":1787472000082}
+{"type":"step/end","data":{"turn":14,"step":1},"seq":83,"time":1787472000083}
+{"type":"turn/end","data":{"turn":14,"reason":{"kind":"completed"}},"seq":84,"time":1787472000084}
+{"type":"turn/start","data":{"turn":15},"seq":85,"time":1787472000085}
+{"type":"user/message","data":{"id":"preview-user-15","role":"user","content":[{"type":"text","text":"History checkpoint 15: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":86,"time":1787472000086}
+{"type":"step/start","data":{"turn":15,"step":1},"seq":87,"time":1787472000087}
+{"type":"assistant/message","data":{"turn":15,"step":1,"message":{"id":"preview-assistant-15","role":"assistant","content":[{"type":"text","text":"Checkpoint 15 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":88,"time":1787472000088}
+{"type":"step/end","data":{"turn":15,"step":1},"seq":89,"time":1787472000089}
+{"type":"turn/end","data":{"turn":15,"reason":{"kind":"completed"}},"seq":90,"time":1787472000090}
+{"type":"turn/start","data":{"turn":16},"seq":91,"time":1787472000091}
+{"type":"user/message","data":{"id":"preview-user-16","role":"user","content":[{"type":"text","text":"History checkpoint 16: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":92,"time":1787472000092}
+{"type":"step/start","data":{"turn":16,"step":1},"seq":93,"time":1787472000093}
+{"type":"assistant/message","data":{"turn":16,"step":1,"message":{"id":"preview-assistant-16","role":"assistant","content":[{"type":"text","text":"Checkpoint 16 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":94,"time":1787472000094}
+{"type":"step/end","data":{"turn":16,"step":1},"seq":95,"time":1787472000095}
+{"type":"turn/end","data":{"turn":16,"reason":{"kind":"completed"}},"seq":96,"time":1787472000096}
+{"type":"turn/start","data":{"turn":17},"seq":97,"time":1787472000097}
+{"type":"user/message","data":{"id":"preview-user-17","role":"user","content":[{"type":"text","text":"History checkpoint 17: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":98,"time":1787472000098}
+{"type":"step/start","data":{"turn":17,"step":1},"seq":99,"time":1787472000099}
+{"type":"assistant/message","data":{"turn":17,"step":1,"message":{"id":"preview-assistant-17","role":"assistant","content":[{"type":"text","text":"Checkpoint 17 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":100,"time":1787472000100}
+{"type":"step/end","data":{"turn":17,"step":1},"seq":101,"time":1787472000101}
+{"type":"turn/end","data":{"turn":17,"reason":{"kind":"completed"}},"seq":102,"time":1787472000102}
+{"type":"turn/start","data":{"turn":18},"seq":103,"time":1787472000103}
+{"type":"user/message","data":{"id":"preview-user-18","role":"user","content":[{"type":"text","text":"History checkpoint 18: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":104,"time":1787472000104}
+{"type":"step/start","data":{"turn":18,"step":1},"seq":105,"time":1787472000105}
+{"type":"assistant/message","data":{"turn":18,"step":1,"message":{"id":"preview-assistant-18","role":"assistant","content":[{"type":"text","text":"Checkpoint 18 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":106,"time":1787472000106}
+{"type":"step/end","data":{"turn":18,"step":1},"seq":107,"time":1787472000107}
+{"type":"turn/end","data":{"turn":18,"reason":{"kind":"completed"}},"seq":108,"time":1787472000108}
+{"type":"turn/start","data":{"turn":19},"seq":109,"time":1787472000109}
+{"type":"user/message","data":{"id":"preview-user-19","role":"user","content":[{"type":"text","text":"History checkpoint 19: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":110,"time":1787472000110}
+{"type":"step/start","data":{"turn":19,"step":1},"seq":111,"time":1787472000111}
+{"type":"assistant/message","data":{"turn":19,"step":1,"message":{"id":"preview-assistant-19","role":"assistant","content":[{"type":"text","text":"Checkpoint 19 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":112,"time":1787472000112}
+{"type":"step/end","data":{"turn":19,"step":1},"seq":113,"time":1787472000113}
+{"type":"turn/end","data":{"turn":19,"reason":{"kind":"completed"}},"seq":114,"time":1787472000114}
+{"type":"turn/start","data":{"turn":20},"seq":115,"time":1787472000115}
+{"type":"user/message","data":{"id":"preview-user-20","role":"user","content":[{"type":"text","text":"History checkpoint 20: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":116,"time":1787472000116}
+{"type":"step/start","data":{"turn":20,"step":1},"seq":117,"time":1787472000117}
+{"type":"assistant/message","data":{"turn":20,"step":1,"message":{"id":"preview-assistant-20","role":"assistant","content":[{"type":"text","text":"Checkpoint 20 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":118,"time":1787472000118}
+{"type":"step/end","data":{"turn":20,"step":1},"seq":119,"time":1787472000119}
+{"type":"turn/end","data":{"turn":20,"reason":{"kind":"completed"}},"seq":120,"time":1787472000120}
+{"type":"turn/start","data":{"turn":21},"seq":121,"time":1787472000121}
+{"type":"user/message","data":{"id":"preview-user-21","role":"user","content":[{"type":"text","text":"History checkpoint 21: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":122,"time":1787472000122}
+{"type":"step/start","data":{"turn":21,"step":1},"seq":123,"time":1787472000123}
+{"type":"assistant/message","data":{"turn":21,"step":1,"message":{"id":"preview-assistant-21","role":"assistant","content":[{"type":"text","text":"Checkpoint 21 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":124,"time":1787472000124}
+{"type":"step/end","data":{"turn":21,"step":1},"seq":125,"time":1787472000125}
+{"type":"turn/end","data":{"turn":21,"reason":{"kind":"completed"}},"seq":126,"time":1787472000126}
+{"type":"turn/start","data":{"turn":22},"seq":127,"time":1787472000127}
+{"type":"user/message","data":{"id":"preview-user-22","role":"user","content":[{"type":"text","text":"History checkpoint 22: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":128,"time":1787472000128}
+{"type":"step/start","data":{"turn":22,"step":1},"seq":129,"time":1787472000129}
+{"type":"assistant/message","data":{"turn":22,"step":1,"message":{"id":"preview-assistant-22","role":"assistant","content":[{"type":"text","text":"Checkpoint 22 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":130,"time":1787472000130}
+{"type":"step/end","data":{"turn":22,"step":1},"seq":131,"time":1787472000131}
+{"type":"turn/end","data":{"turn":22,"reason":{"kind":"completed"}},"seq":132,"time":1787472000132}
+{"type":"turn/start","data":{"turn":23},"seq":133,"time":1787472000133}
+{"type":"user/message","data":{"id":"preview-user-23","role":"user","content":[{"type":"text","text":"History checkpoint 23: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":134,"time":1787472000134}
+{"type":"step/start","data":{"turn":23,"step":1},"seq":135,"time":1787472000135}
+{"type":"assistant/message","data":{"turn":23,"step":1,"message":{"id":"preview-assistant-23","role":"assistant","content":[{"type":"text","text":"Checkpoint 23 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":136,"time":1787472000136}
+{"type":"step/end","data":{"turn":23,"step":1},"seq":137,"time":1787472000137}
+{"type":"turn/end","data":{"turn":23,"reason":{"kind":"completed"}},"seq":138,"time":1787472000138}
+{"type":"turn/start","data":{"turn":24},"seq":139,"time":1787472000139}
+{"type":"user/message","data":{"id":"preview-user-24","role":"user","content":[{"type":"text","text":"History checkpoint 24: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":140,"time":1787472000140}
+{"type":"step/start","data":{"turn":24,"step":1},"seq":141,"time":1787472000141}
+{"type":"assistant/message","data":{"turn":24,"step":1,"message":{"id":"preview-assistant-24","role":"assistant","content":[{"type":"text","text":"Checkpoint 24 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":142,"time":1787472000142}
+{"type":"step/end","data":{"turn":24,"step":1},"seq":143,"time":1787472000143}
+{"type":"turn/end","data":{"turn":24,"reason":{"kind":"completed"}},"seq":144,"time":1787472000144}
+{"type":"turn/start","data":{"turn":25},"seq":145,"time":1787472000145}
+{"type":"user/message","data":{"id":"preview-user-25","role":"user","content":[{"type":"text","text":"History checkpoint 25: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":146,"time":1787472000146}
+{"type":"step/start","data":{"turn":25,"step":1},"seq":147,"time":1787472000147}
+{"type":"assistant/message","data":{"turn":25,"step":1,"message":{"id":"preview-assistant-25","role":"assistant","content":[{"type":"text","text":"Checkpoint 25 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":148,"time":1787472000148}
+{"type":"step/end","data":{"turn":25,"step":1},"seq":149,"time":1787472000149}
+{"type":"turn/end","data":{"turn":25,"reason":{"kind":"completed"}},"seq":150,"time":1787472000150}
+{"type":"turn/start","data":{"turn":26},"seq":151,"time":1787472000151}
+{"type":"user/message","data":{"id":"preview-user-26","role":"user","content":[{"type":"text","text":"History checkpoint 26: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":152,"time":1787472000152}
+{"type":"step/start","data":{"turn":26,"step":1},"seq":153,"time":1787472000153}
+{"type":"assistant/message","data":{"turn":26,"step":1,"message":{"id":"preview-assistant-26","role":"assistant","content":[{"type":"text","text":"Checkpoint 26 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":154,"time":1787472000154}
+{"type":"step/end","data":{"turn":26,"step":1},"seq":155,"time":1787472000155}
+{"type":"turn/end","data":{"turn":26,"reason":{"kind":"completed"}},"seq":156,"time":1787472000156}
+{"type":"turn/start","data":{"turn":27},"seq":157,"time":1787472000157}
+{"type":"user/message","data":{"id":"preview-user-27","role":"user","content":[{"type":"text","text":"History checkpoint 27: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":158,"time":1787472000158}
+{"type":"step/start","data":{"turn":27,"step":1},"seq":159,"time":1787472000159}
+{"type":"assistant/message","data":{"turn":27,"step":1,"message":{"id":"preview-assistant-27","role":"assistant","content":[{"type":"text","text":"Checkpoint 27 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":160,"time":1787472000160}
+{"type":"step/end","data":{"turn":27,"step":1},"seq":161,"time":1787472000161}
+{"type":"turn/end","data":{"turn":27,"reason":{"kind":"completed"}},"seq":162,"time":1787472000162}
+{"type":"turn/start","data":{"turn":28},"seq":163,"time":1787472000163}
+{"type":"user/message","data":{"id":"preview-user-28","role":"user","content":[{"type":"text","text":"History checkpoint 28: verify deterministic preview state."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":164,"time":1787472000164}
+{"type":"step/start","data":{"turn":28,"step":1},"seq":165,"time":1787472000165}
+{"type":"assistant/message","data":{"turn":28,"step":1,"message":{"id":"preview-assistant-28","role":"assistant","content":[{"type":"text","text":"Checkpoint 28 is recorded."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":166,"time":1787472000166}
+{"type":"step/end","data":{"turn":28,"step":1},"seq":167,"time":1787472000167}
+{"type":"turn/end","data":{"turn":28,"reason":{"kind":"completed"}},"seq":168,"time":1787472000168}
+{"type":"session/end-seed","data":{},"seq":169,"time":1787472100000}
+{"type":"turn/start","data":{"turn":29},"seq":170,"time":1787472100001}
+{"type":"user/message","data":{"id":"preview-review-user","role":"user","content":[{"type":"text","text":"Review whether the preview fixture is isolated from future WebFS data."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":171,"time":1787472100002}
+{"type":"subagent/descriptor","data":{"version":2,"mode":"one-shot","provider":"fork","label":"Review preview architecture"},"seq":172,"time":1787472100003}
+{"type":"step/start","data":{"turn":29,"step":1},"seq":173,"time":1787472100004}
+{"type":"assistant/message","data":{"turn":29,"step":1,"message":{"id":"preview-review-assistant","role":"assistant","content":[{"type":"text","text":"The bundled fixture is static image content; future WebFS state remains user-owned."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":174,"time":1787472100005}
+{"type":"step/end","data":{"turn":29,"step":1},"seq":175,"time":1787472100006}
+{"type":"turn/end","data":{"turn":29,"reason":{"kind":"completed"}},"seq":176,"time":1787472100007}

+ 8 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl

@@ -0,0 +1,8 @@
+{"type":"session","version":0,"id":"preview-follow-up-builder","createdAt":1787472200000,"cwd":"/dsh/workspace","parentSession":"preview-showcase","origin":"subagent","delegationDepth":1,"agentPreset":"standard"}
+{"type":"turn/start","data":{"turn":1},"seq":0,"time":1787472200000}
+{"type":"user/message","data":{"id":"preview-builder-user","role":"user","content":[{"type":"text","text":"Check that the Preview workspace can support follow-up tasks."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":1,"time":1787472200001}
+{"type":"subagent/descriptor","data":{"version":2,"mode":"continuable","provider":"spawn","label":"Continue preview verification"},"seq":2,"time":1787472200002}
+{"type":"step/start","data":{"turn":1,"step":1},"seq":3,"time":1787472200003}
+{"type":"assistant/message","data":{"turn":1,"step":1,"message":{"id":"preview-builder-assistant","role":"assistant","content":[{"type":"text","text":"This child is continuable and ready for another verification turn."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":4,"time":1787472200004}
+{"type":"step/end","data":{"turn":1,"step":1},"seq":5,"time":1787472200005}
+{"type":"turn/end","data":{"turn":1,"reason":{"kind":"completed"}},"seq":6,"time":1787472200006}

Fichier diff supprimé car celui-ci est trop grand
+ 173 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.jsonl


+ 24 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json

@@ -0,0 +1,24 @@
+{
+  "unit": {
+    "name": "session_projcache",
+    "version": 3
+  },
+  "global": null,
+  "tables": {
+    "sessions": {
+      "preview-showcase": {
+        "identity": {
+          "createdAt": 1787472000000,
+          "cwd": "/dsh/workspace"
+        },
+        "rows": {
+          "title": {
+            "ver": 1,
+            "seq": 198,
+            "val": "WebWorker Preview Showcase"
+          }
+        }
+      }
+    }
+  }
+}

+ 8 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/.agents/skills/preview-tour/SKILL.md

@@ -0,0 +1,8 @@
+---
+name: preview-tour
+description: Inspect the bundled Preview workspace and its deterministic Session examples.
+---
+
+# Preview tour
+
+Read the workspace files, inspect the tool gallery, open both subagent histories, and load the earlier conversation page.

+ 9 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/PREVIEW.md

@@ -0,0 +1,9 @@
+# Preview Workspace
+
+This deterministic workspace is bundled with the browser-only preview.
+
+- `src/preview.ts` is the file changed by the example write result.
+- `data/tasks.json` mirrors the completed preview checklist.
+- `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing.
+
+Refresh the preview to restore these image bytes.

+ 17 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/data/tasks.json

@@ -0,0 +1,17 @@
+{
+  "title": "Preview verification",
+  "tasks": [
+    {
+      "name": "Inspect tool cards",
+      "status": "completed"
+    },
+    {
+      "name": "Open both subagents",
+      "status": "completed"
+    },
+    {
+      "name": "Load earlier history",
+      "status": "completed"
+    }
+  ]
+}

+ 3 - 0
packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/src/preview.ts

@@ -0,0 +1,3 @@
+export const previewStatus = 'ready'
+
+export const previewFeatures = ['tools', 'subagents', 'pagination'] as const

+ 3 - 4
packages/experimental/webworker-runtime/tests/node/child-process.spec.ts

@@ -108,10 +108,9 @@ it('reports that a synchronous run cannot happen, without throwing at the probe'
   })
   expect(spawnSync(launcherPath(), ['--ro', '/', '--', 'echo', 'x']).error?.message)
     .toContain('commands run asynchronously')
-  expect(spawnSync(launcherPath(), ['--probe', '--'])).toMatchObject({
-    status: LAUNCHER_FAILURE_EXIT,
-    stderr: expect.any(Buffer),
-  })
+  const failedProbe = spawnSync(launcherPath(), ['--probe', '--'])
+  expect(failedProbe.status).toBe(LAUNCHER_FAILURE_EXIT)
+  expect(Buffer.isBuffer(failedProbe.stderr)).toBe(true)
 })
 
 it('keeps the native Landlock package API and CLI failure contract', async () => {

+ 18 - 1
packages/experimental/webworker-runtime/tests/storage/tar.spec.ts

@@ -4,7 +4,7 @@
  */
 import { describe, expect, it } from 'vitest'
 import { packTar, parseTar } from '../../src/storage/tar.ts'
-import { loadVfsImage } from '../../src/storage/memory.ts'
+import { loadVfsImage, loadVfsOverlay } from '../../src/storage/memory.ts'
 
 const encoder = new TextEncoder()
 
@@ -38,4 +38,21 @@ describe('tar codec', () => {
     expect(vfs.existsSync('/dsh/workspace')).toBe(true)
     expect(vfs.existsSync('/dsh/absent')).toBe(false)
   })
+
+  it('applies ordered data overlays without exposing runtime paths', () => {
+    const vfs = loadVfsImage(packTar({
+      'config/cordis.yml': encoder.encode('- id: subject\n'),
+      'workspace/status.txt': encoder.encode('base'),
+    }), '/dsh')
+    loadVfsOverlay(packTar({
+      'workspace/status.txt': encoder.encode('fixture'),
+      'home/sessions/example/session.jsonl': encoder.encode('{}\n'),
+    }), '/dsh', vfs)
+    expect(vfs.readFileSync('/dsh/workspace/status.txt', 'utf8')).toBe('fixture')
+    expect(vfs.readFileSync('/dsh/home/sessions/example/session.jsonl', 'utf8')).toBe('{}\n')
+    expect(() => loadVfsOverlay(packTar({
+      'config/cordis.yml': encoder.encode('replaced'),
+    }), '/dsh', vfs)).toThrow(/overlay entry must stay under home\/ or workspace/)
+    expect(vfs.readFileSync('/dsh/config/cordis.yml', 'utf8')).toBe('- id: subject\n')
+  })
 })

+ 22 - 0
packages/experimental/webworker-runtime/tests/transport/frames.spec.ts

@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+import { parseInboundFrame } from '../../src/transport/frames.ts'
+
+describe('tunnel init frame', () => {
+  it('retains the selected overlay order', () => {
+    expect(parseInboundFrame({
+      t: 'init',
+      image: 'base.tar.gz',
+      overlays: ['workspace.tar.gz', 'session.tar.gz'],
+    })).toEqual({
+      t: 'init',
+      image: 'base.tar.gz',
+      overlays: ['workspace.tar.gz', 'session.tar.gz'],
+    })
+  })
+
+  it('rejects a missing or non-string overlay list', () => {
+    expect(() => parseInboundFrame({ t: 'init', image: 'base.tar.gz' })).toThrow(/array of string overlay urls/)
+    expect(() => parseInboundFrame({ t: 'init', image: 'base.tar.gz', overlays: [1] }))
+      .toThrow(/array of string overlay urls/)
+  })
+})

+ 21 - 0
packages/experimental/webworker-runtime/tests/transport/tunnel-client.spec.ts

@@ -54,6 +54,27 @@ function stubWorker(): {
   }
 }
 
+// The opening frame preserves overlay order for deterministic pre-boot mounts.
+{
+  const { worker, sent } = stubWorker()
+  const tunnel = new WorkerTunnel(worker)
+  tunnel.init('https://preview.test/base.tar.gz', [
+    'https://preview.test/first.tar.gz',
+    'https://preview.test/second.tar.gz',
+  ])
+  check('the init frame carries ordered overlays', sent[0], {
+    t: 'init',
+    image: 'https://preview.test/base.tar.gz',
+    overlays: ['https://preview.test/first.tar.gz', 'https://preview.test/second.tar.gz'],
+  })
+
+  const direct = stubWorker()
+  new WorkerTunnel(direct.worker).init('https://preview.test/base.tar.gz')
+  check('the direct init path defaults to no overlays', direct.sent[0], {
+    t: 'init', image: 'https://preview.test/base.tar.gz', overlays: [],
+  })
+}
+
 // A normal reply resolves and says nothing on the console.
 {
   const { worker, sent, deliver } = stubWorker()

+ 118 - 0
packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts

@@ -0,0 +1,118 @@
+import { readFileSync, readdirSync } from 'node:fs'
+import { join, relative } from 'node:path'
+import { describe, expect, it } from 'vitest'
+import { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
+import { scanLog } from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
+import { foldSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
+import {
+  buildVfsExampleFiles,
+  VFS_EXAMPLE_OLDEST_MESSAGE,
+  VFS_EXAMPLE_ROOT,
+  VFS_EXAMPLE_SESSION_IDS,
+  VFS_EXAMPLE_TAIL_MESSAGE,
+  VFS_EXAMPLE_TITLE,
+} from './vfs-example-fixture.ts'
+
+function filesUnder(root: string): string[] {
+  const files: string[] = []
+  const visit = (directory: string): void => {
+    for (const entry of readdirSync(directory, { withFileTypes: true })) {
+      const path = join(directory, entry.name)
+      if (entry.isDirectory()) visit(path)
+      else if (entry.isFile()) files.push(relative(root, path).replaceAll('\\', '/'))
+    }
+  }
+  visit(root)
+  return files.sort()
+}
+
+function readSession(id: string): ReturnType<typeof scanLog> {
+  return scanLog(readFileSync(
+    join(VFS_EXAMPLE_ROOT, 'home/sessions/--dsh-workspace--', id, 'session.jsonl'),
+  ))
+}
+
+function textOf(event: SessionEvent): string {
+  if (event.type === 'user/message') {
+    return event.data.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
+  }
+  if (event.type === 'assistant/message') {
+    return event.data.message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
+  }
+  return ''
+}
+
+describe('WebWorker preview VFS example', () => {
+  it('matches its deterministic source byte for byte', () => {
+    const expected = buildVfsExampleFiles()
+    expect(filesUnder(VFS_EXAMPLE_ROOT)).toEqual([...expected.keys()].sort())
+    for (const [path, content] of expected) {
+      expect(readFileSync(join(VFS_EXAMPLE_ROOT, path), 'utf8'), path).toBe(content)
+    }
+  })
+
+  it('seeds the cold-list title cache against the main log identity', () => {
+    const cache = JSON.parse(readFileSync(
+      join(VFS_EXAMPLE_ROOT, 'home/storages/session_projcache.json'),
+      'utf8',
+    )) as {
+      unit: { name: string; version: number }
+      tables: { sessions: Record<string, { identity: { createdAt: number; cwd: string }; rows: { title: unknown } }> }
+    }
+    expect(cache.unit).toEqual({ name: 'session_projcache', version: 3 })
+    expect(cache.tables.sessions[VFS_EXAMPLE_SESSION_IDS.main]).toMatchObject({
+      identity: { createdAt: 1_787_472_000_000, cwd: '/dsh/workspace' },
+      rows: { title: { ver: 1, val: VFS_EXAMPLE_TITLE } },
+    })
+  })
+
+  it('restores the main production log with paging and tool coverage', () => {
+    const { meta, events } = readSession(VFS_EXAMPLE_SESSION_IDS.main)
+    expect(meta).toMatchObject({
+      id: VFS_EXAMPLE_SESSION_IDS.main,
+      cwd: '/dsh/workspace',
+      delegationDepth: 0,
+      agentPreset: 'standard',
+    })
+    expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
+    expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
+    expect(() => Session.fromRestore(SessionId(meta.id), events, meta)).not.toThrow()
+
+    const messages = events.filter(event =>
+      (event.type === 'user/message' || event.type === 'assistant/message') && event.surfaceOp === 'append')
+    expect(messages.length).toBeGreaterThan(50)
+    expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_OLDEST_MESSAGE))).toBe(true)
+    expect(messages.some(event => textOf(event).includes(VFS_EXAMPLE_TAIL_MESSAGE))).toBe(true)
+    expect(events.some(event => event.type === 'session/title'
+      && (event.data as { title?: unknown }).title === VFS_EXAMPLE_TITLE)).toBe(true)
+
+    const tools = events.flatMap(event => event.type === 'tool/call' ? [event.data.name] : [])
+    expect(new Set(tools)).toEqual(new Set([
+      'read', 'write', 'bash', 'glob', 'grep', 'web_search', 'todo_write', 'subagent', 'subagent_fork',
+    ]))
+    expect(events.some(event => event.type === 'todo/write')).toBe(true)
+    expect(events.some(event => event.type === 'tool/result' && event.data.message.content[0].isError === true)).toBe(true)
+  })
+
+  it('restores one-shot and continuable child Sessions with durable descriptors', () => {
+    const expected = [
+      [VFS_EXAMPLE_SESSION_IDS.oneShot, 'one-shot'],
+      [VFS_EXAMPLE_SESSION_IDS.continuable, 'continuable'],
+    ] as const
+    for (const [id, mode] of expected) {
+      const { meta, events } = readSession(id)
+      expect(meta).toMatchObject({
+        id,
+        cwd: '/dsh/workspace',
+        parentSession: VFS_EXAMPLE_SESSION_IDS.main,
+        origin: 'subagent',
+        delegationDepth: 1,
+        agentPreset: 'standard',
+      })
+      expect(events.map(event => event.seq)).toEqual(events.map((_, index) => index))
+      expect(events.at(-1)).toMatchObject({ type: 'turn/end', data: { reason: { kind: 'completed' } } })
+      expect(foldSubagentDescriptor(events.slice(meta.seedLength ?? 0))).toMatchObject({ mode })
+      expect(() => Session.fromRestore(SessionId(meta.id), events, meta)).not.toThrow()
+    }
+  })
+})

+ 432 - 0
packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts

@@ -0,0 +1,432 @@
+/** Deterministic source for the filesystem tree bundled into the WebWorker preview. */
+
+import { fileURLToPath } from 'node:url'
+import { SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
+import {
+  eventLines, projectKey, toHeaderLine,
+} from '@deepseek-ai/dsh-session-persistence-jsonl/src/format.ts'
+import { snapshotSubagentDescriptor } from '@deepseek-ai/dsh-subagent'
+
+/** Root copied by the preview image's repository adapter. */
+export const VFS_EXAMPLE_ROOT = fileURLToPath(new URL('./fixtures/vfs-example', import.meta.url))
+
+/** Durable ids used by browser assertions and subagent parent links. */
+export const VFS_EXAMPLE_SESSION_IDS = {
+  main: SessionId('preview-showcase'),
+  oneShot: SessionId('preview-architecture-review'),
+  continuable: SessionId('preview-follow-up-builder'),
+} as const
+
+/** Stable title rendered in the root Session list. */
+export const VFS_EXAMPLE_TITLE = 'WebWorker Preview Showcase'
+
+/** Oldest prompt, intentionally outside the first 50-message history page. */
+export const VFS_EXAMPLE_OLDEST_MESSAGE = 'History checkpoint 01: verify deterministic preview state.'
+
+/** Settled tail marker used by browser acceptance and the demonstration GIF. */
+export const VFS_EXAMPLE_TAIL_MESSAGE = 'Preview tour complete'
+
+const WORKSPACE = '/dsh/workspace'
+const CREATED_AT = 1_787_472_000_000
+const HISTORICAL_TURNS = 28
+
+const PREVIEW_GUIDE = `# Preview Workspace
+
+This deterministic workspace is bundled with the browser-only preview.
+
+- \`src/preview.ts\` is the file changed by the example write result.
+- \`data/tasks.json\` mirrors the completed preview checklist.
+- \`.agents/skills/preview-tour/SKILL.md\` proves dot directories survive image packing.
+
+Refresh the preview to restore these image bytes.
+`
+
+const PREVIEW_SOURCE_BEFORE = 'export const previewStatus = \'draft\'\n'
+
+const PREVIEW_SOURCE = `export const previewStatus = 'ready'
+
+export const previewFeatures = ['tools', 'subagents', 'pagination'] as const
+`
+
+const TASKS = `${JSON.stringify({
+  title: 'Preview verification',
+  tasks: [
+    { name: 'Inspect tool cards', status: 'completed' },
+    { name: 'Open both subagents', status: 'completed' },
+    { name: 'Load earlier history', status: 'completed' },
+  ],
+}, null, 2)}\n`
+
+const SKILL = `---
+name: preview-tour
+description: Inspect the bundled Preview workspace and its deterministic Session examples.
+---
+
+# Preview tour
+
+Read the workspace files, inspect the tool gallery, open both subagent histories, and load the earlier conversation page.
+`
+
+interface EventDraft {
+  readonly type: string
+  readonly data: unknown
+  readonly surfaceOp?: 'append'
+  readonly sourceEventSeqs?: number[]
+  readonly ignorable?: true
+}
+
+class EventLog {
+  readonly events: SessionEvent[]
+  private nextTime: number
+
+  constructor(time: number, seed: readonly SessionEvent[] = []) {
+    this.events = seed.map(event => structuredClone(event))
+    this.nextTime = Math.max(time, (this.events.at(-1)?.time ?? time - 1) + 1)
+  }
+
+  add(draft: EventDraft): number {
+    const seq = this.events.length
+    this.events.push({ ...draft, seq, time: this.nextTime++ } as unknown as SessionEvent)
+    return seq
+  }
+}
+
+function userMessage(id: string, text: string): EventDraft {
+  return {
+    type: 'user/message',
+    data: {
+      id,
+      role: 'user',
+      content: [{ type: 'text', text }],
+      source: { kind: 'user' },
+    },
+    surfaceOp: 'append',
+  }
+}
+
+function assistantMessage(id: string, turn: number, step: number, content: unknown[]): EventDraft {
+  return {
+    type: 'assistant/message',
+    data: {
+      turn,
+      step,
+      message: {
+        id,
+        role: 'assistant',
+        content,
+        source: { kind: 'model', provider: 'preview-fixture', model: 'deterministic' },
+      },
+    },
+    sourceEventSeqs: [],
+    surfaceOp: 'append',
+  }
+}
+
+interface GalleryCall {
+  readonly id: string
+  readonly name: string
+  readonly args: Record<string, unknown>
+  readonly result: string
+  readonly meta?: unknown
+  readonly error?: { readonly name: string; readonly code: string }
+  readonly todos?: Array<{ readonly content: string; readonly status: 'pending' | 'in_progress' | 'completed' }>
+}
+
+function readResult(): { text: string; meta: unknown } {
+  const lines = PREVIEW_GUIDE.trimEnd().split('\n').map((text, index) => ({ number: index + 1, text }))
+  return {
+    text: `<path>PREVIEW.md</path>\n<type>file</type>\n<content>\n${lines.map(line => `${String(line.number)}: ${line.text}`).join('\n')}\n\n(End of file - total ${String(lines.length)} lines)\n</content>`,
+    meta: { path: 'PREVIEW.md', offset: 1, lines, totalLines: lines.length, lang: 'md' },
+  }
+}
+
+function galleryCalls(): GalleryCall[] {
+  const read = readResult()
+  return [
+    {
+      id: 'preview-read',
+      name: 'read',
+      args: { file_path: 'PREVIEW.md' },
+      result: read.text,
+      meta: read.meta,
+    },
+    {
+      id: 'preview-write',
+      name: 'write',
+      args: { file_path: 'src/preview.ts', content: PREVIEW_SOURCE },
+      result: '<path>src/preview.ts</path>\n<type>file</type>\n<content>\nUpdated file\n</content>',
+      meta: { diffs: [{ path: 'src/preview.ts', oldText: PREVIEW_SOURCE_BEFORE, newText: PREVIEW_SOURCE }] },
+    },
+    {
+      id: 'preview-bash',
+      name: 'bash',
+      args: { command: "printf 'preview ready\\n'", description: 'Print the preview readiness marker' },
+      result: 'preview ready\n',
+    },
+    {
+      id: 'preview-glob',
+      name: 'glob',
+      args: { pattern: '**/*', path: '.' },
+      result: 'PREVIEW.md\ndata/tasks.json\nsrc/preview.ts',
+      meta: {
+        shape: 'paths',
+        paths: ['PREVIEW.md', 'data/tasks.json', 'src/preview.ts'],
+        truncated: false,
+        total: 3,
+      },
+    },
+    {
+      id: 'preview-grep',
+      name: 'grep',
+      args: { pattern: 'preview', path: '.', include: '*.{md,ts,json}' },
+      result: 'PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = \'ready\'',
+      meta: {
+        shape: 'matches',
+        files: [
+          { path: 'PREVIEW.md', matches: [{ lineNumber: 3, line: 'This deterministic workspace is bundled with the browser-only preview.' }] },
+          { path: 'src/preview.ts', matches: [{ lineNumber: 1, line: "export const previewStatus = 'ready'" }] },
+        ],
+        truncated: false,
+        total: 2,
+      },
+    },
+    {
+      id: 'preview-web-search',
+      name: 'web_search',
+      args: { queries: ['Web Worker filesystem compatibility'] },
+      result: 'Browser workers can host deterministic in-memory filesystems.\n\nSources:\n1. MDN Web Workers API — https://developer.mozilla.org/docs/Web/API/Web_Workers_API',
+      meta: {
+        sources: [{
+          url: 'https://developer.mozilla.org/docs/Web/API/Web_Workers_API',
+          title: 'Web Workers API',
+          snippet: 'Web Workers run scripts in background threads.',
+        }],
+        truncated: false,
+        answer: 'Browser workers can host deterministic in-memory filesystems.',
+      },
+    },
+    {
+      id: 'preview-todo',
+      name: 'todo_write',
+      args: {
+        todos: [
+          { content: 'Inspect tool cards', status: 'completed' },
+          { content: 'Open both subagents', status: 'completed' },
+          { content: 'Load earlier history', status: 'in_progress' },
+        ],
+      },
+      result: 'Updated todo list: 0 pending, 1 in progress, 2 completed.',
+      todos: [
+        { content: 'Inspect tool cards', status: 'completed' },
+        { content: 'Open both subagents', status: 'completed' },
+        { content: 'Load earlier history', status: 'in_progress' },
+      ],
+    },
+    {
+      id: 'preview-subagent',
+      name: 'subagent',
+      args: { description: 'Continue preview verification', prompt: 'Check the remaining preview cases.', run_in_background: true },
+      result: `started subagent ${VFS_EXAMPLE_SESSION_IDS.continuable}`,
+    },
+    {
+      id: 'preview-subagent-fork',
+      name: 'subagent_fork',
+      args: { description: 'Review preview architecture', prompt: 'Review the fixture architecture.', run_in_background: false },
+      result: 'The preview fixture remains separate from user-owned WebFS data.',
+    },
+    {
+      id: 'preview-failure',
+      name: 'read',
+      args: { file_path: 'missing.txt' },
+      result: 'Error: ENOENT: no such file, open missing.txt',
+      error: { name: 'FsError', code: 'ENOENT' },
+    },
+  ]
+}
+
+function addClosedTextTurn(log: EventLog, turn: number): void {
+  const checkpoint = String(turn).padStart(2, '0')
+  log.add({ type: 'turn/start', data: { turn } })
+  log.add(userMessage(`preview-user-${checkpoint}`, `History checkpoint ${checkpoint}: verify deterministic preview state.`))
+  if (turn === 1) {
+    log.add({
+      type: 'session/title',
+      data: { title: VFS_EXAMPLE_TITLE, messageSeqs: [], source: { kind: 'user' } },
+    })
+  }
+  log.add({ type: 'step/start', data: { turn, step: 1 } })
+  log.add(assistantMessage(
+    `preview-assistant-${checkpoint}`,
+    turn,
+    1,
+    [{ type: 'text', text: `Checkpoint ${checkpoint} is recorded.` }],
+  ))
+  log.add({ type: 'step/end', data: { turn, step: 1 } })
+  log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
+}
+
+function mainLog(): { readonly events: SessionEvent[]; readonly forkSeedLength: number } {
+  const log = new EventLog(CREATED_AT)
+  for (let turn = 1; turn <= HISTORICAL_TURNS; turn++) addClosedTextTurn(log, turn)
+  const forkSeedLength = log.events.length
+  const turn = HISTORICAL_TURNS + 1
+  const calls = galleryCalls()
+
+  log.add({ type: 'turn/start', data: { turn } })
+  log.add(userMessage('preview-gallery-user', 'Show the seeded workspace, tool cards, subagents, and pagination in one tour.'))
+  log.add({ type: 'step/start', data: { turn, step: 1 } })
+  log.add(assistantMessage('preview-gallery-tools', turn, 1, [
+    { type: 'reasoning', text: 'I will inspect the deterministic workspace and collect each preview surface.' },
+    ...calls.map(call => ({ type: 'tool-call', id: call.id, name: call.name, arguments: JSON.stringify(call.args) })),
+  ]))
+  for (const call of calls) {
+    log.add({
+      type: 'tool/call',
+      data: { turn, step: 1, callId: call.id, name: call.name, arguments: JSON.stringify(call.args) },
+    })
+    if (call.todos !== undefined) log.add({ type: 'todo/write', data: { todos: call.todos } })
+    log.add({
+      type: 'tool/result',
+      data: {
+        turn,
+        step: 1,
+        message: {
+          id: `${call.id}-result`,
+          role: 'user',
+          content: [{
+            type: 'tool-result',
+            toolCallId: call.id,
+            content: [{ type: 'text', text: call.result }],
+            isError: call.error !== undefined,
+          }],
+          source: { kind: 'tool', callId: call.id },
+        },
+        ...call.meta === undefined ? {} : { meta: call.meta },
+        ...call.error === undefined ? {} : { error: call.error },
+      },
+      surfaceOp: 'append',
+    })
+  }
+  log.add({ type: 'step/end', data: { turn, step: 1 } })
+  log.add({ type: 'step/start', data: { turn, step: 2 } })
+  log.add(assistantMessage('preview-gallery-final', turn, 2, [{
+    type: 'text',
+    text: `## ${VFS_EXAMPLE_TAIL_MESSAGE}\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect.`,
+  }]))
+  log.add({ type: 'step/end', data: { turn, step: 2 } })
+  log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
+  return { events: log.events, forkSeedLength }
+}
+
+function oneShotLog(seed: readonly SessionEvent[]): SessionEvent[] {
+  const log = new EventLog(CREATED_AT + 100_000, seed)
+  log.add({ type: 'session/end-seed', data: {} })
+  const turn = HISTORICAL_TURNS + 1
+  log.add({ type: 'turn/start', data: { turn } })
+  log.add(userMessage('preview-review-user', 'Review whether the preview fixture is isolated from future WebFS data.'))
+  log.add({
+    type: 'subagent/descriptor',
+    data: snapshotSubagentDescriptor({
+      mode: 'one-shot', provider: 'fork', label: 'Review preview architecture',
+    }),
+  })
+  log.add({ type: 'step/start', data: { turn, step: 1 } })
+  log.add(assistantMessage('preview-review-assistant', turn, 1, [{
+    type: 'text',
+    text: 'The bundled fixture is static image content; future WebFS state remains user-owned.',
+  }]))
+  log.add({ type: 'step/end', data: { turn, step: 1 } })
+  log.add({ type: 'turn/end', data: { turn, reason: { kind: 'completed' } } })
+  return log.events
+}
+
+function continuableLog(): SessionEvent[] {
+  const log = new EventLog(CREATED_AT + 200_000)
+  log.add({ type: 'turn/start', data: { turn: 1 } })
+  log.add(userMessage('preview-builder-user', 'Check that the Preview workspace can support follow-up tasks.'))
+  log.add({
+    type: 'subagent/descriptor',
+    data: snapshotSubagentDescriptor({
+      mode: 'continuable', provider: 'spawn', label: 'Continue preview verification',
+    }),
+  })
+  log.add({ type: 'step/start', data: { turn: 1, step: 1 } })
+  log.add(assistantMessage('preview-builder-assistant', 1, 1, [{
+    type: 'text',
+    text: 'This child is continuable and ready for another verification turn.',
+  }]))
+  log.add({ type: 'step/end', data: { turn: 1, step: 1 } })
+  log.add({ type: 'turn/end', data: { turn: 1, reason: { kind: 'completed' } } })
+  return log.events
+}
+
+function header(
+  id: SessionHeader['id'],
+  createdAt: number,
+  child?: { readonly parentSession: SessionHeader['id']; readonly mode: 'one-shot' | 'continuable'; readonly seedLength?: number },
+): SessionHeader {
+  return {
+    version: 0,
+    id,
+    createdAt,
+    cwd: WORKSPACE,
+    delegationDepth: child === undefined ? 0 : 1,
+    agentPreset: 'standard',
+    ...child === undefined ? {} : {
+      parentSession: child.parentSession,
+      origin: 'subagent' as const,
+      ...child.seedLength === undefined ? {} : { seedLength: child.seedLength },
+    },
+  }
+}
+
+function renderLog(meta: SessionHeader, events: readonly SessionEvent[]): string {
+  return `${JSON.stringify(toHeaderLine(meta))}\n${eventLines(events, true)}\n`
+}
+
+/** Build every committed fixture file as repository-relative UTF-8 text. */
+export function buildVfsExampleFiles(): ReadonlyMap<string, string> {
+  const main = mainLog()
+  const project = projectKey(WORKSPACE)
+  const sessionPath = (id: string): string => `home/sessions/${project}/${id}/session.jsonl`
+  const projectionCache = `${JSON.stringify({
+    unit: { name: 'session_projcache', version: 3 },
+    global: null,
+    tables: {
+      sessions: {
+        [VFS_EXAMPLE_SESSION_IDS.main]: {
+          identity: { createdAt: CREATED_AT, cwd: WORKSPACE },
+          rows: {
+            title: { ver: 1, seq: main.events.at(-1)?.seq ?? -1, val: VFS_EXAMPLE_TITLE },
+          },
+        },
+      },
+    },
+  }, null, 2)}\n`
+  return new Map([
+    ['workspace/PREVIEW.md', PREVIEW_GUIDE],
+    ['workspace/src/preview.ts', PREVIEW_SOURCE],
+    ['workspace/data/tasks.json', TASKS],
+    ['workspace/.agents/skills/preview-tour/SKILL.md', SKILL],
+    ['home/storages/session_projcache.json', projectionCache],
+    [sessionPath(VFS_EXAMPLE_SESSION_IDS.main), renderLog(
+      header(VFS_EXAMPLE_SESSION_IDS.main, CREATED_AT),
+      main.events,
+    )],
+    [sessionPath(VFS_EXAMPLE_SESSION_IDS.oneShot), renderLog(
+      header(VFS_EXAMPLE_SESSION_IDS.oneShot, CREATED_AT + 100_000, {
+        parentSession: VFS_EXAMPLE_SESSION_IDS.main,
+        mode: 'one-shot',
+        seedLength: main.forkSeedLength,
+      }),
+      oneShotLog(main.events.slice(0, main.forkSeedLength)),
+    )],
+    [sessionPath(VFS_EXAMPLE_SESSION_IDS.continuable), renderLog(
+      header(VFS_EXAMPLE_SESSION_IDS.continuable, CREATED_AT + 200_000, {
+        parentSession: VFS_EXAMPLE_SESSION_IDS.main,
+        mode: 'continuable',
+      }),
+      continuableLog(),
+    )],
+  ])
+}

+ 9 - 0
pnpm-lock.yaml

@@ -4848,6 +4848,15 @@ importers:
       '@deepseek-ai/dsh-sandbox-policy':
         specifier: workspace:^
         version: link:../../sandbox/sandbox-policy
+      '@deepseek-ai/dsh-session':
+        specifier: workspace:^
+        version: link:../../core/session
+      '@deepseek-ai/dsh-session-persistence-jsonl':
+        specifier: workspace:^
+        version: link:../../session/session-persistence-jsonl
+      '@deepseek-ai/dsh-subagent':
+        specifier: workspace:^
+        version: link:../../subagent/subagent
       '@deepseek-ai/dsh-subprocess-local':
         specifier: workspace:^
         version: link:../../subprocess/subprocess-local

+ 13 - 1
scripts/session-fixture-layout.spec.ts

@@ -1,7 +1,7 @@
 import { describe, expect, it } from 'vitest'
 import { type SessionEvent } from '@deepseek-ai/dsh-session'
 import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
-import { canonicalSessionFixture } from './session-fixture-layout.ts'
+import { canonicalSessionFixture, isPhysicalSessionFixture } from './session-fixture-layout.ts'
 
 const HEADER = '  {"type":"session","version":0,"id":"fixture","createdAt":1,"delegationDepth":0}  '
 
@@ -68,3 +68,15 @@ describe('canonicalSessionFixture', () => {
       .toThrow(/broken\.jsonl: session snapshot line 2: malformed text-chunks storage row/)
   })
 })
+
+describe('isPhysicalSessionFixture', () => {
+  it('excludes only persisted logs under the WebWorker example root', () => {
+    expect(isPhysicalSessionFixture(
+      'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/main/session.jsonl',
+    )).toBe(true)
+    expect(isPhysicalSessionFixture(
+      'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/README.jsonl',
+    )).toBe(false)
+    expect(isPhysicalSessionFixture('apps/web/tests/snapshots/example/session.jsonl')).toBe(false)
+  })
+})

+ 15 - 0
scripts/session-fixture-layout.ts

@@ -7,6 +7,10 @@ import { resolve } from 'node:path'
 import { packChunkRuns, type SessionEvent } from '@deepseek-ai/dsh-session'
 import { parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
 
+/** Physical persistence artifacts validated by the WebWorker runtime fixture spec. */
+const PHYSICAL_SESSION_FIXTURE_ROOT =
+  'packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/'
+
 /** One repository session fixture and its canonical projected representation. */
 export interface SessionFixtureLayout {
   /** Repository-relative path with `/` separators. */
@@ -17,6 +21,16 @@ export interface SessionFixtureLayout {
   canonical: string
 }
 
+/**
+ * Whether a repository JSONL is a production-layout persistence artifact rather
+ * than an envelope-free replay snapshot owned by this script.
+ * @param path - Repository-relative path with `/` separators.
+ * @returns True only for Session logs under the WebWorker VFS example root.
+ */
+export function isPhysicalSessionFixture(path: string): boolean {
+  return path.startsWith(PHYSICAL_SESSION_FIXTURE_ROOT) && path.endsWith('/session.jsonl')
+}
+
 function isSessionHeader(value: unknown): boolean {
   return value !== null && typeof value === 'object' && (value as { type?: unknown }).type === 'session'
 }
@@ -109,6 +123,7 @@ function discoverJsonlFiles(root: string): string[] {
  */
 export function inspectSessionFixtureLayouts(root: string): SessionFixtureLayout[] {
   return discoverJsonlFiles(root).flatMap((path) => {
+    if (isPhysicalSessionFixture(path)) return []
     const source = readFileSync(resolve(root, path), 'utf8')
     const canonical = canonicalSessionFixture(source, path)
     return canonical === undefined ? [] : [{ path, source, canonical }]

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff