/** * Chunk-row codec tests: pack/expand round-trip losslessness (example-based and * property-based), run-boundary rules, whitelist fall-through, and decoder * validation failures. */ import { describe, expect, it } from 'vitest' import fc from 'fast-check' import { CallId } from '@deepseek-ai/dsh-llm' import type { StreamChunk } from '@deepseek-ai/dsh-llm' import { decodeStorageRecord, packChunkRuns } from '@deepseek-ai/dsh-session' import type { ChunkRow, SessionEvent, StorageRecord } from '@deepseek-ai/dsh-session' /** Build an `assistant/chunk` event with the exact live-append shape. */ function chunkEvent(seq: number, time: number, chunk: StreamChunk, turn = 1, step = 1): SessionEvent { return { type: 'assistant/chunk', seq, time, data: { turn, step, chunk } } } /** Sequential delta events (contiguous seqs, fixed 10ms gaps) of one kind. */ function deltaRun(kind: 'text-delta' | 'reasoning-delta', count: number, seq0 = 0, index = 0): SessionEvent[] { return Array.from({ length: count }, (_, k) => chunkEvent(seq0 + k, 1000 + 10 * k, { type: kind, index, text: `t${k}` })) } /** Decode a packed record list back to a flat event list. */ function decodeAll(records: readonly StorageRecord[]): SessionEvent[] { return records.flatMap(record => decodeStorageRecord(JSON.parse(JSON.stringify(record)))) } describe('packChunkRuns', () => { it('packs a text-delta run into one text-chunks row and round-trips it', () => { const events = deltaRun('text-delta', 5) const packed = packChunkRuns(events) expect(packed).toHaveLength(1) const row = packed[0] as ChunkRow expect(row.type).toBe('text-chunks') expect(row.seq0).toBe(0) expect(row.time0).toBe(1000) expect(row.data).toMatchObject({ turn: 1, step: 1, index: 0, dt: [10, 10, 10, 10], texts: ['t0', 't1', 't2', 't3', 't4'] }) expect(decodeAll(packed)).toStrictEqual(events) }) it('packs reasoning and tool-call runs under their own tags', () => { const reasoning = deltaRun('reasoning-delta', 3) const toolCall = [4, 5, 6].map(seq => chunkEvent(seq, 1000 + seq, { type: 'tool-call-delta', index: 1, id: CallId('c1'), name: 'write', argumentsDelta: `a${seq}` })) const packed = packChunkRuns([...reasoning, ...toolCall]) expect(packed.map(r => (r as ChunkRow).type)).toStrictEqual(['reasoning-chunks', 'tool-call-chunks']) const row = packed[1] as ChunkRow & { type: 'tool-call-chunks' } expect(row.data).toMatchObject({ id: 'c1', name: 'write', args: ['a4', 'a5', 'a6'] }) expect(decodeAll(packed)).toStrictEqual([...reasoning, ...toolCall]) }) it('packs a name-less tool-call run and round-trips field absence', () => { const events = [0, 1, 2].map(seq => chunkEvent(seq, 1000, { type: 'tool-call-delta', index: 0, id: CallId('c1'), argumentsDelta: `a${seq}` })) const packed = packChunkRuns(events) expect(packed).toHaveLength(1) expect(Object.hasOwn((packed[0] as ChunkRow).data, 'name')).toBe(false) const decoded = decodeAll(packed) expect(decoded).toStrictEqual(events) expect(decoded.every(e => !Object.hasOwn((e.data as { chunk: object }).chunk, 'name'))).toBe(true) }) it('leaves runs shorter than three events verbatim', () => { const events = deltaRun('text-delta', 2) expect(packChunkRuns(events)).toStrictEqual(events) }) it('leaves non-delta chunks and non-chunk events verbatim between runs', () => { const events: SessionEvent[] = [ chunkEvent(0, 1000, { type: 'block-start', index: 0, blockType: 'text' }), ...deltaRun('text-delta', 3, 1), chunkEvent(4, 1040, { type: 'block-end', index: 0, block: { type: 'text', text: 't0t1t2' } }), { type: 'step/end', seq: 5, time: 1050, data: { turn: 1, step: 1 } }, ] const packed = packChunkRuns(events) expect(packed).toHaveLength(4) expect((packed[1] as ChunkRow).type).toBe('text-chunks') expect(decodeAll(packed)).toStrictEqual(events) }) it.each([ ['a seq gap', deltaRun('text-delta', 3).map((e, k) => ({ ...e, seq: k === 2 ? 9 : e.seq }))], ['a kind switch', [...deltaRun('text-delta', 2), ...deltaRun('reasoning-delta', 1, 2)]], ['a block-index switch', [...deltaRun('text-delta', 2), ...deltaRun('text-delta', 1, 2, 7)]], ['a step switch', deltaRun('text-delta', 3).map((e, k) => k === 2 ? chunkEvent(e.seq, e.time, (e.data as { chunk: StreamChunk }).chunk, 1, 2) : e)], ])('breaks a run on %s (both halves too short to pack)', (_label, events) => { expect(packChunkRuns(events as SessionEvent[])).toStrictEqual(events) }) it('breaks a tool-call run on call-id or name change', () => { const call = (seq: number, id: string, name?: string): SessionEvent => chunkEvent(seq, 1000, { type: 'tool-call-delta', index: 0, id: CallId(id), ...name !== undefined ? { name } : {}, argumentsDelta: 'a' }) const idSwitch = [call(0, 'c1', 'w'), call(1, 'c1', 'w'), call(2, 'c2', 'w')] expect(packChunkRuns(idSwitch)).toStrictEqual(idSwitch) const namePresence = [call(0, 'c1', 'w'), call(1, 'c1', 'w'), call(2, 'c1')] expect(packChunkRuns(namePresence)).toStrictEqual(namePresence) }) it('stores an off-whitelist delta verbatim (extra field, bad type, fractional time)', () => { const extraField = { ...chunkEvent(0, 1000, { type: 'text-delta', index: 0, text: 'x' }), surfaceOp: 'append' } const badText = chunkEvent(1, 1001, { type: 'text-delta', index: 0, text: 7 as unknown as string }) const fractionalTime = chunkEvent(2, 1001.5, { type: 'text-delta', index: 0, text: 'y' }) const events = [extraField, badText, fractionalTime] as SessionEvent[] expect(packChunkRuns(events)).toStrictEqual(events) }) it('breaks a run on a time gap beyond safe-integer range (subtraction would round)', () => { // Both endpoints are safe integers, but their true difference (~2^54) // exceeds exact double range: b - a rounds, so a + (b - a) !== b and a // packed row would decode to a different timestamp. const a = Number.MIN_SAFE_INTEGER const b = Number.MAX_SAFE_INTEGER - 1 expect(a + (b - a)).not.toBe(b) // the rounding this guard exists for const events = [ chunkEvent(0, a, { type: 'text-delta', index: 0, text: 'x' }), chunkEvent(1, b, { type: 'text-delta', index: 0, text: 'y' }), chunkEvent(2, b + 1, { type: 'text-delta', index: 0, text: 'z' }), ] expect(packChunkRuns(events)).toStrictEqual(events) // split at the gap; halves too short expect(decodeAll(packChunkRuns(events))).toStrictEqual(events) }) it('stores a delta with an off-whitelist data envelope verbatim (parsed-fixture shapes)', () => { const mk = (seq: number, data: unknown): SessionEvent => ({ type: 'assistant/chunk', seq, time: 1000, data } as SessionEvent) const events = [ mk(0, 'not-an-object'), mk(1, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' }, extra: 1 }), mk(2, { turn: 'x', step: 1, chunk: { type: 'text-delta', index: 0, text: 'a' } }), mk(3, { turn: 1, step: 1, chunk: 'not-an-object' }), mk(4, { turn: 1, step: 1, chunk: { type: 'text-delta', index: 'x', text: 'a' } }), mk(5, { turn: 1, step: 1, chunk: { type: 'tool-call-delta', index: 0, id: 7, argumentsDelta: 'a' } }), mk(6, { turn: 1, step: 1, chunk: { type: 'tool-call-delta', index: 0, id: 'c', name: 7, argumentsDelta: 'a' } }), ] expect(packChunkRuns(events)).toStrictEqual(events) }) }) describe('decodeStorageRecord', () => { it('passes non-row values through as single events, unvalidated', () => { const event = { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } } expect(decodeStorageRecord(event)).toStrictEqual([event]) expect(decodeStorageRecord('junk')).toStrictEqual(['junk']) expect(decodeStorageRecord(null)).toStrictEqual([null]) }) it('reconstructs timestamps through negative dt gaps (clock stepped back)', () => { const events = [ chunkEvent(0, 1000, { type: 'text-delta', index: 0, text: 'a' }), chunkEvent(1, 990, { type: 'text-delta', index: 0, text: 'b' }), chunkEvent(2, 995, { type: 'text-delta', index: 0, text: 'c' }), ] expect(decodeAll(packChunkRuns(events))).toStrictEqual(events) }) it.each([ ['a non-object data', { type: 'text-chunks', seq0: 0, time0: 1, data: 'x' }], ['an envelope with extra keys', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [], texts: ['a'] }, extra: 1 }], ['a negative seq0', { type: 'text-chunks', seq0: -1, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [], texts: ['a'] } }], ['a non-finite time0', { type: 'text-chunks', seq0: 0, time0: Infinity, data: { turn: 1, step: 1, index: 0, dt: [], texts: ['a'] } }], ['a fractional time0', { type: 'text-chunks', seq0: 0, time0: 1.5, data: { turn: 1, step: 1, index: 0, dt: [], texts: ['a'] } }], ['a data shape mismatch', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [], args: ['a'] } }], ['a non-string member', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [], texts: [7] } }], ['an empty member list', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [], texts: [] } }], ['a dt arity mismatch', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [1, 2], texts: ['a', 'b'] } }], ['a non-finite dt gap', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [NaN], texts: ['a', 'b'] } }], ['a fractional dt gap', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [0.5], texts: ['a', 'b'] } }], ['a member seq leaving safe range', { type: 'text-chunks', seq0: Number.MAX_SAFE_INTEGER, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [0, 0], texts: ['a', 'b', 'c'] } }], ['a member time leaving safe range', { type: 'text-chunks', seq0: 0, time0: Number.MAX_SAFE_INTEGER, data: { turn: 1, step: 1, index: 0, dt: [1], texts: ['a', 'b'] } }], ['a non-numeric turn', { type: 'text-chunks', seq0: 0, time0: 1, data: { turn: 'x', step: 1, index: 0, dt: [], texts: ['a'] } }], ['a tool-call row without id', { type: 'tool-call-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, dt: [], args: ['a'] } }], ['a tool-call row with non-string id', { type: 'tool-call-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, id: 7, dt: [], args: ['a'] } }], ['a tool-call row with non-string name', { type: 'tool-call-chunks', seq0: 0, time0: 1, data: { turn: 1, step: 1, index: 0, id: 'c', name: 7, dt: [], args: ['a'] } }], ])('throws on %s', (_label, row) => { expect(() => decodeStorageRecord(row)).toThrow(/malformed .* storage row/) }) }) // --- Property: pack∘decode is the identity over arbitrary event batches --- const deltaChunkArb: fc.Arbitrary = fc.oneof( fc.record({ type: fc.constant<'text-delta'>('text-delta'), index: fc.nat(2), text: fc.string() }), fc.record({ type: fc.constant<'reasoning-delta'>('reasoning-delta'), index: fc.nat(2), text: fc.string() }), fc.record({ type: fc.constant<'tool-call-delta'>('tool-call-delta'), index: fc.nat(2), id: fc.constantFrom(CallId('c1'), CallId('c2')), argumentsDelta: fc.string(), }), fc.record({ type: fc.constant<'tool-call-delta'>('tool-call-delta'), index: fc.nat(2), id: fc.constantFrom(CallId('c1'), CallId('c2')), name: fc.constantFrom('write', 'read'), argumentsDelta: fc.string(), }), ) const boundaryChunkArb: fc.Arbitrary = fc.oneof( fc.record({ type: fc.constant<'block-start'>('block-start'), index: fc.nat(2), blockType: fc.constant<'text'>('text') }), fc.record({ type: fc.constant<'finish'>('finish'), reason: fc.constant({ kind: 'stop' as const }) }), ) /** * Batches with contiguous seqs, arbitrary timestamps, mixed chunk kinds and * turn/step placement. Times draw from the FULL safe-integer range (not just * realistic clocks) so the property exercises the gap-overflow guard: two safe * endpoints can differ by more than a double subtracts exactly. */ const batchArb: fc.Arbitrary = fc.array( fc.record({ chunk: fc.oneof({ weight: 4, arbitrary: deltaChunkArb }, { weight: 1, arbitrary: boundaryChunkArb }), time: fc.oneof( { weight: 4, arbitrary: fc.integer({ min: 995, max: 9000 }) }, { weight: 1, arbitrary: fc.integer({ min: Number.MIN_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER }) }, ), turn: fc.nat(1), step: fc.nat(1), }), { maxLength: 40 }, // JSON round-trip normalizes fast-check's null-prototype records into the // plain objects real log events are (the log is JSON), so equality compares // values, not prototypes. ).map(entries => JSON.parse(JSON.stringify( entries.map((entry, k) => chunkEvent(k, entry.time, entry.chunk, entry.turn, entry.step)), )) as SessionEvent[]) describe('chunk-row codec properties', () => { it('JSON-serialized pack∘decode reproduces every batch exactly', () => { fc.assert(fc.property(batchArb, (events) => { expect(decodeAll(packChunkRuns(events))).toStrictEqual(events) })) }) })