From e883dc235474ff37fc49bad99ee6802ee68cb933 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Sun, 23 Aug 2026 22:53:28 +0800 Subject: [PATCH] feat(webworker): add selectable preview fixtures --- apps/web/src/preview.ts | 16 +- apps/web/tests/preview-boot.e2e.ts | 235 ++++++++-- .../preview-boot/source-chooser.expected.md | 15 + knip.json | 3 +- .../webworker-packer/package.json | 2 +- .../experimental/webworker-packer/src/bin.ts | 44 +- .../webworker-packer/src/index.ts | 7 +- .../experimental/webworker-packer/src/pack.ts | 66 ++- .../webworker-packer/src/repository.ts | 34 +- .../tests/image-loadable.spec.ts | 30 +- .../webworker-runtime/package.json | 3 + .../webworker-runtime/src/client/client.ts | 5 +- .../webworker-runtime/src/client/index.ts | 76 ++- .../src/client/source-chooser.ts | 178 ++++++++ .../webworker-runtime/src/fixture-manifest.ts | 67 +++ .../webworker-runtime/src/image-layout.ts | 12 +- .../webworker-runtime/src/index.ts | 8 +- .../builtin_modules/implemented/fs-watch.ts | 1 + .../builtin_modules/implemented/stream.ts | 2 + .../webworker-runtime/src/shell/fs-access.ts | 18 +- .../webworker-runtime/src/storage/memory.ts | 36 ++ .../webworker-runtime/src/transport/frames.ts | 10 +- .../webworker-runtime/src/worker-host.ts | 12 +- .../webworker-runtime/src/worker.ts | 10 +- .../tests/client/source-chooser.spec.ts | 150 ++++++ .../tests/fixture-manifest.spec.ts | 48 ++ .../preview-architecture-review/session.jsonl | 178 ++++++++ .../preview-follow-up-builder/session.jsonl | 8 + .../preview-showcase/session.jsonl | 200 ++++++++ .../home/storages/session_projcache.json | 24 + .../.agents/skills/preview-tour/SKILL.md | 8 + .../fixtures/vfs-example/workspace/PREVIEW.md | 9 + .../vfs-example/workspace/data/tasks.json | 17 + .../vfs-example/workspace/src/preview.ts | 3 + .../tests/node/child-process.spec.ts | 7 +- .../tests/storage/tar.spec.ts | 19 +- .../tests/transport/frames.spec.ts | 22 + .../tests/transport/tunnel-client.spec.ts | 21 + .../tests/vfs-example-fixture.spec.ts | 118 +++++ .../tests/vfs-example-fixture.ts | 432 ++++++++++++++++++ pnpm-lock.yaml | 9 + scripts/session-fixture-layout.spec.ts | 14 +- scripts/session-fixture-layout.ts | 15 + 43 files changed, 2084 insertions(+), 108 deletions(-) create mode 100644 apps/web/tests/snapshots/preview-boot/source-chooser.expected.md create mode 100644 packages/experimental/webworker-runtime/src/client/source-chooser.ts create mode 100644 packages/experimental/webworker-runtime/src/fixture-manifest.ts create mode 100644 packages/experimental/webworker-runtime/tests/client/source-chooser.spec.ts create mode 100644 packages/experimental/webworker-runtime/tests/fixture-manifest.spec.ts create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.jsonl create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/.agents/skills/preview-tour/SKILL.md create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/PREVIEW.md create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/data/tasks.json create mode 100644 packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/src/preview.ts create mode 100644 packages/experimental/webworker-runtime/tests/transport/frames.spec.ts create mode 100644 packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts create mode 100644 packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts diff --git a/apps/web/src/preview.ts b/apps/web/src/preview.ts index 586cbcab5d..f3d51f8bbc 100644 --- a/apps/web/src/preview.ts +++ b/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 }) diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 1b8bc85f32..b4675926bd 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/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 } +interface PreviewAssets { + /** Static-host-relative path to a generated file outside `dist/`. */ + readonly overrides: ReadonlyMap + 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/` 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() + 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/`. + * @param overrides - Generated deployment files used when `dist/` has none. */ -async function respond(request: IncomingMessage, response: ServerResponse, imagePath: string): Promise { +async function respond( + request: IncomingMessage, + response: ServerResponse, + overrides: ReadonlyMap, +): Promise { 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/`. + * @param overrides - Generated deployment files used when `dist/` has none. * @returns The origin to navigate, and its teardown. */ -async function serveDist(imagePath: string): Promise { - const server = createServer((request, response) => { void respond(request, response, imagePath) }) +async function serveDist(overrides: ReadonlyMap): Promise { + const server = createServer((request, response) => { void respond(request, response, overrides) }) await new Promise((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(work: Promise, 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 { }) 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 { 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 { 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 { : 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 { + 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((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 } + }).__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() + } +} diff --git a/apps/web/tests/snapshots/preview-boot/source-chooser.expected.md b/apps/web/tests/snapshots/preview-boot/source-chooser.expected.md new file mode 100644 index 0000000000..18dd2a442b --- /dev/null +++ b/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" diff --git a/knip.json b/knip.json index b457dd54f3..b6c4e33254 100644 --- a/knip.json +++ b/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", diff --git a/packages/experimental/webworker-packer/package.json b/packages/experimental/webworker-packer/package.json index e9219b35f9..a8155676dd 100644 --- a/packages/experimental/webworker-packer/package.json +++ b/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": { diff --git a/packages/experimental/webworker-packer/src/bin.ts b/packages/experimental/webworker-packer/src/bin.ts index 57a3c77730..093fac23d5 100644 --- a/packages/experimental/webworker-packer/src/bin.ts +++ b/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 [--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')) diff --git a/packages/experimental/webworker-packer/src/index.ts b/packages/experimental/webworker-packer/src/index.ts index ea054b26b0..ea203a6f7a 100644 --- a/packages/experimental/webworker-packer/src/index.ts +++ b/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' diff --git a/packages/experimental/webworker-packer/src/pack.ts b/packages/experimental/webworker-packer/src/pack.ts index a5dcb00156..3e86610233 100644 --- a/packages/experimental/webworker-packer/src/pack.ts +++ b/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 => JSON.parse(readFileSync(file, 'utf8')) as Record @@ -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 } +} diff --git a/packages/experimental/webworker-packer/src/repository.ts b/packages/experimental/webworker-packer/src/repository.ts index 6ae2e59bee..5e433ea425 100644 --- a/packages/experimental/webworker-packer/src/repository.ts +++ b/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. * diff --git a/packages/experimental/webworker-packer/tests/image-loadable.spec.ts b/packages/experimental/webworker-packer/tests/image-loadable.spec.ts index e75b4f523b..609e3ccf80 100644 --- a/packages/experimental/webworker-packer/tests/image-loadable.spec.ts +++ b/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 diff --git a/packages/experimental/webworker-runtime/package.json b/packages/experimental/webworker-runtime/package.json index 422bd4f5ec..68643425f8 100644 --- a/packages/experimental/webworker-runtime/package.json +++ b/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", diff --git a/packages/experimental/webworker-runtime/src/client/client.ts b/packages/experimental/webworker-runtime/src/client/client.ts index 7fa05a5355..0300347297 100644 --- a/packages/experimental/webworker-runtime/src/client/client.ts +++ b/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). */ diff --git a/packages/experimental/webworker-runtime/src/client/index.ts b/packages/experimental/webworker-runtime/src/client/index.ts index c84f637896..8494e1c06a 100644 --- a/packages/experimental/webworker-runtime/src/client/index.ts +++ b/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 } +function bootReadyGate(): PromiseWithResolvers { + return (globalThis as BootReadyGlobal).__DSH_BOOT_READY__ ??= Promise.withResolvers() +} + +/** + * 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 { + 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 { - const ready = (globalThis as BootReadyGlobal).__DSH_BOOT_READY__ ??= Promise.withResolvers() + 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), diff --git a/packages/experimental/webworker-runtime/src/client/source-chooser.ts b/packages/experimental/webworker-runtime/src/client/source-chooser.ts new file mode 100644 index 0000000000..6a5174ccd3 --- /dev/null +++ b/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> = { + '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', +} + +function escapeMarkup(value: string): string { + return value.replace(/[&<>"']/g, character => ENTITIES[character] ?? character) +} + +function optionMarkup(choice: PreviewSourceChoice, selected: string): string { + return `` +} + +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 { + 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 = `
+
+

选择 Preview 数据源

+

数据会在 Worker 和应用启动前挂载;刷新页面可重新选择。

+
+ 文件系统来源 + ${choices.map(choice => optionMarkup(choice, selected)).join('')} +
+ +
+
` + const form = root.querySelector('[data-preview-source-card]') + if (form === null) throw new Error('preview source chooser: form was not rendered') + const sourceId = await new Promise((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 +} diff --git a/packages/experimental/webworker-runtime/src/fixture-manifest.ts b/packages/experimental/webworker-runtime/src/fixture-manifest.ts new file mode 100644 index 0000000000..d594960576 --- /dev/null +++ b/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 | undefined { + return typeof value === 'object' && value !== null && !Array.isArray(value) + ? value as Record + : 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() + 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 } +} diff --git a/packages/experimental/webworker-runtime/src/image-layout.ts b/packages/experimental/webworker-runtime/src/image-layout.ts index 83693ea292..3e16f5a529 100644 --- a/packages/experimental/webworker-runtime/src/image-layout.ts +++ b/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 diff --git a/packages/experimental/webworker-runtime/src/index.ts b/packages/experimental/webworker-runtime/src/index.ts index cf39694ca0..e24faa1eb7 100644 --- a/packages/experimental/webworker-runtime/src/index.ts +++ b/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' diff --git a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs-watch.ts b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs-watch.ts index 396ae3c22e..71864a7caf 100644 --- a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs-watch.ts +++ b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs-watch.ts @@ -413,6 +413,7 @@ export function watchAsync( throw(reason?: unknown): Promise> { 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) }, } diff --git a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/stream.ts b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/stream.ts index 757961a42a..a7fca23540 100644 --- a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/stream.ts +++ b/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. diff --git a/packages/experimental/webworker-runtime/src/shell/fs-access.ts b/packages/experimental/webworker-runtime/src/shell/fs-access.ts index 48da48302a..9099c43559 100644 --- a/packages/experimental/webworker-runtime/src/shell/fs-access.ts +++ b/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 => requireActiveVfs() - const stat = async (path: string): Promise => { + const stat = (path: string): Promise => { 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 => { + writeText: (path: string, text: string, append = false): Promise => { if (append) vfs().appendFileSync(path, text) else vfs().writeFileSync(path, text) + return Promise.resolve() }, - mkdir: async (path: string, recursive: boolean): Promise => { + mkdir: (path: string, recursive: boolean): Promise => { vfs().mkdirSync(path, { recursive }) + return Promise.resolve() }, - remove: async (path: string, options: { recursive: boolean; force: boolean }): Promise => { + remove: (path: string, options: { recursive: boolean; force: boolean }): Promise => { vfs().rmSync(path, options) + return Promise.resolve() }, - rename: async (from: string, to: string): Promise => { + rename: (from: string, to: string): Promise => { vfs().renameSync(from, to) + return Promise.resolve() }, } } diff --git a/packages/experimental/webworker-runtime/src/storage/memory.ts b/packages/experimental/webworker-runtime/src/storage/memory.ts index 96a63f859f..6f41fbc9ee 100644 --- a/packages/experimental/webworker-runtime/src/storage/memory.ts +++ b/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 +} diff --git a/packages/experimental/webworker-runtime/src/transport/frames.ts b/packages/experimental/webworker-runtime/src/transport/frames.ts index 67d89b73fd..a04a2cec5a 100644 --- a/packages/experimental/webworker-runtime/src/transport/frames.ts +++ b/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') { diff --git a/packages/experimental/webworker-runtime/src/worker-host.ts b/packages/experimental/webworker-runtime/src/worker-host.ts index f73f1d2e5c..bbbf6a3900 100644 --- a/packages/experimental/webworker-runtime/src/worker-host.ts +++ b/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 /** 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 `/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), diff --git a/packages/experimental/webworker-runtime/src/worker.ts b/packages/experimental/webworker-runtime/src/worker.ts index f82c5fd1fb..f0912aef93 100644 --- a/packages/experimental/webworker-runtime/src/worker.ts +++ b/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) { diff --git a/packages/experimental/webworker-runtime/tests/client/source-chooser.spec.ts b/packages/experimental/webworker-runtime/tests/client/source-chooser.spec.ts new file mode 100644 index 0000000000..6c629a2af8 --- /dev/null +++ b/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 & "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 { + const fetch = vi.fn(async () => Response.json(manifest)) + vi.stubGlobal('fetch', fetch) + return fetch +} + +function submitChooser(): void { + const form = document.querySelector('[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 = '
' + 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('input[value="example"]')?.checked).toBe(true) + expect(document.querySelector('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('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 = '
' + 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 = '
' + 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 = '
' + const unavailableSelection = choosePreviewSource(MANIFEST_URL) + await vi.waitFor(() => { expect(document.querySelector('form')).not.toBeNull() }) + const selected = document.querySelector('input:checked') + if (selected === null) throw new Error('test selection is missing') + selected.value = 'missing' + submitChooser() + await expect(unavailableSelection).rejects.toThrow(/unavailable source/) + }) +}) diff --git a/packages/experimental/webworker-runtime/tests/fixture-manifest.spec.ts b/packages/experimental/webworker-runtime/tests/fixture-manifest.spec.ts new file mode 100644 index 0000000000..01b3d00e9a --- /dev/null +++ b/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) + }) +}) diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-architecture-review/session.jsonl new file mode 100644 index 0000000000..291a18ba1e --- /dev/null +++ b/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} diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-follow-up-builder/session.jsonl new file mode 100644 index 0000000000..6353e854c4 --- /dev/null +++ b/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} diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.jsonl b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.jsonl new file mode 100644 index 0000000000..7b15d63fb3 --- /dev/null +++ b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/sessions/--dsh-workspace--/preview-showcase/session.jsonl @@ -0,0 +1,200 @@ +{"type":"session","version":0,"id":"preview-showcase","createdAt":1787472000000,"cwd":"/dsh/workspace","delegationDepth":0,"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":"turn/start","data":{"turn":29},"seq":169,"time":1787472000169} +{"type":"user/message","data":{"id":"preview-gallery-user","role":"user","content":[{"type":"text","text":"Show the seeded workspace, tool cards, subagents, and pagination in one tour."}],"source":{"kind":"user"}},"surfaceOp":"append","seq":170,"time":1787472000170} +{"type":"step/start","data":{"turn":29,"step":1},"seq":171,"time":1787472000171} +{"type":"assistant/message","data":{"turn":29,"step":1,"message":{"id":"preview-gallery-tools","role":"assistant","content":[{"type":"reasoning","text":"I will inspect the deterministic workspace and collect each preview surface."},{"type":"tool-call","id":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},{"type":"tool-call","id":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"},{"type":"tool-call","id":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"},{"type":"tool-call","id":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},{"type":"tool-call","id":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},{"type":"tool-call","id":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"},{"type":"tool-call","id":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"},{"type":"tool-call","id":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"},{"type":"tool-call","id":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"},{"type":"tool-call","id":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":172,"time":1787472000172} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-read","name":"read","arguments":"{\"file_path\":\"PREVIEW.md\"}"},"seq":173,"time":1787472000173} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-read-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-read","content":[{"type":"text","text":"PREVIEW.md\nfile\n\n1: # Preview Workspace\n2: \n3: This deterministic workspace is bundled with the browser-only preview.\n4: \n5: - `src/preview.ts` is the file changed by the example write result.\n6: - `data/tasks.json` mirrors the completed preview checklist.\n7: - `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing.\n8: \n9: Refresh the preview to restore these image bytes.\n\n(End of file - total 9 lines)\n"}],"isError":false}],"source":{"kind":"tool","callId":"preview-read"}},"meta":{"path":"PREVIEW.md","offset":1,"lines":[{"number":1,"text":"# Preview Workspace"},{"number":2,"text":""},{"number":3,"text":"This deterministic workspace is bundled with the browser-only preview."},{"number":4,"text":""},{"number":5,"text":"- `src/preview.ts` is the file changed by the example write result."},{"number":6,"text":"- `data/tasks.json` mirrors the completed preview checklist."},{"number":7,"text":"- `.agents/skills/preview-tour/SKILL.md` proves dot directories survive image packing."},{"number":8,"text":""},{"number":9,"text":"Refresh the preview to restore these image bytes."}],"totalLines":9,"lang":"md"}},"surfaceOp":"append","seq":174,"time":1787472000174} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-write","name":"write","arguments":"{\"file_path\":\"src/preview.ts\",\"content\":\"export const previewStatus = 'ready'\\n\\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\\n\"}"},"seq":175,"time":1787472000175} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-write-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-write","content":[{"type":"text","text":"src/preview.ts\nfile\n\nUpdated file\n"}],"isError":false}],"source":{"kind":"tool","callId":"preview-write"}},"meta":{"diffs":[{"path":"src/preview.ts","oldText":"export const previewStatus = 'draft'\n","newText":"export const previewStatus = 'ready'\n\nexport const previewFeatures = ['tools', 'subagents', 'pagination'] as const\n"}]}},"surfaceOp":"append","seq":176,"time":1787472000176} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-bash","name":"bash","arguments":"{\"command\":\"printf 'preview ready\\\\n'\",\"description\":\"Print the preview readiness marker\"}"},"seq":177,"time":1787472000177} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-bash-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-bash","content":[{"type":"text","text":"preview ready\n"}],"isError":false}],"source":{"kind":"tool","callId":"preview-bash"}}},"surfaceOp":"append","seq":178,"time":1787472000178} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-glob","name":"glob","arguments":"{\"pattern\":\"**/*\",\"path\":\".\"}"},"seq":179,"time":1787472000179} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-glob-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-glob","content":[{"type":"text","text":"PREVIEW.md\ndata/tasks.json\nsrc/preview.ts"}],"isError":false}],"source":{"kind":"tool","callId":"preview-glob"}},"meta":{"shape":"paths","paths":["PREVIEW.md","data/tasks.json","src/preview.ts"],"truncated":false,"total":3}},"surfaceOp":"append","seq":180,"time":1787472000180} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-grep","name":"grep","arguments":"{\"pattern\":\"preview\",\"path\":\".\",\"include\":\"*.{md,ts,json}\"}"},"seq":181,"time":1787472000181} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-grep-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-grep","content":[{"type":"text","text":"PREVIEW.md:3:This deterministic workspace is bundled with the browser-only preview.\nsrc/preview.ts:1:export const previewStatus = 'ready'"}],"isError":false}],"source":{"kind":"tool","callId":"preview-grep"}},"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}},"surfaceOp":"append","seq":182,"time":1787472000182} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-web-search","name":"web_search","arguments":"{\"queries\":[\"Web Worker filesystem compatibility\"]}"},"seq":183,"time":1787472000183} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-web-search-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-web-search","content":[{"type":"text","text":"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"}],"isError":false}],"source":{"kind":"tool","callId":"preview-web-search"}},"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."}},"surfaceOp":"append","seq":184,"time":1787472000184} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-todo","name":"todo_write","arguments":"{\"todos\":[{\"content\":\"Inspect tool cards\",\"status\":\"completed\"},{\"content\":\"Open both subagents\",\"status\":\"completed\"},{\"content\":\"Load earlier history\",\"status\":\"in_progress\"}]}"},"seq":185,"time":1787472000185} +{"type":"todo/write","data":{"todos":[{"content":"Inspect tool cards","status":"completed"},{"content":"Open both subagents","status":"completed"},{"content":"Load earlier history","status":"in_progress"}]},"seq":186,"time":1787472000186} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-todo-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-todo","content":[{"type":"text","text":"Updated todo list: 0 pending, 1 in progress, 2 completed."}],"isError":false}],"source":{"kind":"tool","callId":"preview-todo"}}},"surfaceOp":"append","seq":187,"time":1787472000187} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-subagent","name":"subagent","arguments":"{\"description\":\"Continue preview verification\",\"prompt\":\"Check the remaining preview cases.\",\"run_in_background\":true}"},"seq":188,"time":1787472000188} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-subagent-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-subagent","content":[{"type":"text","text":"started subagent preview-follow-up-builder"}],"isError":false}],"source":{"kind":"tool","callId":"preview-subagent"}}},"surfaceOp":"append","seq":189,"time":1787472000189} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-subagent-fork","name":"subagent_fork","arguments":"{\"description\":\"Review preview architecture\",\"prompt\":\"Review the fixture architecture.\",\"run_in_background\":false}"},"seq":190,"time":1787472000190} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-subagent-fork-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-subagent-fork","content":[{"type":"text","text":"The preview fixture remains separate from user-owned WebFS data."}],"isError":false}],"source":{"kind":"tool","callId":"preview-subagent-fork"}}},"surfaceOp":"append","seq":191,"time":1787472000191} +{"type":"tool/call","data":{"turn":29,"step":1,"callId":"preview-failure","name":"read","arguments":"{\"file_path\":\"missing.txt\"}"},"seq":192,"time":1787472000192} +{"type":"tool/result","data":{"turn":29,"step":1,"message":{"id":"preview-failure-result","role":"user","content":[{"type":"tool-result","toolCallId":"preview-failure","content":[{"type":"text","text":"Error: ENOENT: no such file, open missing.txt"}],"isError":true}],"source":{"kind":"tool","callId":"preview-failure"}},"error":{"name":"FsError","code":"ENOENT"}},"surfaceOp":"append","seq":193,"time":1787472000193} +{"type":"step/end","data":{"turn":29,"step":1},"seq":194,"time":1787472000194} +{"type":"step/start","data":{"turn":29,"step":2},"seq":195,"time":1787472000195} +{"type":"assistant/message","data":{"turn":29,"step":2,"message":{"id":"preview-gallery-final","role":"assistant","content":[{"type":"text","text":"## Preview tour complete\n\nThe workspace, specialized tool cards, two subagent histories, and an earlier history page are ready to inspect."}],"source":{"kind":"model","provider":"preview-fixture","model":"deterministic"}}},"sourceEventSeqs":[],"surfaceOp":"append","seq":196,"time":1787472000196} +{"type":"step/end","data":{"turn":29,"step":2},"seq":197,"time":1787472000197} +{"type":"turn/end","data":{"turn":29,"reason":{"kind":"completed"}},"seq":198,"time":1787472000198} diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/home/storages/session_projcache.json new file mode 100644 index 0000000000..9248dd7ae7 --- /dev/null +++ b/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" + } + } + } + } + } +} diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/.agents/skills/preview-tour/SKILL.md b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/.agents/skills/preview-tour/SKILL.md new file mode 100644 index 0000000000..4d4f875565 --- /dev/null +++ b/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. diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/PREVIEW.md b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/PREVIEW.md new file mode 100644 index 0000000000..ae96a96efe --- /dev/null +++ b/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. diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/data/tasks.json b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/data/tasks.json new file mode 100644 index 0000000000..65ad487ce7 --- /dev/null +++ b/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" + } + ] +} diff --git a/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/src/preview.ts b/packages/experimental/webworker-runtime/tests/fixtures/vfs-example/workspace/src/preview.ts new file mode 100644 index 0000000000..1494e6ea20 --- /dev/null +++ b/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 diff --git a/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts b/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts index 6a5826a19a..e61693e479 100644 --- a/packages/experimental/webworker-runtime/tests/node/child-process.spec.ts +++ b/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 () => { diff --git a/packages/experimental/webworker-runtime/tests/storage/tar.spec.ts b/packages/experimental/webworker-runtime/tests/storage/tar.spec.ts index 47aee5ec6b..efde651e9c 100644 --- a/packages/experimental/webworker-runtime/tests/storage/tar.spec.ts +++ b/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') + }) }) diff --git a/packages/experimental/webworker-runtime/tests/transport/frames.spec.ts b/packages/experimental/webworker-runtime/tests/transport/frames.spec.ts new file mode 100644 index 0000000000..9519962309 --- /dev/null +++ b/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/) + }) +}) diff --git a/packages/experimental/webworker-runtime/tests/transport/tunnel-client.spec.ts b/packages/experimental/webworker-runtime/tests/transport/tunnel-client.spec.ts index ceacba25df..64e550dc48 100644 --- a/packages/experimental/webworker-runtime/tests/transport/tunnel-client.spec.ts +++ b/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() diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.spec.ts new file mode 100644 index 0000000000..561cf04699 --- /dev/null +++ b/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 { + 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 } + } + 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() + } + }) +}) diff --git a/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts b/packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts new file mode 100644 index 0000000000..dbc151eb86 --- /dev/null +++ b/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 + 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: `PREVIEW.md\nfile\n\n${lines.map(line => `${String(line.number)}: ${line.text}`).join('\n')}\n\n(End of file - total ${String(lines.length)} lines)\n`, + 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: 'src/preview.ts\nfile\n\nUpdated file\n', + 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 { + 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(), + )], + ]) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5c25bbf2ce..e763188fd0 100644 --- a/pnpm-lock.yaml +++ b/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 diff --git a/scripts/session-fixture-layout.spec.ts b/scripts/session-fixture-layout.spec.ts index c0b87953b7..05dfd7a4d0 100644 --- a/scripts/session-fixture-layout.spec.ts +++ b/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) + }) +}) diff --git a/scripts/session-fixture-layout.ts b/scripts/session-fixture-layout.ts index 996b40fbca..e26cb7f90b 100644 --- a/scripts/session-fixture-layout.ts +++ b/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 }]