返回 DeepSeek-Reasonix
controller-profile-lifecycle.test.tsx
根目录 / desktop / frontend / src / __tests__ / controller-profile-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 { useControllerProfileCommands } from "../lib/useControllerProfileCommands";
6 import { useSessionOperations } from "../app-runtime/useSessionOperations";
7 import type { ControllerProfileResource } from "../app-runtime/controllerProfileOwner";
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!: (value: boolean) => void; let reject!: (error: Error) => void;
14 const promise = new Promise<boolean>((yes, no) => { resolve = yes; reject = no; });
15 return { promise, resolve, reject };
16 }
17 const calls: string[] = [], errors: unknown[] = [];
18 let pending = deferred();
19 let profileFailure: Error | undefined;
20 let profileGate: ReturnType<typeof deferred> | undefined;
21 const ports = {
22 model: async (tab: string, name: string) => { calls.push(`model:${tab}:${name}`); return pending.promise; },
23 profile: async (tab: string, collaboration: string, approval: string, goal: string) => {
24 calls.push(`profile:${tab}:${collaboration}:${approval}:${goal}`);
25 if (profileGate) return profileGate.promise;
26 if (profileFailure) throw profileFailure;
27 return true;
28 },
29 };
30 let commands!: ReturnType<typeof useControllerProfileCommands>;
31 function Probe({ tab, generation, plan, ready, epoch, remote }: { tab: string; generation: number; plan: boolean; ready: boolean; epoch: string; remote: boolean }) {
32 const profiles: ControllerProfileResource[] = ["A", "B"].map(tabId => ({
33 target: { tabId, sessionKey: tabId + generation }, remote: remote && tabId === "A",
34 profile: { collaboration: tabId === "A" && plan ? "plan" : "normal", approval: "ask", goal: tabId === "B" ? "B goal" : "" },
35 }));
36 const target = profiles.find(value => value.target.tabId === tab)!.target;
37 const operations = useSessionOperations({ visible: target, resources: profiles.map(value => value.target) });
38 commands = useControllerProfileCommands({ target, profiles, ready, runtimeEpoch: epoch, remote: remote && tab === "A", operations, ports,
39 remoteModel: async name => { calls.push(`remote:${tab}:${name}`); await pending.promise; }, report: error => errors.push(error) });
40 return null;
41 }
42 const paint = (tab = "A", plan = true, generation = 1, ready = false, epoch = "runtime-1", remote = false) => act(async () => root.render(
43 <Probe tab={tab} generation={generation} plan={plan} ready={ready} epoch={epoch} remote={remote} />));
44 try {
45 await paint();
46 const change = commands.switchModel("first");
47 await paint("B", false);
48 pending.resolve(true); assert.equal(await change, false, "a completed source write does not regain UI ownership on B");
49 assert.deepEqual(calls, ["model:A:first", "profile:A:normal:ask:"], "post-rebuild profile is the latest committed source value, not the old render or B");
50
51 calls.length = 0; pending = deferred(); await paint();
52 const stale = commands.switchModel("replaced");
53 await paint("A", true, 2); pending.resolve(true);
54 assert.equal(await stale, false);
55 assert.deepEqual(calls, ["model:A:replaced"], "replacement session rejects old post-model profile writes");
56
57 calls.length = 0; pending = deferred(); await paint();
58 const first = commands.switchModel("old");
59 const old = pending; pending = deferred();
60 const second = commands.switchModel("new");
61 old.resolve(true); assert.equal(await first, false);
62 pending.resolve(true); assert.equal(await second, true);
63 assert.deepEqual(calls, ["model:A:old", "model:A:new", "profile:A:plan:ask:"], "superseded model cannot restore its profile or clear the new request");
64
65 calls.length = 0; errors.length = 0; pending = deferred();
66 const failure = commands.switchModelFromUi("failure");
67 await paint("B"); await paint("A"); pending.reject(Error("old source failure"));
68 assert.equal(await failure, false); assert.deepEqual(errors, [], "A-B-A cannot revive old error UI");
69
70 pending = deferred();
71 const currentFailure = commands.switchModelFromUi("current-failure");
72 const error = Error("model failed"); pending.reject(error);
73 assert.equal(await currentFailure, false); assert.deepEqual(errors, [error], "UI failure is presented exactly once");
74 errors.length = 0; pending = deferred();
75 const directFailure = commands.switchModel("slash-model"); pending.reject(error);
76 await assert.rejects(directFailure, error);
77 assert.deepEqual(errors, [], "awaiting callers retain the reject contract without duplicate UI handling");
78
79 profileFailure = Error("restore failed");
80 assert.equal(await commands.applyProfile("A", false), false, "send readiness retains false-on-failure semantics");
81 await paint("A", true, 1, true);
82 assert.deepEqual(errors, [profileFailure], "background restoration reports its real error once");
83 profileFailure = undefined; errors.length = 0; await paint();
84
85 calls.length = 0; await paint("A", true, 1, true);
86 assert.deepEqual(calls, ["profile:A:plan:ask:"], "ready restoration shares source-profile execution");
87 await paint("A", false, 1, true);
88 assert.equal(calls.at(-1), "profile:A:normal:ask:");
89 calls.length = 0;
90 await paint("A", false, 1, true, "runtime-2");
91 assert.deepEqual(calls, ["profile:A:normal:ask:"], "same profile on a replacement runtime is restored without relying on object churn");
92
93 for (const modelFirst of [true, false]) {
94 await paint(); calls.length = 0; errors.length = 0;
95 profileGate = deferred(); pending = deferred();
96 if (!modelFirst) await paint("A", true, 1, true);
97 const overlapping = commands.switchModelFromUi("overlap");
98 await act(async () => pending.resolve(true));
99 if (modelFirst) await paint("A", true, 1, true);
100 const sharedError = Error("one Controller application failed");
101 await act(async () => profileGate!.reject(sharedError));
102 assert.equal(await overlapping, false);
103 assert.deepEqual(errors, [sharedError], "model and readiness observers share one failure owner in either completion order");
104 profileGate = undefined;
105 }
106
107 calls.length = 0; pending = deferred(); await paint("A", true, 1, true, "runtime-1", true);
108 const remote = commands.switchModel("remote-model");
109 await paint("B", true, 1, false, "runtime-1", true);
110 pending.resolve(true); assert.equal(await remote, false);
111 assert.deepEqual(calls, ["remote:A:remote-model"], "remote model stays source-bound and never uses local profile restoration");
112
113 await paint(); calls.length = 0; pending = deferred();
114 const disposed = commands.switchModel("disposed");
115 await act(async () => root.unmount()); pending.resolve(true); await disposed;
116 commands.switchModel("after-unmount");
117 assert.deepEqual(calls, ["model:A:disposed"], "unmount revokes continuation and stable entry immediately");
118 console.log("controller profile lifecycle: committed source, replacement, ordering, ABA, ready restore and disposal passed");
119 } finally { dom.window.close(); }
120
120 lines Plain Text