122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
import assert from 'node:assert/strict'
|
|
import { applyAssistantEvent, getMessageParts } from '../src/shared/message-parts'
|
|
import type { ChatEvent, ChatMessage, ToolCallRecord } from '../src/shared/types'
|
|
|
|
function tool(overrides: Partial<ToolCallRecord> = {}): ToolCallRecord {
|
|
return {
|
|
id: 'tool-1',
|
|
name: 'read_file',
|
|
args: { path: 'notes.txt' },
|
|
argsRaw: '{"path":"notes.txt"}',
|
|
status: 'running',
|
|
...overrides
|
|
}
|
|
}
|
|
|
|
function reduce(message: ChatMessage, event: ChatEvent): ChatMessage {
|
|
return applyAssistantEvent(message, event)
|
|
}
|
|
|
|
function testOrderedPartsAndImmutableUpdates(): void {
|
|
const initial: ChatMessage = {
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: '',
|
|
tools: [],
|
|
parts: [],
|
|
createdAt: 1,
|
|
status: 'streaming'
|
|
}
|
|
|
|
const afterFirstText = reduce(initial, { runId: 'run-1', type: 'text', delta: 'before' })
|
|
const afterToolCall = reduce(afterFirstText, { runId: 'run-1', type: 'tool_call', call: tool() })
|
|
const afterSecondText = reduce(afterToolCall, { runId: 'run-1', type: 'text', delta: 'after' })
|
|
const afterToolResult = reduce(afterSecondText, {
|
|
runId: 'run-1',
|
|
type: 'tool_result',
|
|
call: tool({ status: 'ok', output: 'file contents', summary: 'file contents', durationMs: 12 })
|
|
})
|
|
const final = reduce(afterToolResult, {
|
|
runId: 'run-1',
|
|
type: 'done',
|
|
turnDurationMs: 22000,
|
|
content: 'beforeafter',
|
|
tools: [tool({ status: 'ok', output: 'file contents', summary: 'file contents', durationMs: 12 })]
|
|
})
|
|
|
|
assert.deepEqual(final.parts, [
|
|
{ type: 'text', text: 'before' },
|
|
{ type: 'tool', toolId: 'tool-1' },
|
|
{ type: 'text', text: 'after' }
|
|
])
|
|
assert.equal(final.content, 'beforeafter')
|
|
assert.equal(final.status, 'done')
|
|
assert.equal(final.turnDurationMs, 22000, 'terminal duration survives the shared live/persistence projection')
|
|
assert.equal(final.tools?.length, 1, 'tool updates must not create duplicate tool records')
|
|
assert.equal(final.parts?.filter((part) => part.type === 'tool' && part.toolId === 'tool-1').length, 1,
|
|
'tool updates must not create duplicate ordered references')
|
|
assert.equal(final.tools?.[0].status, 'ok')
|
|
assert.equal(final.tools?.[0].output, 'file contents')
|
|
|
|
assert.equal(initial.content, '', 'reducing events must not mutate the original message')
|
|
assert.deepEqual(initial.parts, [], 'reducing events must not mutate the original parts array')
|
|
assert.deepEqual(afterFirstText.parts, [{ type: 'text', text: 'before' }],
|
|
'later text and tool updates must not mutate prior message snapshots')
|
|
assert.equal(afterFirstText.content, 'before')
|
|
assert.deepEqual(afterToolResult.parts, [
|
|
{ type: 'text', text: 'before' },
|
|
{ type: 'tool', toolId: 'tool-1' },
|
|
{ type: 'text', text: 'after' }
|
|
])
|
|
}
|
|
|
|
function testLegacyFallback(): void {
|
|
const legacy: ChatMessage = {
|
|
id: 'legacy-1',
|
|
role: 'assistant',
|
|
content: 'legacy response',
|
|
tools: [tool({ id: 'legacy-tool', status: 'ok' })],
|
|
createdAt: 2
|
|
}
|
|
|
|
assert.deepEqual(getMessageParts(legacy), [
|
|
{ type: 'text', text: 'legacy response' },
|
|
{ type: 'tool', toolId: 'legacy-tool' }
|
|
])
|
|
}
|
|
|
|
function testTerminalToolFinalization(): void {
|
|
const initial: ChatMessage = {
|
|
id: 'assistant-terminal',
|
|
role: 'assistant',
|
|
content: 'partial answer',
|
|
tools: [tool({ output: 'retained output' })],
|
|
parts: [
|
|
{ type: 'text', text: 'partial answer' },
|
|
{ type: 'tool', toolId: 'tool-1' }
|
|
],
|
|
createdAt: 3,
|
|
status: 'streaming'
|
|
}
|
|
|
|
const cancelled = reduce(initial, { runId: 'run-cancel', type: 'cancelled' })
|
|
assert.equal(cancelled.status, 'cancelled')
|
|
assert.equal(cancelled.tools?.[0].status, 'cancelled')
|
|
assert.equal(cancelled.tools?.[0].output, 'retained output')
|
|
|
|
const errored = reduce(initial, { runId: 'run-error', type: 'error', message: 'connection lost' })
|
|
assert.equal(errored.status, 'error')
|
|
assert.equal(errored.error, 'connection lost')
|
|
assert.equal(errored.tools?.[0].status, 'error')
|
|
assert.equal(errored.tools?.[0].output, 'retained output')
|
|
|
|
assert.equal(initial.status, 'streaming', 'terminal updates must leave the prior message unchanged')
|
|
assert.equal(initial.tools?.[0].status, 'running', 'terminal updates must leave prior tool state unchanged')
|
|
assert.equal(initial.tools?.[0].output, 'retained output')
|
|
}
|
|
|
|
testOrderedPartsAndImmutableUpdates()
|
|
testLegacyFallback()
|
|
testTerminalToolFinalization()
|
|
console.log('message-order-regression: all checks passed')
|