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