返回 DeepSeek-Reasonix
turn-fork-transcript.test.tsx
根目录 / desktop / frontend / src / __tests__ / turn-fork-transcript.test.tsx
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
77 lines Plain Text