返回 DeepSeek-Reasonix
session-prompt-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / session-prompt-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 { useSessionOperations } from "../app-runtime/useSessionOperations";
6 import { useSessionPromptCommands } from "../app-runtime/useSessionPromptCommands";
7 import type { PromptPorts } from "../app-runtime/sessionPromptExecutor";
8 import { CommandCancelled } from "../lib/commandOutcome";
9
10 const dom = new JSDOM("<div id='root'></div>");
11 Object.assign(globalThis, { window: dom.window, document: dom.window.document, IS_REACT_ACT_ENVIRONMENT: true });
12 const root = createRoot(document.getElementById("root")!);
13 function deferred() { let resolve!: () => void; const promise = new Promise<void>(done => { resolve = done; }); return { promise, resolve }; }
14 let entered = deferred();
15 let gate = deferred();
16 let prompt = "approval-A";
17 const calls: string[] = [];
18 const ports: PromptPorts = {
19 isPromptCurrentForTab: target => target.tabId === "A" && target.promptId === prompt,
20 approveForTab: target => { calls.push(`approve:${target.tabId}`); },
21 resolvePlanForTab: target => { calls.push(`resolve:${target.tabId}:${target.promptId}`); },
22 resolveRecoveryForTab: target => { calls.push(`recover:${target.tabId}`); },
23 answerQuestionForTab: async target => { calls.push(`question:${target.tabId}`); },
24 answerMCPForTab: target => { calls.push(`mcp:${target.tabId}`); },
25 setCollaborationModeForTab: async tab => { calls.push(`mode:${tab}`); },
26 clearGoalForTab: async tab => { calls.push(`clear:${tab}`); entered.resolve(); await gate.promise; },
27 setRemoteComposerProfile: async tab => { calls.push(`remote:${tab}`); entered.resolve(); await gate.promise; return [prompt]; },
28 patchComposerProfile: tab => { calls.push(`patch:${tab}`); },
29 notePlanMode: tab => { calls.push(`remember:${tab}`); },
30 drainRemoteApprovals: tab => { calls.push(`drain:${tab}`); },
31 rememberRevision: tab => { calls.push(`revision:${tab}`); },
32 };
33 let commands!: ReturnType<typeof useSessionPromptCommands>;
34 function Probe({ tab, generation = "1", remote = false }: { tab: string; generation?: string; remote?: boolean }) {
35 const target = { tabId: tab, sessionKey: tab + generation };
36 const operations = useSessionOperations({ visible: target, resources: ["A", "B"].map(tabId => ({ tabId, sessionKey: tabId + generation })) });
37 commands = useSessionPromptCommands({ target, session: { hostId: "local", sessionId: `session-${tab}` }, sessionGeneration: Number(generation), approval: { id: prompt, tool: "exit_plan_mode", subject: "plan", turnId: `turn-${tab}`, runtimeEpoch: `runtime-${tab}` },
38 remote, goal: "fixture", toolApprovalMode: "ask", ports, operations, reportError: error => { throw error; } });
39 return null;
40 }
41 async function paint(tab: string, generation = "1", remote = false) {
42 await act(async () => root.render(<Probe tab={tab} generation={generation} remote={remote} />));
43 }
44 function reset() { calls.length = 0; entered = deferred(); gate = deferred(); prompt = "approval-A"; }
45 try {
46 await paint("A");
47 let pending = commands.handleApprovalAnswer(true, false, false);
48 await entered.promise;
49 await paint("B");
50 gate.resolve(); await pending;
51 assert.deepEqual(calls, ["clear:A", "mode:A", "remember:A", "patch:A", "resolve:A:approval-A"]);
52
53 reset(); await paint("A");
54 pending = commands.handleExitPlan(); await entered.promise;
55 prompt = "replacement";
56 gate.resolve(); await assert.rejects(pending, CommandCancelled);
57 assert.deepEqual(calls, ["clear:A"], "replacement prompt revokes the entire continuation, including mode changes");
58
59 reset(); await paint("A");
60 pending = commands.handleExitPlan(); await entered.promise;
61 await paint("A", "2"); gate.resolve(); await assert.rejects(pending, CommandCancelled);
62 assert.deepEqual(calls, ["clear:A"], "same tab with a different session cannot resolve an old approval");
63
64 reset(); await paint("A", "1", true);
65 pending = commands.handleExitPlan(); await entered.promise;
66 await paint("B", "1", true); await paint("A", "1", true);
67 gate.resolve(); await pending;
68 assert.deepEqual(calls, ["remote:A", "remember:A", "patch:A", "resolve:A:approval-A"], "ABA never drains the new surface's approvals");
69
70 reset(); await paint("A");
71 pending = commands.handleExitPlan(); await entered.promise;
72 await commands.handleApprovalAnswer(false, false, false);
73 gate.resolve(); await assert.rejects(pending, CommandCancelled);
74 assert.deepEqual(calls, ["clear:A", "resolve:A:approval-A"], "new decision supersedes the older mode/approval chain");
75
76 reset(); await paint("A");
77 pending = commands.handleExitPlan(); await entered.promise;
78 await act(async () => root.unmount());
79 gate.resolve(); await assert.rejects(pending, CommandCancelled);
80 await assert.rejects(commands.handleRecoveryAnswer("stop"), CommandCancelled);
81 assert.deepEqual(calls, ["clear:A"], "unmount revokes the stable entry and every in-flight continuation");
82 console.log("session prompts: source, prompt identity, replacement, ABA, supersession and disposal passed");
83 } finally { dom.window.close(); }
84
84 lines Plain Text