fix(webworker): preserve bigint file handle identity
This commit is contained in:
1 parent
0a9c774617
commit
a7bd71e024
6 files changed
+60
-7
No files matched your search
@@ -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({
|
||||
|
||||
Reference in new issue
Block a user