test(fs): close abort/concurrency/observed coverage gaps + with-key e2e

Behavioral gaps from the coverage audit (line coverage was already 100%; these
close BEHAVIOR gaps):

- fs-local: service-level writeText/editText pre-abort → FS_ABORTED (file
  unchanged); concurrent guarded-write race and mixed write-vs-edit race (one
  wins, one FS_STALE_VERSION, locks released); edit→edit version refresh at the
  provider; the replaceIfVersion post-write version matches a fresh stat. fsio:
  a mid-stream abort → FS_ABORTED (previously only pre-abort was covered).
- fs-policy: the agent-without-session owner rung ({agent:{}} → no owner →
  createIfAbsent / FS_NOT_OBSERVED); fs/write-intent first-wins (symmetric to
  the existing edit-intent test).
- tool-fs: abort-through-the-tool for read/write/edit (isError FS_ABORTED, file
  unchanged); a deterministic tool-tier concurrent-edit race via a shared read;
  the throwing-fs/observed contract (a throwing listener surfaces as isError but
  the mutation already hit disk); the replace_all edit message; parseReadArgs
  rejects fractional/NaN offset and zero/negative limit.
- dsh-fs: FsError chains a cause through ErrorOptions.

New with-key e2e (packages/fs/tool-fs/tests/fs-tools.e2e.ts, self-skips without
DEEPSEEK_API_KEY): a real model drives the real read/write/edit tools to create
→ read → edit a file, verified on disk; a second test proves a relative path
resolves against the per-session cwd (factory meta.cwd) not config.cwd. Booted
via a plain tests/harness.ts. Added dsh-agent-loop + dsh-llm-deepseek devDeps.
This commit is contained in:
Tianyi Cui
2026-07-02 20:38:06 +08:00
parent 94cbec8162
commit b3f8b4c9c6
10 changed files with 348 additions and 0 deletions
@@ -196,6 +196,41 @@ describe('writeText', () => {
.rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' })
expect(lockCount(fs)).toBe(0)
})
it('replaceIfVersion returns the post-write version (matches a fresh stat)', async () => {
await writeFile(join(dir, 'a.txt'), 'v1')
const target = await fs.resolve('a.txt')
const before = await versionOf(target)
// Change the byte length so the mtimeMs:size token provably differs (a
// same-size same-tick rewrite can collide — the documented version-token
// limitation; not what this test is about).
const outcome = await fs.writeText(target, 'a much longer replacement body', { kind: 'replaceIfVersion', version: before })
expect(outcome.version).not.toBe(before)
expect(outcome.version).toBe(await versionOf(target))
})
it('honors a pre-aborted signal without creating the file', async () => {
const target = await fs.resolve('aborted.txt')
await expect(fs.writeText(target, 'x', undefined, AbortSignal.abort()))
.rejects.toMatchObject({ code: 'FS_ABORTED' })
await expect(stat(join(dir, 'aborted.txt'))).rejects.toMatchObject({ code: 'ENOENT' })
expect(lockCount(fs)).toBe(0)
})
it('two concurrent guarded writes: one updates, the other is rejected as stale', async () => {
await writeFile(join(dir, 'a.txt'), 'base')
const target = await fs.resolve('a.txt')
const version = await versionOf(target)
const results = await Promise.allSettled([
fs.writeText(target, 'one', { kind: 'replaceIfVersion', version }),
fs.writeText(target, 'two', { kind: 'replaceIfVersion', version }),
])
expect(results.filter(r => r.status === 'fulfilled')).toHaveLength(1)
const rejected = results.filter(r => r.status === 'rejected')
expect(rejected).toHaveLength(1)
expect((rejected[0] as PromiseRejectedResult).reason).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(lockCount(fs)).toBe(0)
})
})
describe('editText', () => {
@@ -297,6 +332,41 @@ describe('editText', () => {
expect((rejected[0] as PromiseRejectedResult).reason).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(lockCount(fs)).toBe(0)
})
it('honors a pre-aborted signal without rewriting the file', async () => {
await writeFile(join(dir, 'a.txt'), 'keep')
const target = await fs.resolve('a.txt')
await expect(fs.editText(target, { oldString: 'keep', newString: 'x', replaceAll: false }, undefined, AbortSignal.abort()))
.rejects.toMatchObject({ code: 'FS_ABORTED' })
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('keep')
expect(lockCount(fs)).toBe(0)
})
it('a successful edit refreshes the version so an immediate follow-up edit proceeds', async () => {
await writeFile(join(dir, 'a.txt'), 'one two')
const target = await fs.resolve('a.txt')
const first = await fs.editText(target, { oldString: 'one', newString: 'ONE', replaceAll: false }, { version: await versionOf(target) })
// The version the first edit returned is a valid guard for a second edit —
// no intervening re-stat needed.
const second = await fs.editText(target, { oldString: 'two', newString: 'TWO', replaceAll: false }, { version: first.version })
expect(second.replacements).toBe(1)
expect(await readFile(join(dir, 'a.txt'), 'utf8')).toBe('ONE TWO')
})
it('concurrent write vs edit at the same version: one wins, the other is stale', async () => {
await writeFile(join(dir, 'a.txt'), 'base')
const target = await fs.resolve('a.txt')
const version = await versionOf(target)
const results = await Promise.allSettled([
fs.writeText(target, 'written', { kind: 'replaceIfVersion', version }),
fs.editText(target, { oldString: 'base', newString: 'edited', replaceAll: false }, { version }),
])
expect(results.filter(r => r.status === 'fulfilled')).toHaveLength(1)
const rejected = results.filter(r => r.status === 'rejected')
expect(rejected).toHaveLength(1)
expect((rejected[0] as PromiseRejectedResult).reason).toMatchObject({ code: 'FS_STALE_VERSION' })
expect(lockCount(fs)).toBe(0)
})
})
describe('symlink targetKey identity', () => {