Review follow-ups on the context provenance row: - the producer name now follows ToolRow's aria-hidden separator dot instead of a literal middot inside the text, so the two disclosure rows expose one accessible-name shape; - the English steering caption becomes the noun `Interjection`, parallel to the Chinese 插话 and to the label this bubble carried before; - the history fold gets its own provenance assertion, so the mapping it declares separately from TranscriptAdapter is pinned on both sides; - the superseded-in-part bullet on the no-interjection-chrome note now enumerates the third Decision bullet too, whose no-badge and no-IconActions clauses had both lapsed.
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
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 { ev } from './event-script.ts'
|
|
|
|
const at = (seq: number, event: Record<string, unknown>): 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: '<available_skills>…</available_skills>' }],
|
|
// 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' },
|
|
}),
|
|
})
|
|
const { contexts } = projectConversationHistory([{ event: injected }])
|
|
expect(contexts[contexts.length - 1]?.nodes).toMatchObject([{
|
|
kind: 'context', seq: 0, provenance: { role: 'inject', label: 'dsh-tool-skill' },
|
|
}])
|
|
})
|
|
|
|
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' },
|
|
})
|
|
})
|
|
})
|