| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- /**
- * 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)).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<StreamChunk> = 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<StreamChunk> = 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<SessionEvent[]> = 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)
- }))
- })
- })
|