fix(fs): validate read meta semantics and sync public result-view docs

readMetaFromMeta narrows the opaque persisted meta boundary, so beyond
shape it now rejects replayed JSON that is well-typed but semantically
invalid: line numbers must be 1-based integers, totalLines a non-negative
integer, and line numbers must strictly increase without exceeding
totalLines. Any violation declines to the generic fallback.

Sync the public ToolResultView contract across the core/tools and
tool-fs READMEs and docs/core-data-structures/tools for the fourth
result-view member and the ReadFileLine vocabulary, and expand the
Agent Note Testing section with the new rejection paths and the snapshot
evidence this PR carries.
This commit is contained in:
Chinesezjc
2026-07-30 19:42:25 +08:00
parent 1d0e6eea32
commit 26488d6e82
15 changed files with 61 additions and 23 deletions

View File

@@ -166,4 +166,27 @@ describe('readMetaFromMeta', () => {
expect(readMetaFromMeta({ ...good, lines: [null] })).toBeUndefined()
expect(readMetaFromMeta({ ...good, lang: 5 })).toBeUndefined()
})
it('rejects a line number that is not a 1-based integer', () => {
expect(readMetaFromMeta({ ...good, lines: [{ number: 0, text: 'x' }], totalLines: 1 })).toBeUndefined()
expect(readMetaFromMeta({ ...good, lines: [{ number: 1.5, text: 'x' }], totalLines: 2 })).toBeUndefined()
expect(readMetaFromMeta({ ...good, lines: [{ number: NaN, text: 'x' }], totalLines: 1 })).toBeUndefined()
expect(readMetaFromMeta({ ...good, lines: [{ number: Infinity, text: 'x' }], totalLines: 1 })).toBeUndefined()
})
it('rejects a totalLines that is not a non-negative integer', () => {
expect(readMetaFromMeta({ ...good, totalLines: -1 })).toBeUndefined()
expect(readMetaFromMeta({ ...good, totalLines: 1.5 })).toBeUndefined()
expect(readMetaFromMeta({ ...good, totalLines: NaN })).toBeUndefined()
})
it('rejects lines that do not strictly increase or exceed totalLines', () => {
const twoLines = { path: '/abs/a', lang: 'ts' }
// Duplicate line numbers.
expect(readMetaFromMeta({ ...twoLines, lines: [{ number: 1, text: 'a' }, { number: 1, text: 'b' }], totalLines: 2 })).toBeUndefined()
// Out-of-order line numbers.
expect(readMetaFromMeta({ ...twoLines, lines: [{ number: 2, text: 'b' }, { number: 1, text: 'a' }], totalLines: 2 })).toBeUndefined()
// A line number past totalLines.
expect(readMetaFromMeta({ ...twoLines, lines: [{ number: 3, text: 'c' }], totalLines: 2 })).toBeUndefined()
})
})

View File

@@ -351,7 +351,6 @@ describe('read tool', () => {
if (result.isError) throw new Error('expected read success')
expect(result.meta).toEqual({ path: '/abs/notes', lines: [{ number: 1, text: 'plain' }], totalLines: 1 })
})
})
describe('formatReadOutput footer variants', () => {