返回 DeepSeek-Reasonix
chat-reasoning-load-full-streaming.test.tsx
根目录 / desktop / frontend / src / __tests__ / chat-reasoning-load-full-streaming.test.tsx
1 import assert from "node:assert/strict";
2 import { act, createElement } from "react";
3 import { createRoot } from "react-dom/client";
4 import { createTranscriptHarness } from "./transcript-dom-harness";
5 import type { Item } from "../lib/useController";
6
7 const harness = await createTranscriptHarness({ deterministic: true });
8 const { ChatSource } = await harness.loadModule<typeof import("../lib/chatViewSource")>("/src/lib/chatViewSource.ts");
9 const { ChatNodeList } = await harness.loadModule<typeof import("../components/ChatNodes")>("/src/components/ChatNodes.tsx");
10 const { ChatMountedOrder } = await harness.loadModule<typeof import("../lib/chatMountedOrder")>("/src/lib/chatMountedOrder.ts");
11 const { ChatContentLoader } = await harness.loadModule<typeof import("../lib/chatContentLoader")>("/src/lib/chatContentLoader.ts");
12 const { ChatScrollController } = await harness.loadModule<typeof import("../lib/chatScrollController")>("/src/lib/chatScrollController.ts");
13 const { LocaleProvider } = await harness.loadModule<typeof import("../lib/i18n")>("/src/lib/i18n.tsx");
14
15 const source = new ChatSource("reasoning-load-full");
16 let fail = false;
17 let release: (() => void) | undefined;
18 const loads: string[] = [];
19 const loader = new ChatContentLoader(undefined, (item, field) => {
20 if (field !== "reasoning") return Promise.resolve("");
21 loads.push(item.id);
22 if (fail) return Promise.reject(new Error("unavailable"));
23 return new Promise<string>(resolve => { release = () => resolve((item as { reasoning: string }).reasoning); });
24 });
25 const scroll = new ChatScrollController("reasoning-load-full");
26 const mounts = new ChatMountedOrder();
27 await harness.unmount();
28 const root = createRoot(harness.container);
29 const question: Item = { kind: "user", id: "q", text: "think" };
30 const live = (reasoning: string, streaming = true): Item => ({ kind: "assistant", id: "a", text: "", reasoning, streaming });
31 const publish = async (items: Item[], running: boolean) => {
32 await act(async () => source.update({ items, running, hydrating: false, hasOlder: false, loadingOlder: false }));
33 await harness.settle();
34 };
35 const body = () => harness.container.querySelector(".chat-reasoning__body")?.textContent ?? "";
36 const loadButton = () => [...harness.container.querySelectorAll<HTMLButtonElement>(".chat-reasoning__body > .btn")][0];
37
38 try {
39 const first = "a".repeat(9000);
40 await publish([question, live(first)], true);
41 await act(async () => root.render(createElement(LocaleProvider, null,
42 createElement(ChatNodeList, { source, mounts, loader, scroll, actions: { openDetails: () => {}, recover: () => {} } }))));
43 await harness.settle();
44 await act(async () => harness.container.querySelector<HTMLElement>(".chat-reasoning [data-disclosure-row]")!.click());
45 assert.ok(loadButton(), "long streaming reasoning offers a full-text button");
46 await act(async () => loadButton()!.click());
47 await harness.settle();
48 assert.ok(body().length >= first.length, "clicking load shows the whole reasoning while it streams");
49 assert.equal(loadButton(), undefined, "the button is gone once the full text is shown");
50
51 const grown = first + "b".repeat(500);
52 await publish([question, live(grown)], true);
53 assert.ok(body().length >= grown.length, "the loaded view follows the growing stream instead of snapping back");
54 assert.equal(loadButton(), undefined, "streaming chunks do not bring the button back");
55
56 const final = grown + "c".repeat(500);
57 await publish([question, live(final, false)], false);
58 assert.ok(body().length >= final.length, "the body never shrinks while the settled load is pending");
59 assert.equal(loadButton(), undefined, "no button while the settled load is pending");
60 await act(async () => { release?.(); });
61 await harness.settle();
62 assert.ok(body().length >= final.length, "the loaded view survives the settled load");
63 assert.equal(loadButton(), undefined, "settling does not bring the button back");
64 assert.equal(loads.length, 1, "the settled load runs once");
65
66 const copies = loads.length;
67 await publish([question, { ...live("d".repeat(9000)), id: "b" }], true);
68 await act(async () => harness.container.querySelectorAll<HTMLButtonElement>(".chat-reasoning .copybtn")[1]?.click());
69 assert.equal(loads.length, copies, "copying while streaming does not start a load");
70
71 const start = loads.length;
72 await publish([question, { ...live("e".repeat(9000)), id: "c" }], true);
73 await act(async () => harness.container.querySelector<HTMLElement>(".chat-reasoning [data-disclosure-row]")!.click());
74 await act(async () => loadButton()!.click());
75 const idle = loadButton() ? "" : "gone";
76 fail = true;
77 const failedFinal = "e".repeat(9500);
78 await publish([question, { ...live(failedFinal, false), id: "c" }], false);
79 await harness.settle();
80 assert.equal(loads.length, start + 1, "the failing load ran once");
81 assert.ok(body().length >= failedFinal.length, "a failed load does not shrink the body");
82 const retry = loadButton();
83 assert.ok(retry, `a failed load keeps a retry button (${idle})`);
84 assert.equal(retry!.disabled, false, "the retry button is enabled");
85 const failedLabel = retry!.textContent;
86 fail = false;
87 await act(async () => retry!.click());
88 await act(async () => { release?.(); });
89 await harness.settle();
90 assert.equal(loads.length, start + 2, "retry loads again");
91 assert.equal(loadButton(), undefined, "a successful retry removes the button");
92 assert.ok(failedLabel, "the failed state is labelled");
93 } finally {
94 await act(async () => root.unmount());
95 source.dispose(); mounts.dispose(); loader.dispose(); scroll.dispose();
96 await harness.unmount(); await harness.close();
97 }
98 console.log("reasoning load full: stays loaded across streaming chunks and settlement");
99
99 lines Plain Text