Selaa lähdekoodia

test(release): cover packed experimental worker consumers

Tianyi Cui 1 viikko sitten
vanhempi
sitoutus
b5d463ca5c

+ 2 - 2
.agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.md
-2026-09-12-publish-all-experimental-packages.md: 41dff8fbc618a44c101b720f279a7dcf46c17ed3
-2026-09-12-publish-all-experimental-packages.zh.md: 142c48ebe6ffc5725891d6c51afbc4061e069b51
+2026-09-12-publish-all-experimental-packages.md: 8bef877297d096159fcce60e6b479e12461357f2
+2026-09-12-publish-all-experimental-packages.zh.md: 6163e6b4ac11c46dac3897361585d2fcc29ef746

+ 1 - 1
.agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.md

@@ -12,7 +12,7 @@ Users need npm access to Auto review, the Inspector, the CPython PTC backend, an
 
 Every current package under `packages/experimental/` publishes in the dsh release family and the local npm baseline. Each manifest omits `private` and sets `publishConfig.access: public`. The [publication denylist](2026-09-12-experimental-publication-denylist.md) remains available for future private exceptions but contains no directories. Experimental npm names, opt-in composition, dependency isolation, compatibility expectations, and support limits remain unchanged.
 
-The Inspector tarball includes `lib/worker.js`, which its Host entry starts by a sibling URL. Its built-artifact test packs and extracts the package before starting that Worker. The WebWorker packer imports module-proxy and replacement-package tables through the runtime's public library entry; its compiled repository chunk requires no runtime TypeScript source.
+The Inspector tarball includes `lib/worker.js`, which its Host entry starts by a sibling URL. Its built-artifact test packs and extracts the package before starting that Worker. The WebWorker packer imports module-proxy and replacement-package tables through the runtime's public library entry; its compiled repository chunk requires no runtime TypeScript source. Its packed-consumer test imports both extracted tarballs under plain Node and mounts the resulting base image and data overlay without either package's source tree.
 
 The CPython PTC backend ships its Python scripts and still requires a supported external interpreter on Unix. The browser-worker packages expose installed library APIs; the packer's repository CLI still requires its documented built checkout. Publication does not add these packages to any shipped profile or make the browser preview a product launcher.
 

+ 1 - 1
.agents/notes/implemented/process/2026-09-12-publish-all-experimental-packages.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 `packages/experimental/` 下的所有当前包都通过 dsh 发布系列与本地 npm baseline 发布。各 manifest(元数据清单)省略 `private`,并设置 `publishConfig.access: public`。[发布拒绝列表](2026-09-12-experimental-publication-denylist.zh.md)仍可用于未来的私有例外,但不包含任何目录。实验性 npm 名称、显式启用组合、依赖隔离、兼容性预期与支持限制保持不变。
 
-Inspector tarball 包含 `lib/worker.js`,Host 入口通过相邻 URL 启动它。其构建产物测试先打包并解压包,再启动该 Worker。WebWorker 打包器通过运行时的公开库入口导入模块代理与替换包表;编译后的 repository 分片不需要运行时 TypeScript 源码。
+Inspector tarball 包含 `lib/worker.js`,Host 入口通过相邻 URL 启动它。其构建产物测试先打包并解压包,再启动该 Worker。WebWorker 打包器通过运行时的公开库入口导入模块代理与替换包表;编译后的 repository 分片不需要运行时 TypeScript 源码。其已打包消费方测试在普通 Node 下导入两个解压后的 tarball,并在不使用这两个包源码目录的情况下挂载生成的基础镜像与数据叠加层。
 
 CPython PTC 后端携带 Python 脚本,在 Unix 上仍需要受支持的外部解释器。浏览器 worker 包提供已安装库 API;打包器的仓库 CLI(命令行界面)仍需要文档要求的已构建 checkout。发布不会把这些包加入任何随附 profile,也不会让浏览器预览成为产品启动入口。
 

+ 31 - 19
packages/experimental/inspector/tests/built-lib.e2e.ts

@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { fileURLToPath } from 'node:url'
 import { execa } from 'execa'
-import { describe, expect, it, onTestFinished } from 'vitest'
+import { describe, expect, it } from 'vitest'
 import { pnpmInvocation } from '../../../../scripts/pnpm-invocation.ts'
 
 const packageDirectory = fileURLToPath(new URL('..', import.meta.url))
@@ -14,20 +14,42 @@ const built = [
 ].every(file => existsSync(join(packageDirectory, file)))
 
 describe.skipIf(!built)('experimental Inspector built artifact', () => {
-  it('packs its sibling Worker and evaluates the Host from the tarball through plain Node', async (test) => {
+  it('packs its sibling Worker and evaluates the Host from the tarball through plain Node', { retry: 0 }, async (test) => {
     const root = await mkdtemp(join(tmpdir(), 'dsh-inspector-packed-'))
     const consumer = join(root, 'package')
     const dependencies = join(consumer, 'node_modules')
     let linked = false
-    onTestFinished(async () => {
-      if (linked) await unlink(dependencies)
-      await rm(root, { recursive: true, force: true })
+    let pending: Promise<string> = Promise.resolve('')
+    test.onTestFinished(async () => {
+      try {
+        await pending
+      } finally {
+        if (linked) await unlink(dependencies)
+        await rm(root, { recursive: true, force: true })
+      }
     })
+    const run = (command: string, args: string[], cwd: string): Promise<string> => {
+      pending = execa(command, args, {
+        cwd,
+        stdin: 'ignore',
+        timeout: test.task.timeout,
+        cancelSignal: test.signal,
+        killSignal: 'SIGKILL',
+        reject: false,
+      }).then((result) => {
+        expect(result.timedOut, `stderr:\n${result.stderr}`).toBe(false)
+        expect(result.isCanceled, `stderr:\n${result.stderr}`).toBe(false)
+        expect(result.signal, `stderr:\n${result.stderr}`).toBeUndefined()
+        expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0)
+        return result.stdout
+      })
+      return pending
+    }
     const invocation = pnpmInvocation(['pack', '--pack-destination', root])
-    await execa(invocation.command, invocation.args, { cwd: packageDirectory, timeout: test.task.timeout })
+    await run(invocation.command, invocation.args, packageDirectory)
     const archives = (await readdir(root)).filter(name => name.endsWith('.tgz'))
     expect(archives).toHaveLength(1)
-    await execa('tar', ['-xzf', join(root, archives[0]!), '-C', root], { timeout: test.task.timeout })
+    await run('tar', ['-xzf', join(root, archives[0]!), '-C', root], root)
     expect(existsSync(join(consumer, 'lib/worker.js'))).toBe(true)
     await symlink(join(packageDirectory, 'node_modules'), dependencies, process.platform === 'win32' ? 'junction' : 'dir')
     linked = true
@@ -63,17 +85,7 @@ describe.skipIf(!built)('experimental Inspector built artifact', () => {
         await inspector.close()
       }
     `
-    const result = await execa(process.execPath, ['--input-type=module', '-e', script], {
-      cwd: consumer,
-      stdin: 'ignore',
-      timeout: test.task.timeout,
-      killSignal: 'SIGKILL',
-      reject: false,
-    })
-
-    expect(result.timedOut, `stderr:\n${result.stderr}`).toBe(false)
-    expect(result.signal, `stderr:\n${result.stderr}`).toBeUndefined()
-    expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0)
-    expect(JSON.parse(result.stdout.trim()) as unknown).toEqual({ type: 'number', value: 42, description: '42' })
+    const stdout = await run(process.execPath, ['--input-type=module', '-e', script], consumer)
+    expect(JSON.parse(stdout.trim()) as unknown).toEqual({ type: 'number', value: 42, description: '42' })
   })
 })

+ 100 - 0
packages/experimental/webworker-packer/tests/built-lib.e2e.ts

@@ -0,0 +1,100 @@
+/** Installed packer/runtime interoperability without either package's source tree. */
+import { existsSync } from 'node:fs'
+import { mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, unlink, writeFile } from 'node:fs/promises'
+import { tmpdir } from 'node:os'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { execa } from 'execa'
+import { expect, it } from 'vitest'
+import { pnpmInvocation } from '../../../../scripts/pnpm-invocation.ts'
+
+const experimentalDirectory = fileURLToPath(new URL('../..', import.meta.url))
+const packages = ['webworker-runtime', 'webworker-packer']
+const packageNames = new Set(packages.map(name => `@deepseek-ai/dsh-experimental-${name}`))
+
+it('loads both tarballs through plain Node and mounts their base image and overlay', { retry: 0 }, async (test) => {
+  const root = await mkdtemp(join(tmpdir(), 'dsh-webworker-packed-'))
+  const links: string[] = []
+  let pending: Promise<string> = Promise.resolve('')
+  test.onTestFinished(async () => {
+    try {
+      await pending
+    } finally {
+      for (const path of links.reverse()) await unlink(path)
+      await rm(root, { recursive: true, force: true })
+    }
+  })
+  const run = (command: string, args: string[], cwd: string): Promise<string> => {
+    pending = execa(command, args, {
+      cwd,
+      env: { NODE_OPTIONS: undefined, NODE_PATH: undefined, TSX_TSCONFIG_PATH: undefined },
+      stdin: 'ignore',
+      timeout: test.task.timeout,
+      cancelSignal: test.signal,
+      killSignal: 'SIGKILL',
+      reject: false,
+    }).then((result) => {
+      expect(result.timedOut, `stderr:\n${result.stderr}`).toBe(false)
+      expect(result.isCanceled, `stderr:\n${result.stderr}`).toBe(false)
+      expect(result.signal, `stderr:\n${result.stderr}`).toBeUndefined()
+      expect(result.exitCode, `stderr:\n${result.stderr}`).toBe(0)
+      return result.stdout
+    })
+    return pending
+  }
+  await writeFile(join(root, 'package.json'), JSON.stringify({ private: true, type: 'module' }))
+  for (const name of packages) {
+    const source = join(experimentalDirectory, name)
+    const directory = join(root, 'node_modules/@deepseek-ai', `dsh-experimental-${name}`)
+    const packRoot = join(root, name)
+    await mkdir(packRoot, { recursive: true })
+    await mkdir(directory, { recursive: true })
+    const invocation = pnpmInvocation(['pack', '--pack-destination', packRoot])
+    await run(invocation.command, invocation.args, source)
+    const archives = (await readdir(packRoot)).filter(file => file.endsWith('.tgz'))
+    expect(archives).toHaveLength(1)
+    await run('tar', ['-xzf', join(packRoot, archives[0]!), '-C', directory, '--strip-components=1'], root)
+    expect(existsSync(join(directory, 'src'))).toBe(false)
+    const manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as {
+      dependencies?: Record<string, string>
+      peerDependencies?: Record<string, string>
+    }
+    // Only the two subjects resolve from tarballs; their declared external dependencies use the built checkout.
+    for (const dependency of Object.keys({ ...manifest.dependencies, ...manifest.peerDependencies })) {
+      if (packageNames.has(dependency)) continue
+      const target = join(root, 'node_modules', dependency)
+      if (existsSync(target)) continue
+      await mkdir(dirname(target), { recursive: true })
+      await symlink(await realpath(join(source, 'node_modules', dependency)), target, process.platform === 'win32' ? 'junction' : 'dir')
+      links.push(target)
+    }
+  }
+  const script = `
+    import assert from 'node:assert/strict'
+    import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+    import { fileURLToPath } from 'node:url'
+    import * as packer from '@deepseek-ai/dsh-experimental-webworker-packer'
+    import * as runtime from '@deepseek-ai/dsh-experimental-webworker-runtime'
+    import * as client from '@deepseek-ai/dsh-experimental-webworker-runtime/client'
+    for (const name of ['webworker-packer', 'webworker-runtime']) {
+      assert.equal(import.meta.resolve('@deepseek-ai/dsh-experimental-' + name),
+        new URL('./node_modules/@deepseek-ai/dsh-experimental-' + name + '/lib/index.js', import.meta.url).href)
+    }
+    assert.equal(typeof client.connectWorkerHost, 'function')
+    const worker = fileURLToPath(import.meta.resolve('@deepseek-ai/dsh-experimental-webworker-runtime/worker'))
+    assert.ok(existsSync(worker))
+    assert.equal(readFileSync(worker, 'utf8').match(/^import[ \\t]/m), null)
+    const base = packer.packVfsImage({ config: '[]\\n', profile: 'packed-consumer', workspaces: new Map(), resolveFrom: process.cwd(), entries: [] })
+    assert.deepEqual(base.missing, [])
+    const vfs = runtime.loadVfsImage(await runtime.inflateImage(base.image, 'packed base'))
+    assert.ok(vfs.existsSync('/dsh/' + packer.MANIFEST_PATH))
+    mkdirSync('overlay')
+    writeFileSync('overlay/hello.txt', 'packed worker pair\\n')
+    const overlay = packer.packVfsOverlay([{ mount: 'workspace', directory: fileURLToPath(new URL('./overlay', import.meta.url)) }])
+    runtime.loadVfsOverlay(await runtime.inflateImage(overlay.image, 'packed overlay'), '/dsh', vfs)
+    assert.equal(vfs.readFileSync('/dsh/workspace/hello.txt', 'utf8'), 'packed worker pair\\n')
+    console.log('packed image and overlay mounted')
+  `
+  await writeFile(join(root, 'consumer.mjs'), script)
+  expect((await run(process.execPath, ['consumer.mjs'], root)).trim()).toBe('packed image and overlay mounted')
+})