Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
b6dddf934This file is pinned to the selected release. Download the original to open it in your design tool.
import { describe, it, expect } from 'vitest'
import {
lastTurnTextFromRawLines,
lineToEvents,
messagesToEvents,
newTurnState,
windowRawLines,
} from './normalize.js'
// Build a synthetic transcript: 3 turns, each = [user prompt, assistant tool_use, user tool_result].
// uuids: u1 a1 r1 | u2 a2 r2 | u3 a3 r3 (turn starts at line indices 0, 3, 6).
const line = (o: Record<string, unknown>) => JSON.stringify(o)
const userPrompt = (uuid: string, text: string) =>
line({ type: 'user', uuid, message: { role: 'user', content: [{ type: 'text', text }] } })
const asstToolUse = (uuid: string, id: string, name: string) =>
line({ type: 'assistant', uuid, message: { role: 'assistant', content: [{ type: 'tool_use', id, name, input: {} }] } })
const asstText = (uuid: string, text: string) =>
line({ type: 'assistant', uuid, message: { role: 'assistant', content: [{ type: 'text', text }] } })
const userToolResult = (uuid: string, toolUseId: string) =>
line({ type: 'user', uuid, message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: toolUseId, content: 'ok' }] } })
const LINES = [
userPrompt('u1', 'hello 1'), asstToolUse('a1', 't1', 'Bash'), userToolResult('r1', 't1'),
userPrompt('u2', 'hello 2'), asstToolUse('a2', 't2', 'Read'), userToolResult('r2', 't2'),
userPrompt('u3', 'hello 3'), asstToolUse('a3', 't3', 'Grep'), userToolResult('r3', 't3'),
]
describe('windowRawLines', () => {
it('newest window is turn-snapped, reports hasMore + a real oldestCursor', () => {
const w = windowRawLines(LINES, { limit: 2 })
// limit=2 lands mid-turn (index 7); snaps back to the turn start u3 (index 6).
expect(w.window).toEqual(LINES.slice(6, 9))
expect(w.hasMore).toBe(true)
expect(w.oldestCursor).toBe('u3')
expect(w.staleCursor).toBeUndefined()
// The window is self-contained: the tool_use precedes its tool_result, so messagesToEvents'
// stateful tool-name map resolves — the tool card renders with its real name.
const events = messagesToEvents(w.window)
expect(events.some((e) => e.type === 'user_message' && (e.payload as { content: string }).content === 'hello 3')).toBe(true)
expect(events.some((e) => e.type === 'tool_start' && (e.payload as { tool: string }).tool === 'Grep')).toBe(true)
})
it('pages backward with before=oldestCursor until hasMore=false', () => {
const p1 = windowRawLines(LINES, { limit: 2, before: 'u3' })
expect(p1.window).toEqual(LINES.slice(3, 6)) // turn 2
expect(p1.hasMore).toBe(true)
expect(p1.oldestCursor).toBe('u2')
const p2 = windowRawLines(LINES, { limit: 2, before: 'u2' })
expect(p2.window).toEqual(LINES.slice(0, 3)) // turn 1 — oldest
expect(p2.hasMore).toBe(false)
expect(p2.oldestCursor).toBe('u1')
})
it('flags a stale cursor (empty window) when before is not in the file', () => {
const w = windowRawLines(LINES, { limit: 2, before: 'does-not-exist' })
expect(w.staleCursor).toBe(true)
expect(w.window).toEqual([])
expect(w.hasMore).toBe(false)
})
it('returns everything when the window is larger than the transcript', () => {
const w = windowRawLines(LINES, { limit: 100 })
expect(w.window).toEqual(LINES)
expect(w.hasMore).toBe(false)
expect(w.oldestCursor).toBe('u1')
})
})
describe('lastTurnTextFromRawLines', () => {
it('extracts the last real user ask and assistant text from session JSONL', () => {
const lines = [
userPrompt('u1', 'old ask'),
asstText('a1', 'old answer'),
userPrompt('u2', 'current ask'),
asstToolUse('a2', 't2', 'Bash'),
userToolResult('r2', 't2'),
asstText('a3', 'first answer part'),
asstText('a4', 'second answer part'),
]
expect(lastTurnTextFromRawLines(lines)).toEqual({
userMessage: 'current ask',
assistantText: 'first answer part\n\nsecond answer part',
})
})
it('keeps the last REAL ask when bash-mode (!command) lines follow it', () => {
// Bash mode writes the command + its output back as a user line. It is the person running a shell,
// not a prompt, so the recap must stay on the real task instead of drifting to shell mechanics.
const lines = [
userPrompt('u1', 'scan the network for loginable OrangePis'),
asstText('a1', 'Found .143 — orangepi/orangepi works and it has passwordless sudo.'),
userPrompt('b1', "<bash-input>ssh orangepi@172.168.20.143 echo hi</bash-input>"),
userPrompt('b2', '<bash-stdout></bash-stdout><bash-stderr>unsupported option "no...".</bash-stderr>'),
asstText('a2', 'That paste dropped a space; put it on one line.'),
]
// The ask is the task, and the assistant text stays attributed to it (bash lines opened no new turn).
expect(lastTurnTextFromRawLines(lines)).toEqual({
userMessage: 'scan the network for loginable OrangePis',
assistantText: 'Found .143 — orangepi/orangepi works and it has passwordless sudo.\n\nThat paste dropped a space; put it on one line.',
})
})
it('still counts a user line that carries prose alongside a bash block', () => {
const lines = [
userPrompt('u1', 'old ask'),
asstText('a1', 'old answer'),
userPrompt('u2', 'here is the output, what now?\n<bash-stdout>port 22 open</bash-stdout>'),
asstText('a2', 'It is reachable — try the default creds next.'),
]
expect(lastTurnTextFromRawLines(lines)).toEqual({
userMessage: 'here is the output, what now?\n<bash-stdout>port 22 open</bash-stdout>',
assistantText: 'It is reachable — try the default creds next.',
})
})
it('skips compact metadata, platform prompts, and tool_result user echoes', () => {
const lines = [
line({ type: 'system', subtype: 'compact_boundary', compactMetadata: { trigger: 'auto' } }),
line({ type: 'user', uuid: 'meta', isCompactSummary: true, message: { role: 'user', content: [{ type: 'text', text: 'summary' }] } }),
userPrompt('u1', '<!-- CONTEXT SUMMARY -->\nplatform prompt'),
userPrompt('u2', 'real ask'),
userToolResult('r2', 'tool'),
asstText('a2', 'real answer'),
]
expect(lastTurnTextFromRawLines(lines)).toEqual({
userMessage: 'real ask',
assistantText: 'real answer',
})
})
})
describe('Claude local command records', () => {
const modelOutput = userPrompt(
'local-model',
'<local-command-stdout>Set model to \u001b[1mOpus 4.8\u001b[22m and saved as your default for new sessions</local-command-stdout>',
)
const effortOutput = userPrompt(
'local-effort',
'<local-command-stdout>Set effort level to high (saved as your default for new sessions)</local-command-stdout>',
)
it('does not render local command stdout during replay', () => {
expect(messagesToEvents([modelOutput, effortOutput])).toEqual([
{ type: 'done', payload: { result: 'success' } },
])
})
it('does not open a live turn for local command stdout', () => {
const state = newTurnState()
expect(lineToEvents(modelOutput, state)).toEqual([])
expect(lineToEvents(effortOutput, state)).toEqual([])
expect(state.turnOpen).toBe(false)
})
})
describe('turn close on terminal stop reasons', () => {
const asstStop = (uuid: string, text: string, stopReason: string) =>
line({ type: 'assistant', uuid, message: { role: 'assistant', content: [{ type: 'text', text }], stop_reason: stopReason } })
const openTurn = () => {
const state = newTurnState()
lineToEvents(userPrompt('u1', 'hello'), state)
expect(state.turnOpen).toBe(true)
return state
}
it('closes the turn on max_tokens and refusal (not just end_turn/stop_sequence)', () => {
for (const reason of ['end_turn', 'stop_sequence', 'max_tokens', 'refusal']) {
const state = openTurn()
const events = lineToEvents(asstStop('a1', 'partial', reason), state)
expect(events).toContainEqual({ type: 'turn_ended', payload: {} })
expect(state.turnOpen).toBe(false)
}
})
it('does NOT close on tool_use or pause_turn (turn continues)', () => {
for (const reason of ['tool_use', 'pause_turn']) {
const state = openTurn()
const events = lineToEvents(asstStop('a1', 'thinking', reason), state)
expect(events).not.toContainEqual({ type: 'turn_ended', payload: {} })
expect(state.turnOpen).toBe(true)
}
})
it('does NOT close on a terminal stop reason while a tool is still pending', () => {
const state = openTurn()
lineToEvents(asstToolUse('a1', 't1', 'Bash'), state) // adds t1 to pendingTools
const events = lineToEvents(asstStop('a2', 'done', 'end_turn'), state)
expect(events).not.toContainEqual({ type: 'turn_ended', payload: {} })
expect(state.turnOpen).toBe(true)
})
})
describe('bash-mode lines are not turns', () => {
it('a !command line opens no turn and leaves an open one untouched', () => {
const state = newTurnState()
// A bash-only line before any prompt: nothing opens.
expect(lineToEvents(userPrompt('b0', '<bash-input>ls</bash-input>'), state)).toEqual([])
expect(state.turnOpen).toBe(false)
// Open a real turn, then a bash exchange mid-turn: it neither ends nor starts a turn.
lineToEvents(userPrompt('u1', 'scan the network'), state)
expect(state.turnOpen).toBe(true)
expect(lineToEvents(userPrompt('b1', '<bash-input>ssh box echo hi</bash-input>'), state)).toEqual([])
expect(lineToEvents(userPrompt('b2', '<bash-stdout>hi</bash-stdout><bash-stderr></bash-stderr>'), state)).toEqual([])
expect(state.turnOpen).toBe(true)
})
it('a line with real prose beside a bash block still starts a turn', () => {
const state = newTurnState()
const events = lineToEvents(userPrompt('u1', 'what does this mean?\n<bash-stderr>boom</bash-stderr>'), state)
expect(events).toContainEqual({ type: 'turn_started', payload: { userMessage: 'what does this mean?\n<bash-stderr>boom</bash-stderr>' } })
expect(state.turnOpen).toBe(true)
})
})
describe('slash-command prompts', () => {
// Claude Code records a typed command ONLY as these tags; the expansion it sends to the model is a
// separate `isMeta` line we suppress. Before the fix both normalized to "" and NO turn ever opened,
// so the device's `/goal <text>` (deviceWs sends exactly this) tripped the submit-verify retries and
// surfaced "The agent did not accept the message" even though claude had run the command fine.
const goalCmd = userPrompt(
'c1',
'<command-name>/goal</command-name>\n <command-message>goal</command-message>\n <command-args>ship the release</command-args>',
)
const goalStdout = userPrompt('c2', '<local-command-stdout>Goal set: ship the release</local-command-stdout>')
const modelCmd = userPrompt(
'c3',
'<command-name>/model</command-name>\n <command-message>model</command-message>\n <command-args>haiku</command-args>',
)
it('opens a live turn with the command text verbatim (matches the injected fingerprint)', () => {
const state = newTurnState()
expect(lineToEvents(goalCmd, state)).toEqual([
{ type: 'turn_started', payload: { userMessage: '/goal ship the release' } },
])
expect(state.turnOpen).toBe(true)
expect(lineToEvents(goalStdout, state)).toEqual([]) // the echo must not re-open a turn
})
it('keeps local-only TUI commands silent', () => {
const state = newTurnState()
expect(lineToEvents(modelCmd, state)).toEqual([])
expect(state.turnOpen).toBe(false)
})
it('leads the recap with the command text', () => {
expect(lastTurnTextFromRawLines([goalCmd, goalStdout, asstText('a1', 'done')])).toEqual({
userMessage: '/goal ship the release',
assistantText: 'done',
})
})
})