From e1a71c08bc1fa420af6b2e601d03ead92fdff378 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 14:33:27 +0800 Subject: [PATCH] fix(test): retain session fixture line diagnostics --- packages/test-support/llm-replay/src/index.ts | 29 ++++++++++++++++--- .../llm-replay/tests/llm-replay.spec.ts | 6 ++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/test-support/llm-replay/src/index.ts b/packages/test-support/llm-replay/src/index.ts index 2fff5956a7..7f9f208897 100644 --- a/packages/test-support/llm-replay/src/index.ts +++ b/packages/test-support/llm-replay/src/index.ts @@ -166,20 +166,41 @@ export interface SessionScript { * @returns every event after the header, in log order. */ export function parseSessionLog(text: string): SessionEvent[] { - const lines = text.split('\n').filter(line => line.trim().length > 0) const events: SessionEvent[] = [] let nextSeq = 0 + let headerSkipped = false // The JSONL backend guarantees line 0 is the session header. Projected // fixtures omit event envelopes; synthesize them while decoding so callers // still receive complete SessionEvent values. - for (let i = 1; i < lines.length; i++) { - const record = JSON.parse(lines[i] as string) as Record + for (const [index, line] of text.split(/\r?\n/).entries()) { + if (line.trim().length === 0) continue + if (!headerSkipped) { + headerSkipped = true + continue + } + let value: unknown + try { + value = JSON.parse(line) as unknown + } catch (error) { + throw new Error(`session snapshot line ${index + 1} contains invalid JSON`, { cause: error }) + } + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`session snapshot line ${index + 1} must be a JSON object`) + } + const record = value as Record const packed = PACKED_CHUNK_ROW_TYPES.has(record.type as string) const seqKey = packed ? 'seq0' : 'seq' const timeKey = packed ? 'time0' : 'time' if (!Object.hasOwn(record, seqKey)) record[seqKey] = nextSeq if (!Object.hasOwn(record, timeKey)) record[timeKey] = 0 - const decoded = decodeStorageRecord(record) + let decoded: SessionEvent[] + try { + decoded = decodeStorageRecord(record) + } catch (error) { + /* v8 ignore next -- decodeStorageRecord only throws Error instances; the String arm satisfies unknown narrowing. */ + const detail = error instanceof Error ? error.message : String(error) + throw new Error(`session snapshot line ${index + 1}: ${detail}`, { cause: error }) + } events.push(...decoded) nextSeq += decoded.length } diff --git a/packages/test-support/llm-replay/tests/llm-replay.spec.ts b/packages/test-support/llm-replay/tests/llm-replay.spec.ts index 3d6fd2c383..16244c5ce5 100644 --- a/packages/test-support/llm-replay/tests/llm-replay.spec.ts +++ b/packages/test-support/llm-replay/tests/llm-replay.spec.ts @@ -96,6 +96,12 @@ describe('parseSessionLog', () => { expect(parseSessionLog(`${header}\n\n${JSON.stringify(ev)}\n\n`)).toEqual([ev]) }) + it('rejects non-object body rows with their source line', () => { + const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 }) + expect(() => parseSessionLog(`${header}\nnull\n`)) + .toThrow('session snapshot line 2 must be a JSON object') + }) + it('expands a packed chunk row into its events (a fixture recorded with packChunks on)', () => { const header = JSON.stringify({ type: 'session', version: 0, id: 's1', createdAt: 0 }) const row = JSON.stringify({