Files
deepseek-harness/packages/experimental/webworker-runtime/tests/node/fs.spec.ts
T
imccyu f47b1ecac2 feat(webworker): browser worker host runtime and the vfs image packer
Two private experimental packages run the whole harness tree inside one
dedicated Web Worker. dsh-experimental-webworker-runtime owns the in-memory
VFS (BigInt stats with per-path identity and strictly increasing mtimes),
the CommonJS wrapper loader over a lazily-evaluated builtin table whose
shims typecheck against Node's own module types, the postMessage tunnel
speaking plain HTTP, the AsyncLocalStorage runtime, and the worker
assembly. dsh-experimental-webworker-packer lowers every module body at
pack time against the shared wrapper contract, sweeps the profile closure
by static reachability, and writes a deterministically gzip-compressed tar
the worker inflates through the browser's native DecompressionStream while
it downloads.
2026-08-21 20:35:32 +08:00

204 lines
11 KiB
TypeScript

/**
* Behavioural check of this package's `node:fs` bridge over a real MemoryVfs:
* encoding branches, Dirent, file descriptors, FileHandle append/replace semantics,
* and Node's error codes.
*
* Migrated from apps/web-preview/scripts/checks/fs-check.ts.
*
* ONE module instance, and every import says so explicitly. The bridge reaches
* the VFS through a module-level slot (`setActiveVfs`/`requireActiveVfs`), so the
* harness that mounts the VFS and the bridge that reads it must be the same copy
* of `src/storage/memory.ts`. Two copies mean the mount lands in one and the bridge reports
* `no filesystem is mounted` from the other — the incident this check itself
* caused once, when it imported the built `lib/` while the bridge resolved to
* `src/`.
*
* Note the package-name subtlety that made that bug possible: the BARE specifier
* `@deepseek-ai/dsh-experimental-webworker-runtime` resolves to built `lib/index.js`, while
* `…/src/*` resolves to source. Under tsx those two happen to share a `vfs`
* instance today, so a mixed-path version of this file passes — for now. Pinning
* every import to `src/` removes the coincidence instead of depending on it.
*/
import { expect, test } from 'vitest'
import { MemoryVfs } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/memory.ts'
import { setActiveVfs } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/active.ts'
import * as fs from '@deepseek-ai/dsh-experimental-webworker-runtime/src/node/builtin_modules/implemented/fs.ts'
import * as fsp from '@deepseek-ai/dsh-experimental-webworker-runtime/src/node/builtin_modules/implemented/fs/promises.ts'
import type { VfsBigIntStats } from '@deepseek-ai/dsh-experimental-webworker-runtime/src/storage/types.ts'
const vfs = new MemoryVfs()
setActiveVfs(vfs)
// Precondition, not a behaviour: prove the bridge reads the VFS this file mounted.
// If these ever resolve to two module copies again, the write below would report
// `no filesystem is mounted` — but a bridge that answered from some OTHER mounted
// VFS would pass the whole suite while testing the wrong world, so the identity is
// asserted rather than inferred from things working. The probe creates its own
// directory: the suite's `/dsh` tree does not exist yet at this point.
fs.mkdirSync('/dsh/.probe', { recursive: true })
fs.writeFileSync('/dsh/.probe/instance', 'x')
if (!vfs.existsSync('/dsh/.probe/instance')) {
throw new Error('fs-check: the fs bridge is not reading the VFS this harness mounted '
+ '(two module instances — check that every import resolves through src/)')
}
vfs.rmSync('/dsh/.probe', { recursive: true })
const check = (label: string, actual: unknown, expected: unknown): void => {
const [seen, wanted] = [JSON.stringify(actual), JSON.stringify(expected)]
test(label, () => { expect(seen).toBe(wanted) })
}
const throws = (label: string, run: () => unknown, code: string): void => {
let outcome: string
try {
run()
outcome = 'did not throw'
} catch (error) {
outcome = (error as { code?: string }).code ?? (error as Error).message
}
test(label, () => { expect(outcome).toContain(code) })
}
fs.mkdirSync('/dsh/config', { recursive: true })
fs.writeFileSync('/dsh/config/cordis.yml', '- id: timer\n')
check('readFileSync utf8', fs.readFileSync('/dsh/config/cordis.yml', 'utf8'), '- id: timer\n')
check('readFileSync options object', fs.readFileSync('/dsh/config/cordis.yml', { encoding: 'utf8' }), '- id: timer\n')
check('readFileSync bytes length', (fs.readFileSync('/dsh/config/cordis.yml') as Uint8Array).byteLength, 12)
check('readFileSync is Buffer', Buffer.isBuffer(fs.readFileSync('/dsh/config/cordis.yml')), true)
check('existsSync true', fs.existsSync('/dsh/config/cordis.yml'), true)
check('existsSync false', fs.existsSync('/dsh/nope'), false)
check('statSync isFile', fs.statSync('/dsh/config/cordis.yml').isFile(), true)
check('statSync size', fs.statSync('/dsh/config/cordis.yml').size, 12)
check('statSync dir', fs.statSync('/dsh/config').isDirectory(), true)
check('realpathSync', fs.realpathSync('/dsh/config/../config/cordis.yml'), '/dsh/config/cordis.yml')
fs.appendFileSync('/dsh/config/cordis.yml', '- id: llm\n')
check('appendFileSync', fs.readFileSync('/dsh/config/cordis.yml', 'utf8'), '- id: timer\n- id: llm\n')
fs.mkdirSync('/dsh/config/agent-presets/standard', { recursive: true })
fs.writeFileSync('/dsh/config/agent-presets/standard/SKILL.md', '# skill\n')
check('readdirSync names', fs.readdirSync('/dsh/config'), ['agent-presets', 'cordis.yml'])
const entries = fs.readdirSync('/dsh/config', { withFileTypes: true }) as fs.Dirent[]
check('readdirSync withFileTypes', entries.map(entry => [entry.name, entry.isFile(), entry.isDirectory()]), [
['agent-presets', false, true],
['cordis.yml', true, false],
])
check('Dirent parentPath', entries[0]!.parentPath, '/dsh/config')
const temporary = fs.mkdtempSync('/dsh/tmp/run-')
check('mkdtempSync creates directory', fs.statSync(temporary).isDirectory(), true)
check('mkdtempSync unique', fs.mkdtempSync('/dsh/tmp/run-') === temporary, false)
throws('readFileSync missing', () => fs.readFileSync('/dsh/missing'), 'ENOENT')
throws('statSync missing', () => fs.statSync('/dsh/missing'), 'ENOENT')
throws('accessSync missing', () =>{ fs.accessSync('/dsh/missing') }, 'ENOENT')
throws('readdirSync missing', () => fs.readdirSync('/dsh/missing'), 'ENOENT')
throws('watchFile is loud', () => fs.watchFile('/dsh/config/cordis.yml'), 'not implemented')
throws('createReadStream is loud', () => fs.createReadStream('/dsh/config/cordis.yml'), 'not implemented')
const appendFd = fs.openSync('/dsh/log.jsonl', 'a')
fs.writeSync(appendFd, '{"a":1}\n')
fs.writeSync(appendFd, '{"a":2}\n')
fs.closeSync(appendFd)
check('append fd writes', fs.readFileSync('/dsh/log.jsonl', 'utf8'), '{"a":1}\n{"a":2}\n')
const readFd = fs.openSync('/dsh/log.jsonl', 'r')
const target = new Uint8Array(8)
check('readSync count', fs.readSync(readFd, target, 0, 8), 8)
check('readSync bytes', new TextDecoder().decode(target), '{"a":1}\n')
check('readSync continues', fs.readSync(readFd, target, 0, 8), 8)
check('readSync second line', new TextDecoder().decode(target), '{"a":2}\n')
check('readSync at eof', fs.readSync(readFd, target, 0, 8), 0)
fs.closeSync(readFd)
throws('closed fd', () => fs.readSync(readFd, target, 0, 8), 'EBADF')
const writeFd = fs.openSync('/dsh/truncated.txt', 'w')
fs.writeSync(writeFd, 'abc')
fs.closeSync(writeFd)
check('write fd truncates', fs.readFileSync('/dsh/truncated.txt', 'utf8'), 'abc')
fs.renameSync('/dsh/truncated.txt', '/dsh/renamed.txt')
check('renameSync moves', [fs.existsSync('/dsh/truncated.txt'), fs.readFileSync('/dsh/renamed.txt', 'utf8')], [false, 'abc'])
fs.rmSync('/dsh/renamed.txt')
check('rmSync removes', fs.existsSync('/dsh/renamed.txt'), false)
// A FileHandle opened for appending must append, not replace: the JSONL session
// log writes its header frame first and every batch after it through this path.
fs.writeFileSync('/dsh/log-handle.jsonl', 'header\n')
const appendHandle = await fsp.open('/dsh/log-handle.jsonl', 'a')
check('append handle sees the existing size', (await appendHandle.stat()).size, 7)
await appendHandle.writeFile('batch-1\n')
await appendHandle.sync()
await appendHandle.close()
const secondHandle = await fsp.open('/dsh/log-handle.jsonl', 'a')
await secondHandle.writeFile('batch-2\n')
await secondHandle.close()
check('handle.writeFile appends in append mode', fs.readFileSync('/dsh/log-handle.jsonl', 'utf8'), 'header\nbatch-1\nbatch-2\n')
const replaceHandle = await fsp.open('/dsh/log-handle.jsonl', 'w')
await replaceHandle.writeFile('replaced\n')
await replaceHandle.close()
check('handle.writeFile replaces without append mode', fs.readFileSync('/dsh/log-handle.jsonl', 'utf8'), 'replaced\n')
const truncHandle = await fsp.open('/dsh/log-handle.jsonl', 'r+')
await truncHandle.truncate(4)
await truncHandle.close()
check('handle.truncate cuts the tail', fs.readFileSync('/dsh/log-handle.jsonl', 'utf8'), 'repl')
check('promises.readFile', await fsp.readFile('/dsh/config/cordis.yml', 'utf8'), '- id: timer\n- id: llm\n')
await fsp.writeFile('/dsh/promise.txt', 'p')
check('promises.writeFile', fs.readFileSync('/dsh/promise.txt', 'utf8'), 'p')
check('promises.stat', (await fsp.stat('/dsh/promise.txt')).isFile(), true)
await fsp.cp('/dsh/config', '/dsh/config-copy')
check('promises.cp tree', await fsp.readFile('/dsh/config-copy/agent-presets/standard/SKILL.md', 'utf8'), '# skill\n')
await fsp.rm('/dsh/config-copy', { recursive: true })
check('promises.rm recursive', fs.existsSync('/dsh/config-copy'), false)
// ---------------------------------------------------------------------------
// The `{ bigint: true }` stats the filesystem service reads.
//
// `dsh-fs-local` stats EVERY target this way before it lists or reads: it masks
// `mode` with a BigInt literal and builds its version token from
// `dev:ino:size:mtimeNs:ctimeNs`. A number-valued `mode` here made that mask
// throw `Cannot mix BigInt and other types`, which the service reported as
// FS_IO_ERROR and skill discovery swallowed as "empty directory" — the worker
// booted with an empty skill catalog and no error anywhere.
// ---------------------------------------------------------------------------
const bigStats = (path: string): VfsBigIntStats => fs.statSync(path, { bigint: true }) as VfsBigIntStats
fs.writeFileSync('/dsh/versioned.txt', 'one')
{
const stats = bigStats('/dsh/versioned.txt')
check('bigint stat reports mode as a BigInt', typeof stats.mode, 'bigint')
check('bigint mode masks to an owner-only file permission', Number(stats.mode & 0o777n), 0o600)
check('bigint stat reports the identity fields the version token needs', [
typeof stats.dev, typeof stats.ino, typeof stats.size, typeof stats.mtimeNs, typeof stats.ctimeNs,
], ['bigint', 'bigint', 'bigint', 'bigint', 'bigint'])
check('bigint nanosecond time scales the millisecond time', stats.mtimeNs === stats.mtimeMs * 1_000_000n, true)
check('bigint stat still answers the type predicates', [stats.isFile(), stats.isDirectory()], [true, false])
check('plain stat keeps its number shape', typeof fs.statSync('/dsh/versioned.txt').mode, 'number')
}
{
// Two writes inside one millisecond must not produce one version: the service's
// stale-write guard compares these tokens.
const token = (path: string): string => {
const stats = bigStats(path)
return `${stats.dev}:${stats.ino}:${stats.size}:${stats.mtimeNs}:${stats.ctimeNs}`
}
const before = token('/dsh/versioned.txt')
fs.writeFileSync('/dsh/versioned.txt', 'two')
check('a rewrite changes the version token', token('/dsh/versioned.txt') !== before, true)
check('an unchanged file keeps its version token', token('/dsh/versioned.txt'), token('/dsh/versioned.txt'))
}
{
const first = bigStats('/dsh/versioned.txt').ino
fs.writeFileSync('/dsh/versioned.txt', 'three')
check('identity survives a write to the same path', String(bigStats('/dsh/versioned.txt').ino), String(first))
fs.rmSync('/dsh/versioned.txt')
fs.writeFileSync('/dsh/versioned.txt', 'four')
check('a removed and recreated path reports a new identity', bigStats('/dsh/versioned.txt').ino !== first, true)
}
check('a directory reports owner-only directory mode in the bigint shape', Number(bigStats('/dsh/config').mode & 0o777n), 0o700)
check('promises.stat forwards the bigint option', typeof (await fsp.stat('/dsh/config', { bigint: true })).mode, 'bigint')