From a7bd71e0245d64a4603e6d71fd08a73e0db23ab2 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:26:00 +0800 Subject: [PATCH] fix(webworker): preserve bigint file handle identity --- apps/web/tests/preview-boot.e2e.ts | 15 +++++++++++++-- .../node/builtin_modules/implemented/fs.ts | 8 ++++++-- .../webworker-runtime/src/storage/memory.ts | 19 +++++++++++++++++-- .../webworker-runtime/src/storage/types.ts | 7 ++++++- .../webworker-runtime/tests/node/fs.spec.ts | 7 +++++++ .../tests/storage/memory-vfs.spec.ts | 11 +++++++++++ 6 files changed, 60 insertions(+), 7 deletions(-) diff --git a/apps/web/tests/preview-boot.e2e.ts b/apps/web/tests/preview-boot.e2e.ts index 7422f902e1..ca04c720c2 100644 --- a/apps/web/tests/preview-boot.e2e.ts +++ b/apps/web/tests/preview-boot.e2e.ts @@ -34,7 +34,10 @@ import { IMAGE_FILE_NAME, PREVIEW_FIXTURE_MANIFEST_FILE, PREVIEW_FIXTURE_MANIFEST_VERSION, type PreviewFixtureManifest, } from '@deepseek-ai/dsh-experimental-webworker-runtime' -import { buildVfsExampleFiles } from '../../../packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts' +import { + VFS_EXAMPLE_SESSION_IDS, + buildVfsExampleFiles, +} from '../../../packages/experimental/webworker-runtime/tests/vfs-example-fixture.ts' import { captureStableAria, compareOrRefreshGolden, webSnapshotMode } from './scaffold.ts' import { newEnglishPage, REPO_ROOT, saveFailureShot } from './support.ts' @@ -317,7 +320,7 @@ async function bootPreview(origin: string, browser: Browser): Promise { await page.locator('[data-composer-input][data-placeholder="Describe what you want to build, / commands, @ files or sessions"]') .waitFor({ timeout: 30_000 }) - const exercised = await page.evaluate(async () => { + const exercised = await page.evaluate(async ({ seededSessionId, seededSessionTitle }) => { type Result = { result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } } } interface PreviewTransport { fetch(input: string, init: RequestInit): Promise @@ -352,6 +355,9 @@ async function bootPreview(origin: string, browser: Browser): Promise { if (!body.result.ok) throw new Error(`${endpoint} failed: ${body.result.error.message}`) return body.result.value } + const renamed = await remote<{ title: string }>('session/rename', { + request: { sessionId: seededSessionId, title: seededSessionTitle }, + }) const skills = await remote<{ skills: Array<{ name: string }> }>( 'skills/list', { request: { sessionId } }, ) @@ -384,10 +390,15 @@ async function bootPreview(origin: string, browser: Browser): Promise { await remote('credentials/unset', { ref: 'PREVIEW_TEST_SECRET' }) await new Promise((resolve) => { setTimeout(resolve, 250) }) return { + renamedTitle: renamed.title, skillCount: skills.skills.length, credentialConfigured: credentials.PREVIEW_TEST_SECRET?.configured, } + }, { + seededSessionId: VFS_EXAMPLE_SESSION_IDS.main, + seededSessionTitle: SHOWCASE_TITLE, }) + expect(exercised.renamedTitle).toBe(SHOWCASE_TITLE) expect(exercised.skillCount).toBeGreaterThan(0) expect(exercised.credentialConfigured).toBe(true) diff --git a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts index b2f4462a50..66676be09e 100644 --- a/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts +++ b/packages/experimental/webworker-runtime/src/node/builtin_modules/implemented/fs.ts @@ -399,7 +399,7 @@ export interface FileHandle { writeFile(data: string | Uint8Array, encoding?: BufferEncoding): Promise write(data: string | Uint8Array): Promise<{ bytesWritten: number }> read(buffer: Uint8Array, offset?: number, length?: number, position?: number | null): Promise<{ bytesRead: number; buffer: Uint8Array }> - stat(): Promise + stat(options?: VfsStatOptions): Promise truncate(length?: number): Promise sync(): Promise datasync(): Promise @@ -441,7 +441,11 @@ export function openHandleSync(path: PathArg, flags = 'r', mode?: number): FileH bytesRead: readSync(fd, buffer, offset, length, position), buffer, }), - stat: async () => directory ? statSync(target) as VfsStats : descriptor('fstat').file.stat(), + stat: async (options?: VfsStatOptions) => directory + ? statSync(target, options) + : options?.bigint === true + ? descriptor('fstat').file.statBigInt() + : descriptor('fstat').file.stat(), truncate: async (length = 0) => { if (directory) writeFileSync(target, new Uint8Array(length)) else descriptor('ftruncate').file.truncate(length) diff --git a/packages/experimental/webworker-runtime/src/storage/memory.ts b/packages/experimental/webworker-runtime/src/storage/memory.ts index 24d12a688e..3a14c62b49 100644 --- a/packages/experimental/webworker-runtime/src/storage/memory.ts +++ b/packages/experimental/webworker-runtime/src/storage/memory.ts @@ -431,6 +431,18 @@ export class MemoryVfs implements Vfs { return statsOf(node.bytes.length, node.mtimeMs, false, this.identityOfFile(node), node.mode) } + /** @returns BigInt stats for an open file, including its device and inode identity. */ + private fileBigIntStats(node: FileNode): VfsBigIntStats { + return bigIntStatsOf( + node.bytes.length, + node.mtimeMs, + false, + this.identityOfFile(node), + node.mode, + this.fileLinkCount(node), + ) + } + /** Forget removed directory identities, so recreated paths report new ones. */ private forgetIdentity(target: string): void { this.identities.delete(target) @@ -647,7 +659,9 @@ export class MemoryVfs implements Vfs { truncate: async (length = 0): Promise => { current('ftruncate').truncate(length) }, - stat: async (): Promise => current('fstat').stat(), + stat: async (options?: VfsStatOptions): Promise => options?.bigint === true + ? current('fstat').statBigInt() + : current('fstat').stat(), sync: async (): Promise => { current('fsync'); await this.flush() }, datasync: async (): Promise => { current('fdatasync'); await this.flush() }, close: async (): Promise => { closed = true }, @@ -692,6 +706,7 @@ export class MemoryVfs implements Vfs { this.truncateFile(node, length) }, stat: () => this.fileStats(node), + statBigInt: () => this.fileBigIntStats(node), } } @@ -704,7 +719,7 @@ export class MemoryVfs implements Vfs { */ private handleTail(target: string): Pick { return { - stat: async (): Promise => this.plainStats(target), + stat: async (options?: VfsStatOptions): Promise => this.statSync(target, options), sync: async (): Promise => { await this.flush() }, datasync: async (): Promise => { await this.flush() }, close: async (): Promise => {}, diff --git a/packages/experimental/webworker-runtime/src/storage/types.ts b/packages/experimental/webworker-runtime/src/storage/types.ts index f1482e07d6..616d24cf27 100644 --- a/packages/experimental/webworker-runtime/src/storage/types.ts +++ b/packages/experimental/webworker-runtime/src/storage/types.ts @@ -119,7 +119,7 @@ export interface VfsFileHandle { writeFile(data: string | Uint8Array): Promise readFile(options?: VfsReadOptions): Promise truncate(length?: number): Promise - stat(): Promise + stat(options?: VfsStatOptions): Promise sync(): Promise datasync(): Promise close(): Promise @@ -157,6 +157,11 @@ export interface VfsOpenFile { * @returns Current file metadata, including after rename or unlink. */ stat(): VfsStats + /** + * Read BigInt metadata from the opened file identity. + * @returns Current file metadata, including device and inode identity. + */ + statBigInt(): VfsBigIntStats } /** diff --git a/packages/experimental/webworker-runtime/tests/node/fs.spec.ts b/packages/experimental/webworker-runtime/tests/node/fs.spec.ts index ce9e6fd916..cb30f33dc2 100644 --- a/packages/experimental/webworker-runtime/tests/node/fs.spec.ts +++ b/packages/experimental/webworker-runtime/tests/node/fs.spec.ts @@ -115,6 +115,13 @@ check('rmSync removes', fs.existsSync('/dsh/renamed.txt'), false) 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) +const appendHandleStats = await appendHandle.stat({ bigint: true }) as VfsBigIntStats +const appendPathStats = await fsp.stat('/dsh/log-handle.jsonl', { bigint: true }) as VfsBigIntStats +check('bigint handle stat matches the path identity', [ + typeof appendHandleStats.ino, + appendHandleStats.ino === appendPathStats.ino, + appendHandleStats.dev === appendPathStats.dev, +], ['bigint', true, true]) await appendHandle.writeFile('batch-1\n') await appendHandle.sync() check('handle.sync flushes the active VFS', flushes, 1) diff --git a/packages/experimental/webworker-runtime/tests/storage/memory-vfs.spec.ts b/packages/experimental/webworker-runtime/tests/storage/memory-vfs.spec.ts index b80987cd4c..521342f19d 100644 --- a/packages/experimental/webworker-runtime/tests/storage/memory-vfs.spec.ts +++ b/packages/experimental/webworker-runtime/tests/storage/memory-vfs.spec.ts @@ -203,6 +203,17 @@ describe('mutation publication', () => { expect(new TextDecoder().decode(descriptor.read(0, descriptor.stat().size))).toBe('detached') }) + it('reports the path identity through a BigInt file handle stat', async () => { + const vfs = new MemoryVfs() + vfs.seed('/dsh/session.lock', '') + const handle = vfs.open('/dsh/session.lock', 'r') + const held = await handle.stat({ bigint: true }) as VfsBigIntStats + const current = vfs.statSync('/dsh/session.lock', { bigint: true }) as VfsBigIntStats + + expect([held.dev, held.ino]).toEqual([current.dev, current.ino]) + await handle.close() + }) + it('decomposes a directory rename into replayable destination state', () => { const recorded: VfsMutation[] = [] const vfs = new MemoryVfs({