| 1 | import assert from "node:assert/strict"; |
| 2 | import { completionSummaryPresentation, normalizeCompletionSummary } from "../lib/completionSummary"; |
| 3 | import { mergeTurnResult, normalizeTurnChanges, turnChangeText, turnCheckState } from "../lib/turnResult"; |
| 4 | import { historicalResultNotice, withTurnResult, withRunningChecks } from "../lib/completionResultState"; |
| 5 | import { ChatSource } from "../lib/chatViewSource"; |
| 6 | import { initialState, reducer, type State, type Item } from "../lib/useController"; |
| 7 | import { t } from "../lib/i18n"; |
| 8 | import type { TurnChanges } from "../lib/types"; |
| 9 | |
| 10 | const diff: TurnChanges = { id: "0:1", turn: 0, coverage: "complete", files: [{ path: "a.ts", kind: "modify", added: 2, removed: 1 }], added: 2, removed: 1, reasons: [] }; |
| 11 | const legacy = { ...mergeTurnResult(), mutations: 17, changed_files: 9 }; |
| 12 | assert.equal(turnChangeText(legacy, t), "Change statistics unavailable"); |
| 13 | assert.equal(turnCheckState(legacy).status, "unknown"); |
| 14 | const receipt = { verdict: "partial", diff, verifications: [] }; |
| 15 | const result = normalizeCompletionSummary(mergeTurnResult(legacy, receipt, "turn-0", 0)); |
| 16 | assert.equal(result.checkpointTurn, 0); |
| 17 | assert.match(turnChangeText(result, t), /1.*file.*\+2 −1/); |
| 18 | assert.equal(turnCheckState(result).status, "none"); |
| 19 | assert.equal(completionSummaryPresentation(result, "standard", t)?.title, "Turn result"); |
| 20 | assert.match(turnChangeText(mergeTurnResult(undefined, { ...receipt, diff: { ...diff, coverage: "partial" } }), t), /partial/i); |
| 21 | assert.equal(normalizeTurnChanges({ ...diff, added: -1 })?.coverage, "unknown"); |
| 22 | for (const [check, expected] of [ |
| 23 | [{ command: "test", passed: true, exitCode: 1 }, "failed"], |
| 24 | [{ command: "test", passed: true, stale: true }, "stale"], |
| 25 | [{ command: "test", passed: false, interrupted: true }, "interrupted"], |
| 26 | [{ command: "test", passed: true, exitCode: 0 }, "passed"], |
| 27 | ] as const) assert.equal(turnCheckState(mergeTurnResult(undefined, { ...receipt, verifications: [check] })).status, expected); |
| 28 | const user: Item = { kind: "user", id: "u0", text: "change" }; |
| 29 | let state = withTurnResult({ ...initialState, items: [user], seq: 10 } as State, result); |
| 30 | const stableId = state.items[1].id; |
| 31 | state = withTurnResult(state, result); |
| 32 | assert.equal(state.items.length, 2); |
| 33 | assert.equal(state.items[1].id, stableId); |
| 34 | state = withTurnResult({ ...state, items: [...state.items, { ...user, id: "u1" }] }, { ...result, turnId: "turn-1" }); |
| 35 | assert.equal(state.items.length, 4); |
| 36 | assert.equal(state.items[1].id, stableId); |
| 37 | const tool = (id: string): Item => ({ kind: "tool", id, name: "exec_command", args: '{"command":"go test ./..."}', readOnly: false, status: "running", verifying: true }); |
| 38 | state = withRunningChecks({ ...state, items: [...state.items, tool("c1"), tool("c2")] }); |
| 39 | assert.equal(state.completionSummary?.liveChecks?.length, 2); |
| 40 | state = withRunningChecks({ ...state, items: state.items.map(i => i.kind === "tool" && i.id === "c1" ? { ...i, status: "done" } : i) }); |
| 41 | assert.equal(state.completionSummary?.checking, true, "one completed parallel check does not stop the other"); |
| 42 | const history = historicalResultNotice({ role: "notice", content: "", completionReceipt: receipt, checkpointTurn: 0, turnId: "turn-0" }, "history"); |
| 43 | assert.equal(history?.completionSummary?.receipt?.diff?.id, diff.id); |
| 44 | assert.equal(history?.completionSummary?.checkpointTurn, 0); |
| 45 | const answer: Item = { kind: "assistant", id: "a0", text: "done", reasoning: "", streaming: false }; |
| 46 | const source = new ChatSource("completion-results"); |
| 47 | source.update({ items: [user, history!, answer], running: false, hydrating: false, hasOlder: false, loadingOlder: false }); |
| 48 | assert.equal(source.getNodeSnapshot("history")?.kind, "notice", "historical results survive as ordinary records"); |
| 49 | source.dispose(); |
| 50 | for (const phase of ["checking", "verifying", "working", "reviewing"]) { |
| 51 | const plain: State = { ...initialState, items: [user], seq: 1 }; |
| 52 | const next = reducer(plain, { type: "event", e: { kind: "turn_phase", phase } }); |
| 53 | assert.equal(next.turnPhase, phase); |
| 54 | assert.equal(next.completionSummary?.checking ?? false, false, `${phase} is not verification evidence`); |
| 55 | assert.deepEqual(next.items, plain.items, `${phase} must not insert a result card`); |
| 56 | |
| 57 | const checking = withRunningChecks({ ...plain, items: [user, tool("check")] }); |
| 58 | const resultId = checking.items.find(i => i.kind === "notice")!.id; |
| 59 | const active = reducer(checking, { type: "event", e: { kind: "turn_phase", phase } }); |
| 60 | assert.equal(active.completionSummary?.checking, true, `${phase} preserves real running checks`); |
| 61 | assert.equal(active.items.find(i => i.kind === "notice")!.id, resultId); |
| 62 | } |
| 63 | let live = reducer({ ...initialState, items: [user] }, { type: "event", e: { kind: "tool_dispatch", tool: { id: "live", name: "bash", readOnly: false, args: '{"command":"go test ./..."}' } } }); |
| 64 | live = reducer(live, { type: "event", e: { kind: "tool_progress", tool: { id: "live", name: "bash", readOnly: false, verifying: true, output: "running" } } }); |
| 65 | assert.equal(live.completionSummary?.checking, true); |
| 66 | live = reducer(live, { type: "event", e: { kind: "turn_phase", phase: "working" } }); |
| 67 | assert.equal(live.completionSummary?.checking, true); |
| 68 | live = reducer(live, { type: "event", e: { kind: "tool_result", tool: { id: "live", name: "bash", readOnly: false, output: "PASS" } } }); |
| 69 | assert.equal(live.completionSummary?.checking, false); |
| 70 | console.log("turn result truth, stable identity, concurrent checks, phases and history passed"); |
| 71 |