返回 DeepSeek-Reasonix
pending-plan-revision-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / pending-plan-revision-lifecycle.test.tsx
1 import assert from "node:assert/strict";
2 import React, { act } from "react";
3 import { createRoot } from "react-dom/client";
4 import { JSDOM } from "jsdom";
5 import { usePendingPlanRevisions } from "../lib/usePendingPlanRevisions";
6 import { useSessionOperations } from "../app-runtime/useSessionOperations";
7 import { useSessionSubmission } from "../lib/useSessionSubmission";
8
9 const dom = new JSDOM("<div id='root'></div>");
10 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
11 const root = createRoot(document.getElementById("root")!);
12 function deferred() {
13 let resolve!: () => void; let reject!: (error: Error) => void;
14 const promise = new Promise<void>((yes, no) => { resolve = yes; reject = no; });
15 return { promise, resolve, reject };
16 }
17 const requests: { tab: string; text: string; gate: ReturnType<typeof deferred> }[] = [];
18 const errors: unknown[] = [];
19 let remember!: ReturnType<typeof usePendingPlanRevisions>;
20 function Probe({ tab, gen, running, ready }: { tab: string; gen: number; running: boolean; ready: boolean }) {
21 const resources = ["A", "B"].map(tabId => ({ tabId, sessionKey: tabId + gen }));
22 const visible = resources.find(target => target.tabId === tab)!;
23 const operations = useSessionOperations({ visible, resources });
24 const submission = useSessionSubmission({ target: visible, operations, missingSource: "missing",
25 resources: resources.map(target => ({ target, remote: false, ready: true, unavailable: "", goalDraft: false,
26 collaboration: "normal", approval: "ask" })),
27 ports: {
28 send: (tab, text) => { const gate = deferred(); requests.push({ tab, text, gate }); return gate.promise; },
29 clearUndo: () => {}, setGoal: async () => {}, patchGoal: () => {}, profile: async () => true,
30 },
31 });
32 remember = usePendingPlanRevisions({ visible, resources, running, ready, operations,
33 send: submission.sendRevision,
34 report: error => { errors.push(error); },
35 });
36 return null;
37 }
38 const paint = (tab = "A", gen = 1, running = false, ready = true) => act(async () => root.render(<Probe tab={tab} gen={gen} running={running} ready={ready} />));
39 try {
40 await paint("A", 1, true); remember("A", "first");
41 assert.equal(requests.length, 0, "running turn holds its revision");
42 await paint("A", 1, false, false);
43 assert.equal(requests.length, 0, "an idle but uncommitted navigation surface cannot submit a revision");
44 await paint("B"); assert.equal(requests.length, 0, "B does not submit A's pending revision");
45 remember("B", "B revision"); assert.equal(requests[0].tab, "B");
46 await paint("A"); assert.deepEqual(requests.map(({ tab, text }) => [tab, text]), [["B", "B revision"], ["A", "first"]]);
47
48 remember("A", "same text"); remember("A", "same text");
49 await paint("A", 2); remember("A", "replacement");
50 assert.equal(requests.length, 3, "replacement resource can start while the old resource's transport is pending");
51 remember("A", "latest");
52 await act(async () => requests[1].gate.resolve());
53 assert.equal(requests.length, 3, "old finally cannot release the new request or start its queued successor");
54 await act(async () => requests[2].gate.resolve());
55 assert.equal(requests[3].text, "latest", "matching completion starts exactly the replacement revision");
56 await act(async () => requests[3].gate.resolve());
57 await paint("A", 2); assert.equal(requests.length, 4, "terminal revisions leave no retried request");
58 await act(async () => requests[0].gate.resolve());
59
60 remember("A", "failure"); await paint("B", 2); await paint("A", 2);
61 await act(async () => requests[4].gate.reject(Error("old failure")));
62 assert.deepEqual(errors, [], "A-B-A does not restore failure UI ownership");
63 await paint("B", 2); await paint("A", 2);
64 assert.equal(requests[5].text, "failure", "source data survives suppressed old error UI and can be retried on a new activation");
65 await act(async () => requests[5].gate.resolve());
66 remember("A", "retryable revision");
67 await act(async () => requests[6].gate.reject(Error("current failure")));
68 assert.equal(errors.length, 1);
69 await paint("A", 2); assert.equal(requests.length, 7, "unrelated commits do not loop on a failed revision");
70 await paint("B", 2); await paint("A", 2);
71 assert.equal(requests[7].text, "retryable revision", "explicit source reactivation retains the existing retryable revision");
72 await act(async () => requests[7].gate.resolve());
73 remember("A", "disposed"); remember("A", "must not follow");
74 await act(async () => root.unmount());
75 remember("A", "after unmount");
76 await act(async () => requests[8].gate.resolve());
77 assert.equal(requests.length, 9, "synchronous disposal releases queue and revokes old follow-on work");
78 console.log("pending plan revision lifecycle: source queues, request identity, replacement, ABA and disposal passed");
79 } finally { dom.window.close(); }
80
80 lines Plain Text