|
|
@@ -1,99 +1,15 @@
|
|
|
-/** Packed history record folding without token-by-token browser expansion. */
|
|
|
+/** Packed history records decode to the exact Session event stream. */
|
|
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
import { CallId } from '@deepseek-ai/dsh-llm/brand'
|
|
|
-import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows'
|
|
|
import type { SessionHistoryRecord } from '../src/types.ts'
|
|
|
-import { coalesceHistoryChunkRun, historyEntries } from '../src/client/sessions/history-records.ts'
|
|
|
-
|
|
|
-describe('coalesceHistoryChunkRun', () => {
|
|
|
- it('preserves first-token and first-visible boundaries with at most two text events', () => {
|
|
|
- const row: ChunkRow = {
|
|
|
- type: 'text-chunks',
|
|
|
- seq0: 10,
|
|
|
- time0: 100,
|
|
|
- data: {
|
|
|
- turn: 2,
|
|
|
- step: 3,
|
|
|
- index: 0,
|
|
|
- dt: [1, 2, 3, 4],
|
|
|
- texts: ['', ' ', '', 'hello', ' world'],
|
|
|
- },
|
|
|
- }
|
|
|
-
|
|
|
- const events = coalesceHistoryChunkRun(row)
|
|
|
- expect(events).toHaveLength(2)
|
|
|
- expect(events.map(event => ({ seq: event.seq, time: event.time, text: event.data.chunk.type === 'text-delta' ? event.data.chunk.text : '' })))
|
|
|
- .toEqual([
|
|
|
- { seq: 11, time: 101, text: ' ' },
|
|
|
- { seq: 13, time: 106, text: 'hello world' },
|
|
|
- ])
|
|
|
- expect(events[0]?.time).toBe(101)
|
|
|
- expect(events.find(event => event.data.chunk.type === 'text-delta' && event.data.chunk.text.trim() !== '')?.time)
|
|
|
- .toBe(106)
|
|
|
- })
|
|
|
-
|
|
|
- it('joins visible reasoning members into one event at the first non-empty member', () => {
|
|
|
- const row: ChunkRow = {
|
|
|
- type: 'reasoning-chunks',
|
|
|
- seq0: 4,
|
|
|
- time0: 50,
|
|
|
- data: { turn: 1, step: 1, index: 2, dt: [5, 7], texts: ['', 'a', 'b'] },
|
|
|
- }
|
|
|
- const [event] = coalesceHistoryChunkRun(row)
|
|
|
- expect(event).toMatchObject({
|
|
|
- seq: 5,
|
|
|
- time: 55,
|
|
|
- data: { chunk: { type: 'reasoning-delta', index: 2, text: 'ab' } },
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
- it('joins tool arguments while retaining name presence and first-token time', () => {
|
|
|
- const named: ChunkRow = {
|
|
|
- type: 'tool-call-chunks',
|
|
|
- seq0: 20,
|
|
|
- time0: 200,
|
|
|
- data: {
|
|
|
- turn: 2,
|
|
|
- step: 4,
|
|
|
- index: 1,
|
|
|
- id: CallId('call-1'),
|
|
|
- name: 'write',
|
|
|
- dt: [2, 3],
|
|
|
- args: ['', '{"x":', '1}'],
|
|
|
- },
|
|
|
- }
|
|
|
- expect(coalesceHistoryChunkRun(named)).toMatchObject([{
|
|
|
- seq: 20,
|
|
|
- time: 200,
|
|
|
- data: { chunk: { type: 'tool-call-delta', name: 'write', argumentsDelta: '{"x":1}' } },
|
|
|
- }])
|
|
|
-
|
|
|
- const unnamed: ChunkRow = {
|
|
|
- type: 'tool-call-chunks',
|
|
|
- seq0: 20,
|
|
|
- time0: 200,
|
|
|
- data: {
|
|
|
- turn: 2,
|
|
|
- step: 4,
|
|
|
- index: 1,
|
|
|
- id: CallId('call-1'),
|
|
|
- dt: [2, 3],
|
|
|
- args: ['', '', 'x'],
|
|
|
- },
|
|
|
- }
|
|
|
- const [event] = coalesceHistoryChunkRun(unnamed)
|
|
|
- expect(event).toMatchObject({ seq: 22, time: 205, data: { chunk: { argumentsDelta: 'x' } } })
|
|
|
- expect(Object.hasOwn(event?.data.chunk ?? {}, 'name')).toBe(false)
|
|
|
- })
|
|
|
-})
|
|
|
+import { historyEntries } from '../src/client/sessions/history-records.ts'
|
|
|
|
|
|
describe('historyEntries', () => {
|
|
|
- it('keeps ordinary entries and views while folding a packed run without expansion', () => {
|
|
|
+ it('keeps ordinary entries and expands every packed text member with its exact boundary', () => {
|
|
|
const ordinary = {
|
|
|
event: { type: 'turn/start', seq: 0, time: 1, data: { turn: 1 } },
|
|
|
- view: { for: 'call', view: { card: 'generic' } },
|
|
|
- } as unknown as SessionHistoryRecord
|
|
|
+ } as SessionHistoryRecord
|
|
|
const packed: SessionHistoryRecord = {
|
|
|
chunks: {
|
|
|
type: 'text-chunks',
|
|
|
@@ -102,9 +18,48 @@ describe('historyEntries', () => {
|
|
|
data: { turn: 1, step: 1, index: 0, dt: [1, 1, 1], texts: ['a', 'b', 'c', 'd'] },
|
|
|
},
|
|
|
}
|
|
|
+
|
|
|
const entries = historyEntries([ordinary, packed])
|
|
|
- expect(entries).toHaveLength(2)
|
|
|
+
|
|
|
+ expect(entries).toHaveLength(5)
|
|
|
expect(entries[0]).toBe(ordinary)
|
|
|
- expect(entries[1]?.event).toMatchObject({ seq: 1, data: { chunk: { text: 'abcd' } } })
|
|
|
+ expect(entries.slice(1).map(entry => ({
|
|
|
+ seq: entry.event.seq,
|
|
|
+ time: entry.event.time,
|
|
|
+ chunk: entry.event.type === 'assistant/chunk' ? entry.event.data.chunk : undefined,
|
|
|
+ }))).toEqual([
|
|
|
+ { seq: 1, time: 2, chunk: { type: 'text-delta', index: 0, text: 'a' } },
|
|
|
+ { seq: 2, time: 3, chunk: { type: 'text-delta', index: 0, text: 'b' } },
|
|
|
+ { seq: 3, time: 4, chunk: { type: 'text-delta', index: 0, text: 'c' } },
|
|
|
+ { seq: 4, time: 5, chunk: { type: 'text-delta', index: 0, text: 'd' } },
|
|
|
+ ])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('preserves every tool-call fragment and optional-name presence', () => {
|
|
|
+ const packed: SessionHistoryRecord = {
|
|
|
+ chunks: {
|
|
|
+ type: 'tool-call-chunks',
|
|
|
+ seq0: 20,
|
|
|
+ time0: 200,
|
|
|
+ data: {
|
|
|
+ turn: 2,
|
|
|
+ step: 4,
|
|
|
+ index: 1,
|
|
|
+ id: CallId('call-1'),
|
|
|
+ dt: [2, 3],
|
|
|
+ args: ['', '{"x":', '1}'],
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ const events = historyEntries([packed]).map(entry => entry.event)
|
|
|
+
|
|
|
+ expect(events).toMatchObject([
|
|
|
+ { seq: 20, time: 200, data: { chunk: { argumentsDelta: '' } } },
|
|
|
+ { seq: 21, time: 202, data: { chunk: { argumentsDelta: '{"x":' } } },
|
|
|
+ { seq: 22, time: 205, data: { chunk: { argumentsDelta: '1}' } } },
|
|
|
+ ])
|
|
|
+ expect(events.every(event => event.type === 'assistant/chunk'
|
|
|
+ && !Object.hasOwn(event.data.chunk, 'name'))).toBe(true)
|
|
|
})
|
|
|
})
|