/** * Preview acceptance: the browser-only worker deployment boots the real Cordis * tree out of the packed VFS image and reaches an interactive page. * * `dist/preview.html` is the served page plus one bootstrap script tag, so this * run exercises the shipped startup chain: the worker mounts the image, * activates the tree, and answers the page's tunnel until the client settles. * 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 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 { 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 { 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, } from '@deepseek-ai/dsh-experimental-webworker-packer' import { IMAGE_FILE_NAME } from '@deepseek-ai/dsh-experimental-webworker-runtime' import { newEnglishPage, REPO_ROOT, saveFailureShot } from './support.ts' 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) /** Profile the preview deployment composes; `build:preview` packs the same one. */ const PROFILE = 'web' /** Pages the preview needs; the Vite build emits both. */ const PAGES = ['index.html', 'preview.html'] /** * Content types the preview loads. Anything else is served as opaque bytes. * * The image goes out as `application/gzip` with no `content-encoding`: the * worker inflates the gzip member itself, so a transport-decoded body would * leave its `DecompressionStream('gzip')` with plain tar bytes to inflate. */ const MIME: Record = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.svg': 'image/svg+xml', '.gz': 'application/gzip', '.webmanifest': 'application/manifest+json', '.woff2': 'font/woff2', } /** Boot line the worker host writes once its tree finished activating. */ const TREE_ACTIVE = 'webworker host: tree active' /** Image fetch, mount, and tree activation on a loaded machine. */ const BOOT_TIMEOUT_MS = 240_000 /** Client tree settle after the tunnel starts answering. */ const HERO_TIMEOUT_MS = 240_000 /** One served origin over `dist/`. */ interface Site { readonly origin: string /** Release the port; call after the browser is gone. */ close(): Promise } /** * Fail before the browser opens a page the build never produced. * @throws When either preview page is missing from `dist/`. */ function requirePreviewPages(): void { for (const page of PAGES) { if (existsSync(join(DIST_ROOT, page))) continue throw new Error(`preview boot needs apps/web/dist/${page} — run \`pnpm run build\` from the repository root`) } } /** * 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 * 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. * @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: () => {} } const packed = packVfsImage({ config: composeProfile(REPO_ROOT, PROFILE), profile: PROFILE, workspaces: indexWorkspacePackages(REPO_ROOT), resolveFrom: REPO_ROOT, configTrees: configTrees(REPO_ROOT), }) if (packed.missing.length > 0) { 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 }) } } } /** * Answer one request with the file it names under `dist/`; the image path * answers from wherever {@link requireVfsImage} put the file. * @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/`. */ async function respond(request: IncomingMessage, response: ServerResponse, imagePath: string): 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)) response.writeHead(200, { 'content-type': MIME[extname(relative)] ?? 'application/octet-stream' }) response.end(body) } catch { // A miss is a miss: the deployment has no SPA fallback, and hiding one // behind the index page would make a broken asset URL look like a boot // failure. response.writeHead(404) response.end(`not found: ${relative}`) } } /** * Serve `dist/` over loopback with static-host semantics. * @param imagePath - File behind `preview/`. * @returns The origin to navigate, and its teardown. */ async function serveDist(imagePath: string): Promise { const server = createServer((request, response) => { void respond(request, response, imagePath) }) 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') return { origin: `http://127.0.0.1:${String(address.port)}`, close: async () => { server.closeAllConnections() await new Promise((closed, reject) => { server.close((error) => { if (error === undefined) closed() else reject(error) }) }) }, } } /** * Bound one boot milestone so a stall names the milestone instead of surfacing * as the lane's generic test timeout. * @param work - The milestone to wait for. * @param ms - How long it may take. * @param stalled - Error message when it does not arrive in time. * @returns What `work` resolved to. */ async function within(work: Promise, ms: number, stalled: string): Promise { let timer: NodeJS.Timeout | undefined try { return await Promise.race([ work, new Promise((_, reject) => { timer = setTimeout(() => { reject(new Error(stalled)) }, ms) }), ]) } finally { clearTimeout(timer) } } it('boots the packed worker deployment to an interactive page', async () => { requirePreviewPages() const image = requireVfsImage() try { const site = await serveDist(image.path) try { const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-dev-shm-usage'] }) try { await bootPreview(site.origin, browser) } finally { await browser.close() } } finally { await site.close() } } finally { image.cleanup() } }, 600_000) /** * Open the preview page and hold it to both boot milestones. * @param origin - Origin serving `dist/`. * @param browser - Browser to open the page in. */ async function bootPreview(origin: string, browser: Browser): Promise { const page = await newEnglishPage(browser) const pageErrors: Error[] = [] page.on('pageerror', (error) => { pageErrors.push(error) }) // Registered before navigation: the worker reports its tree long before the // tunnel serves the client, so a listener added later would miss the line. const treeActive = new Promise((reported) => { page.on('console', (message) => { const text = message.text() if (text.includes(TREE_ACTIVE)) reported(text) }) }) try { await page.goto(`${origin}/preview.html`, { waitUntil: 'domcontentloaded' }) 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}`) // 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 }) expect(pageErrors.map(error => error.message)).toEqual([]) } catch (error) { await saveFailureShot(page, 'preview-boot') throw pageErrors.length === 0 ? error : new AggregateError([error, ...pageErrors], 'preview boot failed, with uncaught page errors') } }