import { createMessage, createUserMessage } from '@deepseek-ai/dsh-llm' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' import { describe, expect, it } from 'vitest' import { projectConversationHistory } from '../src/client/session-history/history-fold.ts' import { compactHistoryInspectionEntries } from '../src/client/sessions/history.ts' import { inspectRequests } from '../src/client/sessions/request-inspection.ts' import { ev } from './event-script.ts' const at = (seq: number, event: Record): SessionEvent => ({ seq, time: 1_700_000_000_000 + seq, ...event }) as unknown as SessionEvent describe('projectConversationHistory', () => { it('names an injected context node from its durable source, like the live adapter', () => { // The fold declares its own node mapping (jscpd:ignore in the source), so // the provenance projection is pinned on both sides independently. const injected = at(0, { type: 'user/message', surfaceOp: 'append', data: createUserMessage({ content: [{ type: 'text', text: '' }], // A plugin source, because the client program does not see the host // packages that merge richer source kinds; those arms are pinned in // context-provenance.spec.ts. source: { kind: 'plugin', plugin: 'dsh-tool-skill', form: 'catalog' }, }), }) const { contexts } = projectConversationHistory([{ event: injected }]) expect(contexts[contexts.length - 1]?.nodes).toMatchObject([{ kind: 'context', seq: 0, provenance: { role: 'inject', label: 'dsh-tool-skill' }, form: 'catalog', }]) }) it('projects next-step human input as durable steering', () => { const steering = createUserMessage({ content: [{ type: 'text', text: 'change course' }], source: { kind: 'user' }, }) const events = [ at(0, { type: 'agent/inbox/spliced', data: { target: 'next-step', start: 0, inserted: [steering], } }), at(1, { type: 'agent/inbox/spliced', data: { target: 'next-step', start: 0, removedCount: 1, inserted: [], } }), at(2, { type: 'user/message', surfaceOp: 'append', data: steering }), ] const projection = projectConversationHistory(events.map(event => ({ event }))) expect(projection.eventNodes).toMatchObject([{ kind: 'steering', messageId: steering.id, seq: 2, }]) }) it('projects a high-sequence history window without synthesizing its unloaded prefix', () => { const baseSeq = 400_000 const events = [ ev.user(baseSeq, 'loaded tail'), at(baseSeq + 1, { type: 'assistant/message', surfaceOp: { op: 'replace', start: baseSeq, end: baseSeq }, sourceEventSeqs: [baseSeq], data: { turn: 80, step: 1, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'tail summary' }], source: { kind: 'model', provider: 'fake', model: 'fake' }, }), }, }), ] const projection = projectConversationHistory(events.map(event => ({ event }))) expect(projection.eventNodes.map(node => node.seq)).toEqual([baseSeq, baseSeq + 1]) expect(projection.contexts.map(context => ({ originSeq: context.originSeq, nodes: context.nodes.map(node => node.seq), }))).toEqual([ { originSeq: undefined, nodes: [baseSeq] }, { originSeq: baseSeq + 1, nodes: [baseSeq + 1] }, ]) }) it('projects frozen surface generations without widening the core live surface', () => { const events = [ ev.user(0, 'a'), ev.user(1, 'b'), at(2, { type: 'assistant/message', surfaceOp: { op: 'replace', start: 0, end: 0 }, sourceEventSeqs: [0], data: { turn: 1, step: 1, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'summary' }], source: { kind: 'model', provider: 'fake', model: 'fake' }, }), }, }), at(3, { type: 'assistant/message', surfaceOp: { op: 'replace', start: 2, end: 1 }, sourceEventSeqs: [2, 1], data: { turn: 1, step: 2, message: createMessage({ role: 'assistant', content: [{ type: 'text', text: 'summary 2' }], source: { kind: 'model', provider: 'fake', model: 'fake' }, }), }, }), ] expect(projectConversationHistory(events.map(event => ({ event }))).contexts.map(context => ({ id: context.id, parentId: context.parentId, originSeq: context.originSeq, nodes: context.nodes.map(node => node.seq), }))).toEqual([ { id: 0, parentId: undefined, originSeq: undefined, nodes: [0, 1] }, { id: 1, parentId: 0, originSeq: 2, nodes: [2, 1] }, { id: 2, parentId: 1, originSeq: 3, nodes: [3] }, ]) }) it('projects assistant timing and the active request header from history', () => { const projection = projectConversationHistory([ ev.stepStart(0, 1, 2), at(1, { type: 'request/header', data: { reason: 'initial', header: { config: { provider: 'fake', model: 'first' }, tools: [], }, } }), ev.chunkStart(2, 1, 2), ev.chunkText(3, 1, 'token', 2), ev.assistant(4, 1, 'done', 2), ev.stepStart(5, 2, 1), ev.chunkText(6, 2, 'next', 1), ev.assistant(7, 2, 'next done', 1), ].map(event => ({ event }))) expect(projection.eventNodes[0]).toMatchObject({ kind: 'assistant', timing: { stepStartTime: 1_700_000_000_000, firstTokenTime: 1_700_000_000_003, completedTime: 1_700_000_000_004, }, requestConfig: { provider: 'fake', model: 'first' }, }) expect(projection.eventNodes.at(-1)).toMatchObject({ timing: { stepStartTime: 1_700_000_000_005, firstTokenTime: 1_700_000_000_006, completedTime: 1_700_000_000_007, }, requestConfig: { provider: 'fake', model: 'first' }, }) }) it('drops completed token payloads without changing inspection projections', () => { const events = [ ev.user(0, 'before'), ev.stepStart(1, 1, 0), ev.chunkStart(2, 1), ev.chunkText(3, 1, ''), ev.chunkText(4, 1, 'first'), ev.chunkText(5, 1, ' discarded'), at(6, { type: 'assistant/chunk', data: { turn: 1, step: 0, chunk: { type: 'usage', usage: { inputTokens: 4, outputTokens: 2 } }, } }), ev.assistant(7, 1, 'first discarded'), ev.compactSummary(8, 'summary', 0, 7), ev.compactCheckpoint(9, 8, 0, 7), ev.stepStart(10, 2, 0), ev.chunkStart(11, 2), ev.chunkText(12, 2, 'interrupted'), ev.turnEnd(13, 2, 'aborted'), ] const raw = events.map(event => ({ event })) const compacted = compactHistoryInspectionEntries(raw) expect(compacted.map(entry => entry.event.seq)).toEqual([ 0, 1, 4, 6, 7, 8, 9, 10, 11, 12, 13, ]) expect(projectConversationHistory(compacted)).toEqual(projectConversationHistory(raw)) expect(inspectRequests(compacted)).toEqual(inspectRequests(raw)) }) })