返回 DeepSeek-Reasonix
test-todo-visibility.mjs
根目录 / desktop / frontend / scripts / test-todo-visibility.mjs
1 import assert from "node:assert/strict";
2 import { readFileSync } from "node:fs";
3 import path from "node:path";
4 import { performance } from "node:perf_hooks";
5 import { fileURLToPath } from "node:url";
6 import ts from "typescript";
7
8 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
9 const sourcePath = path.join(root, "src", "lib", "todoVisibility.ts");
10 const source = readFileSync(sourcePath, "utf8");
11 const transpiled = ts.transpileModule(source, {
12 compilerOptions: {
13 module: ts.ModuleKind.ES2022,
14 target: ts.ScriptTarget.ES2022,
15 },
16 }).outputText;
17
18 const moduleUrl = `data:text/javascript;base64,${Buffer.from(transpiled).toString("base64")}`;
19 const {
20 dismissedTodoKeyForScope,
21 resolveTodoPanelTodos,
22 sameTodoList,
23 scopedTodoBatchKey,
24 scopedTodoDismissalKey,
25 shouldOpenTodoPanelByDefault,
26 shouldShowTodoPanel,
27 todoBatchKey,
28 todoContinueTarget,
29 todoDismissalKey,
30 todoPanelScope,
31 todoPresentationStatus,
32 } = await import(moduleUrl);
33
34 const completedTodos = [
35 { content: "Inspect the report", status: "completed" },
36 { content: "Ship the fix", status: "completed" },
37 ];
38 const activeTodos = [
39 { content: "Inspect the report", status: "in_progress" },
40 { content: "Ship the fix", status: "pending" },
41 ];
42
43 assert.equal(todoPresentationStatus("in_progress", { running: true, pendingPrompt: false }), "in_progress", "an active turn renders its current todo as in progress");
44 assert.equal(todoPresentationStatus("in_progress", { running: false, pendingPrompt: false }), "paused", "an idle or restored turn renders a stale current todo as ready to continue");
45 assert.equal(todoPresentationStatus("in_progress", { running: true, pendingPrompt: true }), "waiting", "a prompt-blocked turn renders its current todo as waiting for the user");
46 assert.equal(todoPresentationStatus("completed", { running: false, pendingPrompt: false }), "completed", "completed todos remain completed");
47 assert.equal(todoContinueTarget("tab-a", "tab-a", { ready: true, running: false, pendingPrompt: false }), "tab-a", "continue targets the exact idle visible tab");
48 assert.equal(todoContinueTarget("tab-a", "tab-b", { ready: true, running: false, pendingPrompt: false }), null, "a rapid tab switch prevents stale todo routing");
49 assert.equal(todoContinueTarget("tab-a", "tab-a", { ready: true, running: true, pendingPrompt: false }), null, "running turns cannot receive a duplicate continue submission");
50 assert.equal(todoContinueTarget("tab-a", "tab-a", { ready: true, running: false, pendingPrompt: true }), null, "pending prompts must be answered instead of bypassed by continue");
51
52 assert.deepEqual(
53 resolveTodoPanelTodos([], undefined),
54 [],
55 "an authoritative empty canonical list with no live tool clears the panel",
56 );
57 assert.deepEqual(
58 resolveTodoPanelTodos(
59 [{ content: "Inspect the report", status: "in_progress" }, { content: "Ship the fix", status: "pending" }],
60 [{ content: "Inspect the report", status: "completed" }, { content: "Ship the fix", status: "in_progress" }],
61 ),
62 [{ content: "Inspect the report", status: "in_progress" }, { content: "Ship the fix", status: "pending" }],
63 "the authoritative runtime snapshot wins over transcript tool-card data",
64 );
65 assert.deepEqual(
66 resolveTodoPanelTodos(undefined, activeTodos),
67 [],
68 "an unavailable canonical list never falls back to transcript tool-card data",
69 );
70 assert.deepEqual(
71 resolveTodoPanelTodos(activeTodos, undefined),
72 activeTodos,
73 "without a live tool the panel keeps the meta snapshot",
74 );
75 assert.equal(
76 sameTodoList(activeTodos, activeTodos.map((todo) => ({ ...todo }))),
77 true,
78 "equivalent canonical lists do not churn meta state",
79 );
80 assert.equal(
81 sameTodoList(activeTodos, [{ ...activeTodos[0], status: "completed" }, activeTodos[1]]),
82 false,
83 "canonical status changes invalidate meta state",
84 );
85
86 assert.equal(
87 shouldShowTodoPanel("todo-final", null, completedTodos),
88 true,
89 "a completed batch remains mounted so TodoPanel can distinguish restore from a live transition",
90 );
91 assert.equal(
92 shouldShowTodoPanel("todo-active", null, [{ content: "Run tests", status: "in_progress" }]),
93 true,
94 "an active todo_write remains visible",
95 );
96 assert.equal(
97 shouldShowTodoPanel("todo-final", "todo-final", completedTodos),
98 false,
99 "a user dismissal still hides that exact todo list",
100 );
101 const completedKey = todoDismissalKey(completedTodos);
102 const dismissedBySession = new Set([scopedTodoDismissalKey("session:a", completedKey)]);
103 assert.equal(
104 shouldShowTodoPanel(completedKey, dismissedTodoKeyForScope("session:a", dismissedBySession, completedKey), completedTodos),
105 false,
106 "a completed todo dismissal hides the list in the session where it was closed",
107 );
108 assert.equal(
109 shouldShowTodoPanel(completedKey, dismissedTodoKeyForScope("session:b", dismissedBySession, completedKey), completedTodos),
110 true,
111 "a completed todo dismissal in one session does not hide another session's list",
112 );
113 dismissedBySession.add(scopedTodoDismissalKey("session:b", completedKey));
114 assert.equal(
115 shouldShowTodoPanel(completedKey, dismissedTodoKeyForScope("session:a", dismissedBySession, completedKey), completedTodos),
116 false,
117 "closing another session's todo does not forget the first session dismissal",
118 );
119 assert.equal(
120 todoPanelScope({
121 activeTabId: "tab-a",
122 activeTab: {
123 id: "tab-a",
124 scope: "project",
125 workspaceRoot: "/repo",
126 topicId: "topic-a",
127 sessionPath: "/sessions/a.jsonl",
128 },
129 }),
130 "session:/sessions/a.jsonl",
131 "a restored history session uses its session path as the todo panel scope",
132 );
133 assert.equal(
134 todoPanelScope({
135 activeTabId: "tab-b",
136 activeTab: {
137 id: "tab-a",
138 scope: "project",
139 workspaceRoot: "/repo",
140 topicId: "topic-a",
141 sessionPath: "/sessions/a.jsonl",
142 },
143 eventChannel: "desktop-events",
144 }),
145 "tab:tab-b",
146 "a stale active-tab fallback must not scope dismissal to the previous session",
147 );
148 assert.equal(
149 todoPanelScope({
150 activeTabId: "tab-c",
151 activeTab: {
152 id: "tab-c",
153 scope: "project",
154 workspaceRoot: "/repo",
155 topicId: "topic-a",
156 },
157 }),
158 "tab:tab-c",
159 "an unsaved topic session uses its tab id so sibling sessions do not share todo state",
160 );
161 assert.equal(shouldShowTodoPanel(null, null, completedTodos), false, "no canonical todo item means no panel");
162 assert.equal(shouldShowTodoPanel("todo-empty", null, []), false, "empty todo lists do not render a panel");
163
164 const activeKey = todoDismissalKey(activeTodos);
165 assert.equal(
166 activeKey,
167 todoDismissalKey(activeTodos.map((todo) => ({ ...todo }))),
168 "the same task list keeps a stable dismissal key across restored event ids",
169 );
170 assert.equal(
171 shouldShowTodoPanel(activeKey, activeKey, activeTodos),
172 true,
173 "an incomplete restored todo list must reappear even after a stale local dismissal",
174 );
175 assert.equal(
176 shouldShowTodoPanel(activeKey, null, activeTodos),
177 true,
178 "a local dismissal cannot hide unfinished work",
179 );
180 assert.equal(
181 shouldShowTodoPanel(completedKey, null, completedTodos),
182 true,
183 "a completed shelf remains visible until the next host turn clears it",
184 );
185 assert.notEqual(
186 activeKey,
187 todoDismissalKey([{ ...activeTodos[0], status: "completed" }, { ...activeTodos[1], status: "in_progress" }]),
188 "real progress produces a fresh dismissal key",
189 );
190 assert.equal(
191 todoBatchKey(activeTodos),
192 todoBatchKey([{ ...activeTodos[0], status: "completed" }, { ...activeTodos[1], status: "in_progress" }]),
193 "status progress stays in the same todo batch",
194 );
195 assert.equal(
196 scopedTodoBatchKey("session:a", todoBatchKey(activeTodos)),
197 scopedTodoBatchKey("session:a", todoBatchKey([{ ...activeTodos[0], status: "completed" }, { ...activeTodos[1], status: "in_progress" }])),
198 "status progress keeps the same scoped open-state key",
199 );
200 assert.notEqual(
201 scopedTodoBatchKey("session:a", todoBatchKey(activeTodos)),
202 scopedTodoBatchKey("session:b", todoBatchKey(activeTodos)),
203 "the same task list in a different session gets isolated open state",
204 );
205 assert.notEqual(
206 todoBatchKey(activeTodos),
207 todoBatchKey([{ content: "Run a different task", status: "in_progress" }]),
208 "new task content creates a new todo batch",
209 );
210 assert.equal(
211 shouldOpenTodoPanelByDefault(),
212 false,
213 "todo batches collapse by default regardless of completion",
214 );
215
216 const iterations = 200_000;
217 const started = performance.now();
218 for (let i = 0; i < iterations; i += 1) {
219 if (shouldShowTodoPanel("todo-perf", "todo-perf", completedTodos)) {
220 throw new Error("unexpected visible todo panel during performance loop");
221 }
222 }
223 const elapsed = performance.now() - started;
224 const perCallUs = (elapsed * 1000) / iterations;
225
226 assert.ok(elapsed < 500, `todo visibility check is too slow: ${elapsed.toFixed(2)} ms`);
227 console.log(
228 `todo visibility checks: ${iterations} calls in ${elapsed.toFixed(2)} ms (${perCallUs.toFixed(3)} us/call)`,
229 );
230
230 lines Plain Text