fix(fs): persist read window offset in the read card meta
An empty read window (byte cap below the first selected line: `lines: []` with `totalLines > 0`) dropped `offset` from the persisted presentation meta, so a replayed read card could not report where the window starts or where a continuation resumes. Carry `offset` on `FsReadMeta`, `ReadResultView`, and the `presentationMeta` projection, and validate it in `readMetaFromMeta` (1-based integer; the first line number may not fall below it). Re-record the ACP fixtures and the cordis api catalog. Also correct the Note's `parallel-file-reads` golden path (examples/tui-agent -> apps/cli) and record the pre-card replay-degradation tradeoff in the Decision section.
This commit is contained in:
@@ -151,14 +151,19 @@ describe('langFromPath', () => {
|
||||
})
|
||||
|
||||
describe('readMetaFromMeta', () => {
|
||||
const good = { path: '/abs/a.ts', lines: [{ number: 1, text: 'x' }], totalLines: 1, lang: 'ts' }
|
||||
const good = { path: '/abs/a.ts', offset: 1, lines: [{ number: 1, text: 'x' }], totalLines: 1, lang: 'ts' }
|
||||
|
||||
it('narrows a well-formed read meta, with and without a lang hint', () => {
|
||||
expect(readMetaFromMeta(good)).toEqual(good)
|
||||
const noLang = { path: '/abs/a', lines: [], totalLines: 0 }
|
||||
const noLang = { path: '/abs/a', offset: 1, lines: [], totalLines: 0 }
|
||||
expect(readMetaFromMeta(noLang)).toEqual(noLang)
|
||||
})
|
||||
|
||||
it('narrows an empty window at a positive offset (byte cap below the first selected line)', () => {
|
||||
const empty = { path: '/abs/a', offset: 5, lines: [], totalLines: 9 }
|
||||
expect(readMetaFromMeta(empty)).toEqual(empty)
|
||||
})
|
||||
|
||||
it('returns undefined for absent, non-object, or array meta', () => {
|
||||
expect(readMetaFromMeta(undefined)).toBeUndefined()
|
||||
expect(readMetaFromMeta(null)).toBeUndefined()
|
||||
@@ -168,6 +173,7 @@ describe('readMetaFromMeta', () => {
|
||||
|
||||
it('returns undefined when a field is missing or the wrong type (defensive narrowing)', () => {
|
||||
expect(readMetaFromMeta({ ...good, path: 5 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: '1' })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, totalLines: '1' })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: 'nope' })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, lines: [{ number: '1', text: 'x' }] })).toBeUndefined()
|
||||
@@ -176,6 +182,17 @@ describe('readMetaFromMeta', () => {
|
||||
expect(readMetaFromMeta({ ...good, lang: 5 })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects an offset that is not a 1-based integer', () => {
|
||||
expect(readMetaFromMeta({ ...good, offset: 0 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: 1.5 })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: NaN })).toBeUndefined()
|
||||
expect(readMetaFromMeta({ ...good, offset: Infinity })).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a first line number below offset', () => {
|
||||
expect(readMetaFromMeta({ ...good, offset: 2, lines: [{ number: 1, text: 'x' }], totalLines: 2 })).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()
|
||||
@@ -190,7 +207,7 @@ describe('readMetaFromMeta', () => {
|
||||
})
|
||||
|
||||
it('rejects lines that do not strictly increase or exceed totalLines', () => {
|
||||
const twoLines = { path: '/abs/a', lang: 'ts' }
|
||||
const twoLines = { path: '/abs/a', offset: 1, 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.
|
||||
|
||||
Reference in New Issue
Block a user