返回 DeepSeek-Reasonix
composer-profile.test.ts
根目录 / desktop / frontend / src / __tests__ / composer-profile.test.ts
1 // Run: tsx src/__tests__/composer-profile.test.ts
2
3 import {
4 composerProfileFromMeta,
5 composerProfileFromTab,
6 composerProfileMode,
7 controllerComposerProfileCollaborationMode,
8 displayedComposerProfileCollaborationMode,
9 hydrateComposerProfileFromMeta,
10 hydrateComposerProfilesFromTabs,
11 patchComposerProfile,
12 pruneUserPlanModeIntents,
13 resolvePlanRestoreTabId,
14 shouldRestoreUserPlanMode,
15 shouldRestoreUserPlanModeForProfile,
16 updateUserPlanModeIntent,
17 type ComposerProfilesByTab,
18 type UserPlanModeIntents,
19 } from "../lib/composerProfile";
20 import type { Meta, TabMeta } from "../lib/types";
21
22 type LooseTabMeta = Omit<TabMeta, "toolApprovalMode"> & { toolApprovalMode?: TabMeta["toolApprovalMode"] | "" };
23 type LooseMeta = Omit<Meta, "toolApprovalMode"> & { toolApprovalMode?: Meta["toolApprovalMode"] | "" };
24
25 let passed = 0;
26 let failed = 0;
27
28 function eq(a: unknown, b: unknown, label: string) {
29 if (a === b) {
30 process.stdout.write(` PASS ${label}\n`);
31 passed += 1;
32 } else {
33 process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`);
34 failed += 1;
35 }
36 }
37
38 function tab(overrides: Partial<LooseTabMeta> = {}): TabMeta {
39 return {
40 id: "tab-1",
41 scope: "project",
42 workspaceRoot: "/repo",
43 workspaceName: "repo",
44 topicId: "topic-1",
45 topicTitle: "Topic",
46 label: "DeepSeek-R1",
47 ready: true,
48 running: false,
49 mode: "normal",
50 collaborationMode: "normal",
51 toolApprovalMode: "workspace-write",
52 goal: "",
53 goalStatus: "stopped",
54 active: true,
55 cwd: "/repo",
56 ...overrides,
57 } as TabMeta;
58 }
59
60 function meta(overrides: Partial<LooseMeta> = {}): Meta {
61 return {
62 label: "DeepSeek-R1",
63 ready: true,
64 eventChannel: "events",
65 cwd: "/repo",
66 autoApproveTools: false,
67 bypass: false,
68 collaborationMode: "normal",
69 toolApprovalMode: "workspace-write",
70 goal: "",
71 goalStatus: "stopped",
72 ...overrides,
73 } as Meta;
74 }
75
76 console.log("\ncomposer profile");
77
78 {
79 const goalView = {
80 id: "goal-1", revision: 2, objective: "finish the migration", phase: "paused" as const,
81 maxGoalRounds: null, roundsStarted: 4, createdAt: "2026-09-13T10:00:00Z",
82 updatedAt: "2026-09-13T10:10:00Z", activation: "disarmed" as const, stopReason: "user-paused",
83 };
84 eq(composerProfileFromTab(tab({ goal: "finish the migration", goalStatus: "stopped", goalView })).goal,
85 "finish the migration", "paused lifecycle goal remains visible from tab metadata");
86 eq(composerProfileFromMeta(meta({ goal: "finish the migration", goalStatus: "stopped", goalView })).goal,
87 "finish the migration", "paused lifecycle goal remains visible from controller metadata");
88 }
89
90 {
91 let profiles: ComposerProfilesByTab = {};
92 profiles = hydrateComposerProfilesFromTabs(profiles, [tab({ toolApprovalMode: "auto" })]);
93 eq(profiles["tab-1"].toolApprovalMode, "workspace-write", "legacy auto hydrates as workspace write");
94 profiles = hydrateComposerProfileFromMeta(profiles, "tab-1", meta({ toolApprovalMode: "auto" }));
95 eq(Boolean(profiles["tab-1"].pending.toolApprovalMode), false, "workspace write is acknowledged by legacy meta");
96 }
97
98 {
99 let profiles: ComposerProfilesByTab = {};
100 profiles = hydrateComposerProfilesFromTabs(profiles, [tab()]);
101 profiles = patchComposerProfile(
102 profiles,
103 "tab-1",
104 profiles["tab-1"],
105 { collaborationMode: "normal", goalDraftMode: true, goal: "" },
106 ["collaborationMode", "goal"],
107 );
108 profiles = patchComposerProfile(
109 profiles,
110 "tab-1",
111 profiles["tab-1"],
112 { collaborationMode: "plan", goalDraftMode: false, goal: "" },
113 ["collaborationMode", "goal"],
114 );
115
116 profiles = hydrateComposerProfilesFromTabs(profiles, [tab()]);
117
118 eq(displayedComposerProfileCollaborationMode(profiles["tab-1"]), "plan", "stale tab hydration keeps locally selected plan mode");
119 eq(composerProfileMode(profiles["tab-1"]), "plan", "compat mode keeps the plan axis enabled");
120 eq(Boolean(profiles["tab-1"].pending.collaborationMode), true, "pending plan stays pending until backend acknowledges it");
121
122 profiles = hydrateComposerProfilesFromTabs(profiles, [tab({ mode: "plan", collaborationMode: "plan" })]);
123
124 eq(displayedComposerProfileCollaborationMode(profiles["tab-1"]), "plan", "acknowledged tab hydration keeps plan visible");
125 eq(Boolean(profiles["tab-1"].pending.collaborationMode), false, "backend acknowledgement clears pending plan");
126 }
127
128 {
129 let profiles: ComposerProfilesByTab = {};
130 profiles = hydrateComposerProfilesFromTabs(profiles, [tab()]);
131 profiles = patchComposerProfile(
132 profiles,
133 "tab-1",
134 profiles["tab-1"],
135 { collaborationMode: "normal", goalDraftMode: false, goal: "", toolApprovalMode: "workspace-write" },
136 ["toolApprovalMode"],
137 );
138 profiles = hydrateComposerProfileFromMeta(profiles, "tab-1", meta({ toolApprovalMode: "ask" }));
139
140 eq(profiles["tab-1"].toolApprovalMode, "workspace-write", "stale meta cannot erase a pending workspace-write selection");
141 eq(Boolean(profiles["tab-1"].pending.toolApprovalMode), true, "workspace-write selection stays pending while meta is stale");
142
143 profiles = hydrateComposerProfileFromMeta(profiles, "tab-1", meta({ toolApprovalMode: "auto" }));
144
145 eq(profiles["tab-1"].toolApprovalMode, "workspace-write", "acknowledged workspace-write remains enabled");
146 eq(Boolean(profiles["tab-1"].pending.toolApprovalMode), false, "workspace-write pending clears after matching meta");
147 }
148
149 {
150 let profiles: ComposerProfilesByTab = {};
151 profiles = hydrateComposerProfilesFromTabs(profiles, [tab()]);
152 profiles = patchComposerProfile(
153 profiles,
154 "tab-1",
155 profiles["tab-1"],
156 { collaborationMode: "normal", goalDraftMode: true, goal: "" },
157 ["collaborationMode", "goal"],
158 );
159 profiles = hydrateComposerProfilesFromTabs(profiles, [tab()]);
160
161 eq(displayedComposerProfileCollaborationMode(profiles["tab-1"]), "goal", "empty goal draft remains visible through stale tab hydration");
162 eq(controllerComposerProfileCollaborationMode(profiles["tab-1"]), "normal", "empty goal draft syncs to controller as normal");
163 eq(composerProfileMode(profiles["tab-1"]), "normal", "empty goal draft does not enable plan compatibility mode");
164 }
165
166 {
167 let profiles: ComposerProfilesByTab = {};
168 profiles = hydrateComposerProfilesFromTabs(profiles, [tab(), tab({ id: "tab-2" })]);
169 profiles = patchComposerProfile(profiles, "tab-2", profiles["tab-2"], { toolApprovalMode: "workspace-write" }, ["toolApprovalMode"]);
170 profiles = hydrateComposerProfilesFromTabs(profiles, [tab()]);
171
172 eq(Boolean(profiles["tab-2"]), false, "tab hydration removes profiles for closed tabs");
173 }
174
175 {
176 let profiles: ComposerProfilesByTab = {};
177 profiles = hydrateComposerProfilesFromTabs(profiles, [tab({ toolApprovalMode: "auto" })]);
178 profiles = hydrateComposerProfilesFromTabs(profiles, [tab({ toolApprovalMode: "" })]);
179
180 eq(profiles["tab-1"].toolApprovalMode, "workspace-write", "blank tab payload does not demote workspace write to read only");
181
182 profiles = hydrateComposerProfileFromMeta(profiles, "tab-1", meta({ toolApprovalMode: "" }));
183 eq(profiles["tab-1"].toolApprovalMode, "workspace-write", "blank meta payload does not demote workspace write to read only");
184 }
185
186 {
187 let intents: UserPlanModeIntents = {};
188 intents = updateUserPlanModeIntent(intents, "tab-1", true);
189 intents = updateUserPlanModeIntent(intents, "tab-2", false);
190
191 eq(shouldRestoreUserPlanMode(intents, "tab-1"), true, "manual plan intent restores only the tab that enabled it");
192 eq(shouldRestoreUserPlanMode(intents, "tab-2"), false, "normal tabs do not inherit another tab's plan intent");
193
194 intents = updateUserPlanModeIntent(intents, "tab-1", false);
195 eq(shouldRestoreUserPlanMode(intents, "tab-1"), false, "manual normal mode clears plan restore intent");
196 }
197
198 {
199 let intents: UserPlanModeIntents = {};
200 intents = updateUserPlanModeIntent(intents, "tab-1", true);
201 intents = updateUserPlanModeIntent(intents, "tab-2", true);
202 intents = pruneUserPlanModeIntents(intents, ["tab-2"]);
203
204 eq(shouldRestoreUserPlanMode(intents, "tab-1"), false, "closed tabs lose plan restore intent");
205 eq(shouldRestoreUserPlanMode(intents, "tab-2"), true, "open tabs keep plan restore intent");
206 }
207
208 {
209 eq(resolvePlanRestoreTabId("finished-tab", "active-tab"), "finished-tab", "turn_done plan restore uses the event tab when present");
210 eq(resolvePlanRestoreTabId(undefined, "active-tab"), "active-tab", "turn_done plan restore falls back to the active tab for legacy events");
211 }
212
213 {
214 let intents: UserPlanModeIntents = {};
215 intents = updateUserPlanModeIntent(intents, "tab-1", true);
216
217 eq(
218 shouldRestoreUserPlanModeForProfile(intents, "tab-1", { goal: "research task" }),
219 false,
220 "active goals block remembered plan restoration",
221 );
222 eq(
223 shouldRestoreUserPlanModeForProfile(intents, "tab-1", { goal: "" }),
224 true,
225 "empty goals still allow remembered plan restoration",
226 );
227 }
228
229 console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`);
230 if (failed > 0) process.exit(1);
231
231 lines TYPESCRIPT