fix(webworker): preserve bigint file handle identity

This commit is contained in:
imccyu committed 2026-09-09 18:21:16 +08:00
1 parent 0a9c774617
commit a7bd71e024
6 files changed
+60 -7

No files matched your search

+13 -2
View File
@@ -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<void> {
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<T> = { result: { ok: true; value: T } | { ok: false; error: { code: string; message: string } } }
interface PreviewTransport {
fetch(input: string, init: RequestInit): Promise<Response>
@@ -352,6 +355,9 @@ async function bootPreview(origin: string, browser: Browser): Promise<void> {
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<void> {
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)
@@ -399,7 +399,7 @@ export interface FileHandle {
writeFile(data: string | Uint8Array, encoding?: BufferEncoding): Promise<void>
write(data: string | Uint8Array): Promise<{ bytesWritten: number }>
read(buffer: Uint8Array, offset?: number, length?: number, position?: number | null): Promise<{ bytesRead: number; buffer: Uint8Array }>
stat(): Promise<VfsStats>
stat(options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats>
truncate(length?: number): Promise<void>
sync(): Promise<void>
datasync(): Promise<void>
@@ -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)
@@ -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<void> => {
current('ftruncate').truncate(length)
},
stat: async (): Promise<VfsStats> => current('fstat').stat(),
stat: async (options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats> => options?.bigint === true
? current('fstat').statBigInt()
: current('fstat').stat(),
sync: async (): Promise<void> => { current('fsync'); await this.flush() },
datasync: async (): Promise<void> => { current('fdatasync'); await this.flush() },
close: async (): Promise<void> => { 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<VfsFileHandle, 'stat' | 'sync' | 'datasync' | 'close'> {
return {
stat: async (): Promise<VfsStats> => this.plainStats(target),
stat: async (options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats> => this.statSync(target, options),
sync: async (): Promise<void> => { await this.flush() },
datasync: async (): Promise<void> => { await this.flush() },
close: async (): Promise<void> => {},
@@ -119,7 +119,7 @@ export interface VfsFileHandle {
writeFile(data: string | Uint8Array): Promise<void>
readFile(options?: VfsReadOptions): Promise<string | Uint8Array>
truncate(length?: number): Promise<void>
stat(): Promise<VfsStats>
stat(options?: VfsStatOptions): Promise<VfsStats | VfsBigIntStats>
sync(): Promise<void>
datasync(): Promise<void>
close(): Promise<void>
@@ -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
}
/**
@@ -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)
@@ -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({