Merge remote-tracking branch 'origin/master' into feat/read-image-context

# Conflicts:
#	docs/config-catalog.i18n.yaml
#	docs/config-catalog.md
#	docs/config-catalog.zh.md
#	packages/support/llm-replay/src/index.ts
#	packages/support/llm-replay/tests/llm-replay.spec.ts
This commit is contained in:
creatixchu
2026-08-10 20:25:25 +08:00
43 changed files with 1032 additions and 130 deletions

View File

@@ -7,6 +7,7 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { constants as bufferConstants } from 'node:buffer'
import { mkdir, mkdtemp, readFile, realpath, rm, stat, symlink, unlink, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -43,13 +44,39 @@ async function versionOf(target: FsTarget): Promise<FsVersion> {
return info.version
}
async function remountWithDiffLimit(diffBasisMaxBytes: number): Promise<void> {
await fiber.dispose()
fiber = await ctx.plugin(LocalFileSystem, { cwd: dir, diffBasisMaxBytes })
fs = ctx.fs as LocalFileSystem
}
describe('registration', () => {
it('registers LocalFileSystem as ctx.fs with a default cwd', async () => {
const bare = new Context()
const bareFiber = await bare.plugin(LocalFileSystem)
expect((bare.fs as LocalFileSystem).config.cwd).toBe(process.cwd())
expect((bare.fs as LocalFileSystem).config.diffBasisMaxBytes).toBe(10 * 1024 * 1024)
await bareFiber.dispose()
})
it('rejects non-positive, fractional, unsafe, or unallocatable diff-basis limits', async () => {
const maxDiffBasisBytes = Math.min(
bufferConstants.MAX_LENGTH,
bufferConstants.MAX_STRING_LENGTH,
)
const valid = new Context()
const validFiber = await valid.plugin(LocalFileSystem, { diffBasisMaxBytes: maxDiffBasisBytes })
expect((valid.fs as LocalFileSystem).config.diffBasisMaxBytes).toBe(maxDiffBasisBytes)
await validFiber.dispose()
for (const diffBasisMaxBytes of [0, -1, 1.5, maxDiffBasisBytes + 1, Number.MAX_SAFE_INTEGER + 1]) {
const invalid = new Context()
await expect(invalid.plugin(LocalFileSystem, { diffBasisMaxBytes })).rejects.toThrow(
`fs-local: diffBasisMaxBytes must be a positive safe integer no greater than ${maxDiffBasisBytes}`,
)
await invalid.fiber.dispose()
}
})
})
describe('resolve', () => {
@@ -481,6 +508,54 @@ describe('writeText', () => {
expect(outcome.after).toBe('now valid')
})
it('an overwrite of a prior file AT the whole-file bound reports before:null (undiffable), still succeeds', async () => {
// The configured bound keeps the fixture small; 8 bytes at a bound of 8
// pins the exclusive edge without coupling this provider to a read tool.
await remountWithDiffLimit(8)
await writeFile(join(dir, 'big.txt'), '12345678')
const target = await fs.resolve('big.txt')
const outcome = await fs.writeText(target, 'tiny')
expect(outcome.operation).toBe('update')
expect(outcome.before).toBeNull()
expect(outcome.after).toBe('tiny')
})
it('an overwrite whose NEW content is at the whole-file bound reports before:null (no huge contextual diff)', async () => {
// The bound gates BOTH sides of the diff pair: a small prior file rewritten
// with at/above-bound content yields no contextual-hunk basis either, since
// a small-to-huge rewrite's hunk is as large as the new content — the
// consumer must fall back to the whole-file diff card, exactly like a
// create of the same size.
await remountWithDiffLimit(8)
await writeFile(join(dir, 'grow.txt'), 'tiny')
const target = await fs.resolve('grow.txt')
const outcome = await fs.writeText(target, '12345678')
expect(outcome.operation).toBe('update')
expect(outcome.before).toBeNull()
expect(outcome.after).toBe('12345678')
})
it('gates the NEW content by UTF-8 byte length, not character count', async () => {
// Three CJK characters are 9 UTF-8 bytes: below an 8-byte bound by
// characters but at/above it by bytes, so the basis must be declined.
await remountWithDiffLimit(8)
await writeFile(join(dir, 'cjk.txt'), 'tiny')
const target = await fs.resolve('cjk.txt')
const outcome = await fs.writeText(target, '你好吗')
expect(outcome.operation).toBe('update')
expect(outcome.before).toBeNull()
expect(outcome.after).toBe('你好吗')
})
it('an overwrite with BOTH sides below the whole-file bound keeps its contextual before basis', async () => {
await remountWithDiffLimit(8)
await writeFile(join(dir, 'small.txt'), '1234567')
const target = await fs.resolve('small.txt')
const outcome = await fs.writeText(target, 'new')
expect(outcome.before).toBe('1234567')
expect(outcome.after).toBe('new')
})
it('releases per-target mutation locks after success and failure', async () => {
const target = await fs.resolve('a.txt')
await fs.writeText(target, 'created', { kind: 'createIfAbsent' })

View File

@@ -5,7 +5,7 @@
* policy and lives in `dsh-fs-policy`, so it is not tested here.
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { chmod, mkdtemp, readFile, rename, rm, stat, symlink, unlink, writeFile, mkdir, readdir, realpath } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
@@ -16,6 +16,7 @@ import {
probe,
probeNoFollow,
readForEdit,
readTextForDiff,
readWholeText,
resolveLocalTarget,
restoreLineEndings,
@@ -316,6 +317,261 @@ describe('readWholeText', () => {
})
})
describe('readTextForDiff', () => {
it('returns normalized text only when the opened file is strictly below the limit', async () => {
const file = join(dir, 'basis.txt')
await writeFile(file, 'a\r\nb')
expect(await readTextForDiff(file, 5)).toBe('a\nb')
expect(await readTextForDiff(file, 4)).toBeNull()
})
it('bounds the actual opened file rather than trusting an earlier path size', async () => {
const file = join(dir, 'replaced.txt')
await writeFile(file, 'tiny')
const earlierSize = (await stat(file)).size
await writeFile(file, '123456789')
expect(earlierSize).toBeLessThan(8)
expect(await readTextForDiff(file, 8)).toBeNull()
})
it('returns null when the opened file shrinks after descriptor stat', async () => {
const file = join(dir, 'shrinking.txt')
await writeFile(file, 'abcdef')
vi.resetModules()
vi.doMock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open(...args: Parameters<typeof actual.open>) {
const handle = await actual.open(...args)
return {
close: handle.close.bind(handle),
read: handle.read.bind(handle),
async stat(...statArgs: Parameters<typeof handle.stat>) {
const info = await handle.stat(...statArgs)
await writeFile(file, 'abc')
return info
},
}
},
}
})
try {
const { readTextForDiff: isolatedReadTextForDiff } = await import('../src/fsio.ts')
expect(await isolatedReadTextForDiff(file, 8)).toBeNull()
} finally {
vi.doUnmock('node:fs/promises')
vi.resetModules()
}
})
it('returns null when the opened file grows after descriptor stat', async () => {
const file = join(dir, 'growing.txt')
await writeFile(file, 'abcdef')
vi.resetModules()
vi.doMock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open(...args: Parameters<typeof actual.open>) {
const handle = await actual.open(...args)
return {
close: handle.close.bind(handle),
read: handle.read.bind(handle),
async stat(...statArgs: Parameters<typeof handle.stat>) {
const info = await handle.stat(...statArgs)
await writeFile(file, 'abcdef-grown')
return info
},
}
},
}
})
try {
const { readTextForDiff: isolatedReadTextForDiff } = await import('../src/fsio.ts')
expect(await isolatedReadTextForDiff(file, 32)).toBeNull()
} finally {
vi.doUnmock('node:fs/promises')
vi.resetModules()
}
})
it('returns null when the file vanishes before the basis open (deletion race)', async () => {
expect(await readTextForDiff(join(dir, 'deleted-after-preflight.txt'), 32)).toBeNull()
})
it('returns null when the opened descriptor is no longer a regular file', async () => {
const file = join(dir, 'swapped.txt')
await writeFile(file, 'abcdef')
vi.resetModules()
vi.doMock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open(...args: Parameters<typeof actual.open>) {
const handle = await actual.open(...args)
return {
close: handle.close.bind(handle),
read: handle.read.bind(handle),
async stat(...statArgs: Parameters<typeof handle.stat>) {
const info = await handle.stat(...statArgs)
return Object.assign(info, { isFile: () => false })
},
}
},
}
})
try {
const { readTextForDiff: isolatedReadTextForDiff } = await import('../src/fsio.ts')
expect(await isolatedReadTextForDiff(file, 32)).toBeNull()
} finally {
vi.doUnmock('node:fs/promises')
vi.resetModules()
}
})
it('propagates a non-errno fault instead of masking it as a null basis', async () => {
const file = join(dir, 'faulted.txt')
await writeFile(file, 'abcdef')
vi.resetModules()
vi.doMock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open() {
throw new TypeError('forged programming fault')
},
}
})
try {
const { readTextForDiff: isolatedReadTextForDiff } = await import('../src/fsio.ts')
await expect(isolatedReadTextForDiff(file, 32)).rejects.toThrow('forged programming fault')
} finally {
vi.doUnmock('node:fs/promises')
vi.resetModules()
}
})
it('returns null for binary and invalid UTF-8 without blocking the caller write', async () => {
await writeFile(join(dir, 'bin'), Buffer.from([0x68, 0x00, 0x69]))
await writeFile(join(dir, 'bad'), Buffer.from([0x68, 0xff, 0x69]))
expect(await readTextForDiff(join(dir, 'bin'), 8)).toBeNull()
expect(await readTextForDiff(join(dir, 'bad'), 8)).toBeNull()
})
it('honors a pre-aborted signal', async () => {
const file = join(dir, 'basis.txt')
await writeFile(file, 'text')
await expect(readTextForDiff(file, 8, AbortSignal.abort())).rejects.toMatchObject({ code: 'FS_ABORTED' })
})
it.each(['open', 'stat'] as const)('observes cancellation immediately after %s', async (stage) => {
const file = join(dir, 'basis.txt')
await writeFile(file, 'text')
const reached = Promise.withResolvers<undefined>()
const release = Promise.withResolvers<undefined>()
let statCalls = 0
const allocate = vi.spyOn(Buffer, 'allocUnsafe')
vi.resetModules()
vi.doMock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open(...args: Parameters<typeof actual.open>) {
const handle = await actual.open(...args)
if (stage === 'open') {
reached.resolve(undefined)
await release.promise
}
return {
close: handle.close.bind(handle),
read: handle.read.bind(handle),
async stat(...statArgs: Parameters<typeof handle.stat>) {
statCalls += 1
const info = await handle.stat(...statArgs)
if (stage === 'stat') {
reached.resolve(undefined)
await release.promise
}
return info
},
}
},
}
})
try {
const { readTextForDiff: isolatedReadTextForDiff } = await import('../src/fsio.ts')
const controller = new AbortController()
const pending = isolatedReadTextForDiff(file, 8, controller.signal)
await reached.promise
const allocationCalls = allocate.mock.calls.length
controller.abort()
release.resolve(undefined)
await expect(pending).rejects.toMatchObject({ code: 'FS_ABORTED' })
expect(statCalls).toBe(stage === 'open' ? 0 : 1)
expect(allocate).toHaveBeenCalledTimes(allocationCalls)
} finally {
release.resolve(undefined)
allocate.mockRestore()
vi.doUnmock('node:fs/promises')
vi.resetModules()
}
})
it('bounds descriptor reads and observes cancellation before the next chunk', async () => {
const file = join(dir, 'large-basis.txt')
const fileBytes = 200 * 1024
await writeFile(file, 'x'.repeat(fileBytes))
const firstRead = Promise.withResolvers<undefined>()
const releaseFirstRead = Promise.withResolvers<undefined>()
const readLengths: number[] = []
vi.resetModules()
vi.doMock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:fs/promises')>()
return {
...actual,
async open(...args: Parameters<typeof actual.open>) {
const handle = await actual.open(...args)
return {
stat: handle.stat.bind(handle),
close: handle.close.bind(handle),
async read(buffer: Buffer, offset: number, length: number, position: number | null) {
readLengths.push(length)
const result = await handle.read(buffer, offset, length, position)
if (readLengths.length === 1) {
firstRead.resolve(undefined)
await releaseFirstRead.promise
}
return result
},
}
},
}
})
try {
const { readTextForDiff: isolatedReadTextForDiff } = await import('../src/fsio.ts')
const controller = new AbortController()
const pending = isolatedReadTextForDiff(file, fileBytes + 1, controller.signal)
await firstRead.promise
expect(readLengths).toEqual([64 * 1024])
controller.abort()
releaseFirstRead.resolve(undefined)
await expect(pending).rejects.toMatchObject({ code: 'FS_ABORTED' })
expect(readLengths).toHaveLength(1)
} finally {
releaseFirstRead.resolve(undefined)
vi.doUnmock('node:fs/promises')
vi.resetModules()
}
})
})
describe('streamWholeText', () => {
it('streams the whole file as decoded text', async () => {
const file = join(dir, 'a.txt')