| 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 |