返回 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 todoDismissalKey,
29 todoPanelScope,
30 } = await import(moduleUrl);
31
32 const completedTodos = [
33 { content: "Inspect the report", status: "completed" },
34 { content: "Ship the fix", status: "completed" },
35 ];
36 const activeTodos = [
37 { content: "Inspect the report", status: "in_progress" },
38 { content: "Ship the fix", status: "pending" },
39 ];
40
41 assert.deepEqual(
42 resolveTodoPanelTodos([], undefined),
43 [],
44 "an authoritative empty canonical list with no live tool clears the panel",
45 );
46 assert.deepEqual(
47 resolveTodoPanelTodos(
48 [{ content: "Inspect the report", status: "in_progress" }, { content: "Ship the fix", status: "pending" }],
49 [{ content: "Inspect the report", status: "completed" }, { content: "Ship the fix", status: "in_progress" }],
50 ),
51 [{ content: "Inspect the report", status: "completed" }, { content: "Ship the fix", status: "in_progress" }],
52 "a live todo_write snapshot advances mid-turn status past a stale meta snapshot",
53 );
54 assert.deepEqual(
55 resolveTodoPanelTodos(undefined, activeTodos),
56 activeTodos,
57 "an unavailable canonical list falls back to the transcript snapshot",
58 );
59 assert.deepEqual(
60 resolveTodoPanelTodos(activeTodos, undefined),
61 activeTodos,
62 "without a live tool the panel keeps the meta snapshot",
63 );
64 assert.equal(
65 sameTodoList(activeTodos, activeTodos.map((todo) => ({ ...todo }))),
66 true,
67 "equivalent canonical lists do not churn meta state",
68 );
69 assert.equal(
70 sameTodoList(activeTodos, [{ ...activeTodos[0], status: "completed" }, activeTodos[1]]),
71 false,
72 "canonical status changes invalidate meta state",
73 );
74
75 assert.equal(
76 shouldShowTodoPanel("todo-final", null, completedTodos),
77 true,
78 "a completed todo list stays visible in collapsed form until the user dismisses it",
79 );
80 assert.equal(
81 shouldShowTodoPanel("todo-active", null, [{ content: "Run tests", status: "in_progress" }]),
82 true,
83 "an active todo_write remains visible",
84 );
85 assert.equal(
86 shouldShowTodoPanel("todo-final", "todo-final", completedTodos),
87 false,
88 "a user dismissal still hides that exact todo list",
89 );
90 const completedKey = todoDismissalKey(completedTodos);
91 const dismissedBySession = new Set([scopedTodoDismissalKey("session:a", completedKey)]);
92 assert.equal(
93 shouldShowTodoPanel(completedKey, dismissedTodoKeyForScope("session:a", dismissedBySession, completedKey), completedTodos),
94 false,
95 "a completed todo dismissal hides the list in the session where it was closed",
96 );
97 assert.equal(
98 shouldShowTodoPanel(completedKey, dismissedTodoKeyForScope("session:b", dismissedBySession, completedKey), completedTodos),
99 true,
100 "a completed todo dismissal in one session does not hide another session's list",
101 );
102 dismissedBySession.add(scopedTodoDismissalKey("session:b", completedKey));
103 assert.equal(
104 shouldShowTodoPanel(completedKey, dismissedTodoKeyForScope("session:a", dismissedBySession, completedKey), completedTodos),
105 false,
106 "closing another session's todo does not forget the first session dismissal",
107 );
108 assert.equal(
109 todoPanelScope({
110 activeTabId: "tab-a",
111 activeTab: {
112 id: "tab-a",
113 scope: "project",
114 workspaceRoot: "/repo",
115 topicId: "topic-a",
116 sessionPath: "/sessions/a.jsonl",
117 },
118 }),
119 "session:/sessions/a.jsonl",
120 "a restored history session uses its session path as the todo panel scope",
121 );
122 assert.equal(
123 todoPanelScope({
124 activeTabId: "tab-b",
125 activeTab: {
126 id: "tab-a",
127 scope: "project",
128 workspaceRoot: "/repo",
129 topicId: "topic-a",
130 sessionPath: "/sessions/a.jsonl",
131 },
132 eventChannel: "desktop-events",
133 }),
134 "tab:tab-b",
135 "a stale active-tab fallback must not scope dismissal to the previous session",
136 );
137 assert.equal(
138 todoPanelScope({
139 activeTabId: "tab-c",
140 activeTab: {
141 id: "tab-c",
142 scope: "project",
143 workspaceRoot: "/repo",
144 topicId: "topic-a",
145 },
146 }),
147 "tab:tab-c",
148 "an unsaved topic session uses its tab id so sibling sessions do not share todo state",
149 );
150 assert.equal(shouldShowTodoPanel(null, null, completedTodos), false, "no canonical todo item means no panel");
151 assert.equal(shouldShowTodoPanel("todo-empty", null, []), false, "empty todo lists do not render a panel");
152
153 const activeKey = todoDismissalKey(activeTodos);
154 assert.equal(
155 activeKey,
156 todoDismissalKey(activeTodos.map((todo) => ({ ...todo }))),
157 "the same task list keeps a stable dismissal key across restored event ids",
158 );
159 assert.equal(
160 shouldShowTodoPanel(activeKey, activeKey, activeTodos),
161 true,
162 "an incomplete restored todo list must reappear even after a stale local dismissal",
163 );
164 assert.notEqual(
165 activeKey,
166 todoDismissalKey([{ ...activeTodos[0], status: "completed" }, { ...activeTodos[1], status: "in_progress" }]),
167 "real progress produces a fresh dismissal key",
168 );
169 assert.equal(
170 todoBatchKey(activeTodos),
171 todoBatchKey([{ ...activeTodos[0], status: "completed" }, { ...activeTodos[1], status: "in_progress" }]),
172 "status progress stays in the same todo batch",
173 );
174 assert.equal(
175 scopedTodoBatchKey("session:a", todoBatchKey(activeTodos)),
176 scopedTodoBatchKey("session:a", todoBatchKey([{ ...activeTodos[0], status: "completed" }, { ...activeTodos[1], status: "in_progress" }])),
177 "status progress keeps the same scoped open-state key",
178 );
179 assert.notEqual(
180 scopedTodoBatchKey("session:a", todoBatchKey(activeTodos)),
181 scopedTodoBatchKey("session:b", todoBatchKey(activeTodos)),
182 "the same task list in a different session gets isolated open state",
183 );
184 assert.notEqual(
185 todoBatchKey(activeTodos),
186 todoBatchKey([{ content: "Run a different task", status: "in_progress" }]),
187 "new task content creates a new todo batch",
188 );
189 assert.equal(
190 shouldOpenTodoPanelByDefault(),
191 false,
192 "todo batches collapse by default regardless of completion",
193 );
194
195 const iterations = 200_000;
196 const started = performance.now();
197 for (let i = 0; i < iterations; i += 1) {
198 if (shouldShowTodoPanel("todo-perf", "todo-perf", completedTodos)) {
199 throw new Error("unexpected visible todo panel during performance loop");
200 }
201 }
202 const elapsed = performance.now() - started;
203 const perCallUs = (elapsed * 1000) / iterations;
204
205 assert.ok(elapsed < 500, `todo visibility check is too slow: ${elapsed.toFixed(2)} ms`);
206 console.log(
207 `todo visibility checks: ${iterations} calls in ${elapsed.toFixed(2)} ms (${perCallUs.toFixed(3)} us/call)`,
208 );
209
209 lines Plain Text