| 1 | // A completed turn forks from its persisted boundary, identified by the |
| 2 | // assistant message it ends with. Index, page offset, and checkpoint state must |
| 3 | // not move the cut, and every refusal keeps its own reason. |
| 4 | import assert from "node:assert/strict"; |
| 5 | import { act } from "react"; |
| 6 | import { createTranscriptHarness } from "./transcript-dom-harness"; |
| 7 | import type { Item } from "../lib/useController"; |
| 8 | import type { ForkTargetSetView } from "../lib/forkTargets"; |
| 9 | const harness = await createTranscriptHarness(); |
| 10 | const items: Item[] = [ |
| 11 | { kind: "user", id: "u42", text: "history starts mid-session", checkpointTurn: 42 }, |
| 12 | { kind: "assistant", id: "m:a42", text: "answer", reasoning: "", streaming: false, |
| 13 | createdAt: new Date(2026, 8, 12, 12, 34).getTime(), turnDurationMs: 29_000, tokensPerSecond: 183, |
| 14 | turnUsage: { totalTokens: 185_225, uncachedInputTokens: 26_278, cacheReadTokens: 155_520, outputTokens: 3_427, reasoningTokens: 1_909, routes: ["deepseek-official/deepseek-flash"] } }, |
| 15 | ]; |
| 16 | const target = (targets: ForkTargetSetView["targets"], verifiable = true): ForkTargetSetView => ({ targets, verifiable }); |
| 17 | const available = { sourceSessionId: "source-1", sessionGeneration: 4, turnId: "turn-42", boundarySequence: 99, turnNumber: 42, status: "committed", messageId: "a42", available: true }; |
| 18 | const calls: typeof available[] = []; |
| 19 | const props = { forkTargets: target([available]), onFork: (forkTarget: typeof available) => { calls.push(forkTarget); } }; |
| 20 | try { |
| 21 | await harness.render(items, props); await harness.settle(); |
| 22 | const branch = () => harness.container.querySelector<HTMLButtonElement>(".chat-actions button.chat-action-icon:not(.copybtn)")!; |
| 23 | assert.ok(branch(), "completed turn exposes the icon-only Harness branch action"); |
| 24 | assert.equal(branch().textContent, "", "branch label stays in the tooltip instead of the reading flow"); |
| 25 | assert.equal(branch().closest(".chat-actions")?.getAttribute("data-actions-reveal"), "always", "latest turn actions remain visible"); |
| 26 | assert.equal(branch().hasAttribute("disabled"), false, "available branch is interactive"); |
| 27 | assert.equal(branch().getAttribute("aria-disabled"), null); |
| 28 | await act(async () => { branch().focus(); await new Promise(resolve => setTimeout(resolve, 1)); }); |
| 29 | assert.equal(harness.dom.window.document.querySelector('[role="tooltip"]')?.textContent, branch().getAttribute("aria-label"), "keyboard focus exposes the Harness tooltip"); |
| 30 | await act(async () => branch().click()); |
| 31 | assert.deepEqual(calls, [available], "an enabled branch carries the complete source and boundary anchor"); |
| 32 | const statButtons = () => [...harness.container.querySelectorAll<HTMLButtonElement>(".chat-stat-trigger")]; |
| 33 | assert.equal(statButtons().length, 2, "completed answers expose Harness usage and time pills"); |
| 34 | assert.match(statButtons()[0].textContent ?? "", /185\.2K|185K/); |
| 35 | await act(async () => statButtons()[0].click()); |
| 36 | const usageDialog = harness.dom.window.document.querySelector<HTMLElement>("[data-turn-usage-details]")!; |
| 37 | assert.ok(usageDialog, "usage pill opens its anchored details dialog"); |
| 38 | assert.match(usageDialog.textContent ?? "", /deepseek-official\/deepseek-flash/); |
| 39 | assert.match(usageDialog.textContent ?? "", /155,520/); |
| 40 | await act(async () => statButtons()[1].click()); |
| 41 | const timeDialog = harness.dom.window.document.querySelector<HTMLElement>("[data-turn-time-details]")!; |
| 42 | assert.ok(timeDialog, "time pill opens its anchored details dialog"); |
| 43 | assert.match(timeDialog.textContent ?? "", /29/); |
| 44 | assert.match(timeDialog.textContent ?? "", /183/); |
| 45 | assert.match(harness.container.querySelector(".chat-actions__time")?.textContent ?? "", /12:34/); |
| 46 | assert.equal(harness.container.textContent?.includes("Like"), false, "feedback actions are intentionally not transplanted"); |
| 47 | assert.equal(harness.container.querySelector(".msg-edit"), null); |
| 48 | |
| 49 | const refusal = async (overrides: Record<string, unknown>, expect: RegExp, label: string) => { |
| 50 | await harness.render(items, { ...props, ...overrides }); await harness.settle(); |
| 51 | const before = calls.length; |
| 52 | assert.equal(branch().hasAttribute("disabled"), false, `${label}: unavailable action remains focusable for its explanation`); |
| 53 | assert.equal(branch().getAttribute("aria-disabled"), "true", `${label}: reports itself unavailable`); |
| 54 | assert.equal(branch().getAttribute("aria-describedby"), null, `${label}: needs no remount-sensitive description node`); |
| 55 | assert.match(branch().getAttribute("aria-label") ?? "", expect, `${label}: includes its reason in the accessible name`); |
| 56 | await act(async () => branch().click()); |
| 57 | assert.equal(calls.length, before, `${label}: never dispatches`); |
| 58 | }; |
| 59 | await refusal({ forkTargets: undefined }, /Checking which turns/, "unloaded set"); |
| 60 | await refusal({ forkTargets: { targets: [], verifiable: false } }, /verifiable branch boundary/, "legacy history"); |
| 61 | await refusal({ forkTargets: target([{ ...available, available: false, reason: "turn_open" }]) }, /not finished yet/, "open turn"); |
| 62 | // The boundary is proven here; it is the child it would carry that is unsafe, |
| 63 | // so this refusal must not read as a missing boundary. |
| 64 | await refusal({ forkTargets: target([{ ...available, available: false, reason: "active_authority" }]) }, /question or approval.*tool action.*still open/, "unusable boundary"); |
| 65 | await refusal({ forkTargets: target([{ ...available, turnId: "turn-41", messageId: "a41" }]) }, /not finished yet/, "answer without a persisted boundary"); |
| 66 | await refusal({ forkBlocked: "creating" }, /Creating the branch/, "request in flight"); |
| 67 | await refusal({ forkBlocked: "read_only" }, /does not allow creating/, "read-only surface"); |
| 68 | await refusal({ forkBlocked: "unsupported" }, /server's version/, "server without create-only fork"); |
| 69 | for (const running of [{ running: true }, { hydrating: true }]) { |
| 70 | calls.length = 0; |
| 71 | await harness.render(items, { ...props, ...running }); await harness.settle(); |
| 72 | await act(async () => branch().click()); |
| 73 | assert.deepEqual(calls, [available], "the fork entry follows the persisted boundary, not the turn's runtime state"); |
| 74 | } |
| 75 | console.log("chat branches: message identity, per-state reasons and persisted boundaries passed"); |
| 76 | } finally { await harness.unmount(); await harness.close(); } |
| 77 |