| 1 | // Run: tsx src/__tests__/checkpoint-turn-event.test.ts |
| 2 | |
| 3 | import { readFileSync } from "node:fs"; |
| 4 | import { dirname, resolve } from "node:path"; |
| 5 | import { fileURLToPath } from "node:url"; |
| 6 | import { createTurnSubmissionId, initialState, reducer } from "../lib/useController"; |
| 7 | |
| 8 | type ReducerState = ReturnType<typeof reducer>; |
| 9 | |
| 10 | let passed = 0; |
| 11 | let failed = 0; |
| 12 | |
| 13 | function eq(actual: unknown, expected: unknown, label: string) { |
| 14 | if (actual === expected) { |
| 15 | process.stdout.write(` PASS ${label}\n`); |
| 16 | passed += 1; |
| 17 | } else { |
| 18 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`); |
| 19 | failed += 1; |
| 20 | } |
| 21 | } |
| 22 | |
| 23 | function submit(state: ReducerState, text: string, seq: number): ReducerState { |
| 24 | return reducer(state, { type: "user", text, seq, submissionId: `s${seq}` }); |
| 25 | } |
| 26 | |
| 27 | function userById(state: ReducerState, id: string) { |
| 28 | const item = state.items.find((candidate) => candidate.kind === "user" && candidate.id === id); |
| 29 | if (item?.kind === "user") return item; |
| 30 | const local = Object.values(state.localSubmissions).find((candidate) => candidate.localId === id); |
| 31 | return local ? { |
| 32 | kind: "user" as const, |
| 33 | id: local.localId, |
| 34 | submissionId: local.submissionId, |
| 35 | text: local.text, |
| 36 | checkpointTurn: local.checkpointTurn, |
| 37 | failed: local.status === "failed", |
| 38 | } : undefined; |
| 39 | } |
| 40 | |
| 41 | function localById(state: ReducerState, id: string) { |
| 42 | return Object.values(state.localSubmissions).find((candidate) => candidate.localId === id); |
| 43 | } |
| 44 | |
| 45 | function checkpoint(state: ReducerState, id: string): number | undefined { |
| 46 | return userById(state, id)?.checkpointTurn; |
| 47 | } |
| 48 | |
| 49 | console.log("\nturn checkpoint submission binding"); |
| 50 | |
| 51 | { |
| 52 | const base = createTurnSubmissionId("tab-a", 0, 0, "runtime-a"); |
| 53 | eq(createTurnSubmissionId("tab-b", 0, 0, "runtime-a") === base, false, "submission correlation is tab-scoped"); |
| 54 | eq(createTurnSubmissionId("tab-a", 1, 0, "runtime-a") === base, false, "submission correlation changes across session reset"); |
| 55 | eq(createTurnSubmissionId("tab-a", 0, 0, "runtime-b") === base, false, "submission correlation changes across runtime epoch"); |
| 56 | eq(createTurnSubmissionId("tab-a", 0, 1, "runtime-a") === base, false, "submission correlation changes across local sequence"); |
| 57 | } |
| 58 | |
| 59 | { |
| 60 | let state = submit(initialState, "turn zero", 0); |
| 61 | eq(userById(state, "u0")?.submissionId, "s0", "render item id and opaque submission correlation stay distinct"); |
| 62 | state = reducer(state, { type: "event", e: { kind: "turn_started" } }); |
| 63 | state = reducer(state, { type: "event", e: { kind: "notice", level: "info", text: "runtime notice" } }); |
| 64 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s0", checkpointTurn: 0 } }); |
| 65 | |
| 66 | eq(checkpoint(state, "u0"), 0, "turn zero is assigned by exact submission id"); |
| 67 | eq(localById(state, "u0")?.settled, true, "TurnDone consumes the local checkpoint authority once"); |
| 68 | eq(state.pendingSubmissionId, undefined, "matching TurnDone confirms the optimistic submit"); |
| 69 | eq(state.items.some((item) => item.kind === "notice" && item.text === "runtime notice"), true, "intervening items cannot change the target"); |
| 70 | } |
| 71 | |
| 72 | { |
| 73 | let state = submit(initialState, "legacy turn", 1); |
| 74 | state = reducer(state, { type: "event", e: { kind: "turn_done", checkpointTurn: 9 } }); |
| 75 | eq(checkpoint(state, "u1"), undefined, "TurnDone without submission id cannot stamp a local user"); |
| 76 | eq(state.pendingSubmissionId, "s1", "id-less TurnDone cannot confirm a pending submission"); |
| 77 | |
| 78 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "missing", checkpointTurn: 10 } }); |
| 79 | eq(checkpoint(state, "u1"), undefined, "unknown submission id cannot rewrite another user"); |
| 80 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s1" } }); |
| 81 | eq(localById(state, "u1")?.settled, true, "matching checkpoint-less TurnDone still settles the correlation"); |
| 82 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s1", checkpointTurn: 11 } }); |
| 83 | eq(checkpoint(state, "u1"), undefined, "duplicate TurnDone cannot backfill a settled checkpoint-less turn"); |
| 84 | } |
| 85 | |
| 86 | { |
| 87 | let state = submit(initialState, "/effort max", 10); |
| 88 | state = reducer(state, { type: "send_confirmed", submissionId: "s10" }); |
| 89 | eq(state.pendingSubmissionId, undefined, "successful local command Promise confirms its exact correlation"); |
| 90 | state = reducer(state, { type: "controller_rebuilt" }); |
| 91 | |
| 92 | state = submit(state, "normal after effort", 11); |
| 93 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s11", checkpointTurn: 1 } }); |
| 94 | eq(checkpoint(state, "u10"), undefined, "management command stays non-rewindable"); |
| 95 | eq(checkpoint(state, "u11"), 1, "the next normal turn maps independently after a no-TurnDone command"); |
| 96 | } |
| 97 | |
| 98 | { |
| 99 | const current = reducer(initialState, { |
| 100 | type: "history_replace", |
| 101 | items: [{ kind: "user", id: "rev-12", text: "new" }], |
| 102 | startTurn: 0, |
| 103 | totalTurns: 1, |
| 104 | hasOlder: false, |
| 105 | revision: 12, |
| 106 | digest: "digest-12", |
| 107 | }); |
| 108 | const stale = reducer(current, { |
| 109 | type: "history_replace", |
| 110 | items: [{ kind: "user", id: "rev-11", text: "stale" }], |
| 111 | startTurn: 0, |
| 112 | totalTurns: 1, |
| 113 | hasOlder: false, |
| 114 | revision: 11, |
| 115 | digest: "digest-11", |
| 116 | }); |
| 117 | eq(stale, current, "an older history revision cannot replace the current transcript projection"); |
| 118 | } |
| 119 | |
| 120 | { |
| 121 | let state = submit(initialState, "activity A", 15); |
| 122 | state = reducer(state, { type: "unsend" }); |
| 123 | state = submit(state, "activity B", 16); |
| 124 | state = reducer(state, { type: "event", e: { kind: "turn_started", submissionId: "s15" } }); |
| 125 | eq(state.pendingSubmissionId, "s16", "old-id runtime activity cannot confirm the new pending user"); |
| 126 | state = reducer(state, { type: "event", e: { kind: "notice", submissionId: "s16", level: "info", text: "admitted" } }); |
| 127 | eq(state.pendingSubmissionId, undefined, "matching non-TurnDone activity confirms the pending user"); |
| 128 | } |
| 129 | |
| 130 | { |
| 131 | let state = submit(initialState, "extension activity", 17); |
| 132 | const extension = { pluginId: "demo", surfaceId: "status", kind: "status" as const, status: { label: "working" } }; |
| 133 | state = reducer(state, { type: "event", e: { kind: "extension_status", extension } }); |
| 134 | eq(state.pendingSubmissionId, "s17", "uncorrelated extension activity cannot confirm a pending user"); |
| 135 | state = reducer(state, { type: "event", e: { kind: "extension_status", submissionId: "s17", extension } }); |
| 136 | eq(state.pendingSubmissionId, undefined, "exactly correlated extension activity confirms the pending user"); |
| 137 | } |
| 138 | |
| 139 | { |
| 140 | let state = submit(initialState, "lost A", 20); |
| 141 | state = reducer(state, { type: "unsend" }); |
| 142 | state = submit(state, "B after lost A", 21); |
| 143 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s21", checkpointTurn: 2 } }); |
| 144 | |
| 145 | eq(checkpoint(state, "u20"), undefined, "a missing A TurnDone does not consume B ownership"); |
| 146 | eq(checkpoint(state, "u21"), 2, "B maps by id even when A TurnDone never arrives"); |
| 147 | } |
| 148 | |
| 149 | { |
| 150 | let state = submit(initialState, "cancelled A", 30); |
| 151 | state = reducer(state, { type: "unsend" }); |
| 152 | state = submit(state, "pending B", 31); |
| 153 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s30", checkpointTurn: 3 } }); |
| 154 | |
| 155 | eq(checkpoint(state, "u30"), 3, "late A TurnDone still maps exact A"); |
| 156 | eq(checkpoint(state, "u31"), undefined, "late A TurnDone cannot stamp B"); |
| 157 | eq(state.pendingSubmissionId, "s31", "late A TurnDone cannot confirm B pending state"); |
| 158 | eq(state.pendingUser, "pending B", "late A TurnDone cannot flush B pending text"); |
| 159 | |
| 160 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s31", checkpointTurn: 4 } }); |
| 161 | eq(checkpoint(state, "u31"), 4, "B later receives its own checkpoint"); |
| 162 | } |
| 163 | |
| 164 | { |
| 165 | let state = submit(initialState, "A before failed B", 40); |
| 166 | state = reducer(state, { type: "unsend" }); |
| 167 | state = submit(state, "failed B", 41); |
| 168 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s40", checkpointTurn: 5 } }); |
| 169 | state = reducer(state, { type: "send_failed", submissionId: "s41", error: "turn already running" }); |
| 170 | state = submit(state, "C after failed B", 42); |
| 171 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s42", checkpointTurn: 6 } }); |
| 172 | |
| 173 | eq(checkpoint(state, "u40"), 5, "A maps before B rejection"); |
| 174 | eq(userById(state, "u41")?.failed, true, "B rejection marks only B"); |
| 175 | eq(checkpoint(state, "u41"), undefined, "failed B remains non-rewindable"); |
| 176 | eq(checkpoint(state, "u42"), 6, "C maps normally after the rejected B gap"); |
| 177 | } |
| 178 | |
| 179 | { |
| 180 | let state = submit(initialState, "rapid A", 50); |
| 181 | state = reducer(state, { type: "unsend" }); |
| 182 | state = submit(state, "rapid B", 51); |
| 183 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s51", checkpointTurn: 8 } }); |
| 184 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s50", checkpointTurn: 7 } }); |
| 185 | |
| 186 | eq(checkpoint(state, "u50"), 7, "out-of-order A completion still maps A"); |
| 187 | eq(checkpoint(state, "u51"), 8, "out-of-order B completion still maps B"); |
| 188 | } |
| 189 | |
| 190 | { |
| 191 | let state = submit(initialState, "!pwd", 60); |
| 192 | state = reducer(state, { type: "event", e: { kind: "turn_done" } }); |
| 193 | eq(checkpoint(state, "u60"), undefined, "shell TurnDone without submission id stays non-rewindable"); |
| 194 | |
| 195 | state = submit(state, "after shell", 61); |
| 196 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s61", checkpointTurn: 9 } }); |
| 197 | eq(checkpoint(state, "u61"), 9, "shell completion cannot offset the following normal turn"); |
| 198 | } |
| 199 | |
| 200 | { |
| 201 | let state = submit(initialState, "provider error", 70); |
| 202 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s70", checkpointTurn: 10, err: "provider unavailable" } }); |
| 203 | eq(checkpoint(state, "u70"), 10, "error TurnDone maps an admitted checkpoint by id"); |
| 204 | eq(state.items.some((item) => item.kind === "notice" && item.text === "provider unavailable"), true, "error TurnDone still surfaces its warning"); |
| 205 | } |
| 206 | |
| 207 | { |
| 208 | let state = submit(initialState, "same text", 80); |
| 209 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s80", checkpointTurn: 11 } }); |
| 210 | state = submit(state, "same text", 81); |
| 211 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s81", checkpointTurn: 12 } }); |
| 212 | eq(checkpoint(state, "u80"), 11, "first equal-text user keeps its checkpoint"); |
| 213 | eq(checkpoint(state, "u81"), 12, "second equal-text user maps independently by id"); |
| 214 | } |
| 215 | |
| 216 | { |
| 217 | let state = submit(initialState, "old pending", 90); |
| 218 | state = reducer(state, { type: "unsend" }); |
| 219 | state = submit(state, "new pending", 91); |
| 220 | const unchanged = state; |
| 221 | state = reducer(state, { type: "send_failed", submissionId: "s90", error: "late old rejection" }); |
| 222 | eq(state.pendingSubmissionId, unchanged.pendingSubmissionId, "old rejection preserves the newer pending id"); |
| 223 | eq(state.running, unchanged.running, "old rejection preserves the newer runtime"); |
| 224 | eq(state.localSubmissions.s91, unchanged.localSubmissions.s91, "old rejection leaves the newer echo unchanged"); |
| 225 | eq(userById(state, "u90")?.failed, true, "old rejection settles only its own retained echo"); |
| 226 | state = reducer(state, { type: "send_failed", submissionId: "s91", error: "current rejection" }); |
| 227 | eq(userById(state, "u91")?.failed, true, "current send_failed marks the exact pending user"); |
| 228 | eq(userById(state, "u91")?.submissionId, "s91", "send_failed preserves durable identity for a late canonical receipt"); |
| 229 | eq(userById(state, "u90")?.failed, true, "current rejection preserves the older echo status"); |
| 230 | } |
| 231 | |
| 232 | { |
| 233 | let state = submit(initialState, "confirmed delivery", 100); |
| 234 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s100", checkpointTurn: 13 } }); |
| 235 | const confirmed = state; |
| 236 | state = reducer(state, { type: "send_failed", submissionId: "s100", error: "late bridge rejection" }); |
| 237 | eq(state, confirmed, "send_failed after matching TurnDone is a complete no-op"); |
| 238 | eq(checkpoint(state, "u100"), 13, "late failure cannot erase an assigned checkpoint"); |
| 239 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s100", checkpointTurn: 99 } }); |
| 240 | eq(checkpoint(state, "u100"), 13, "duplicate TurnDone cannot overwrite an existing checkpoint"); |
| 241 | } |
| 242 | |
| 243 | { |
| 244 | let state = submit(initialState, "reset prompt", 110); |
| 245 | state = reducer(state, { type: "reset" }); |
| 246 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s110", checkpointTurn: 14 } }); |
| 247 | eq(state.items.length, 0, "session reset removes the old optimistic id target"); |
| 248 | |
| 249 | state = submit(state, "new runtime prompt", 111); |
| 250 | state = reducer(state, { type: "controller_rebuilt" }); |
| 251 | eq(localById(state, "u111")?.settled, true, "controller rebuild retires the old checkpoint authority"); |
| 252 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s111", checkpointTurn: 99 } }); |
| 253 | eq(checkpoint(state, "u111"), undefined, "late pre-rebuild TurnDone cannot stamp the retired item"); |
| 254 | state = submit(state, "post-rebuild prompt", 112); |
| 255 | state = reducer(state, { type: "send_failed", submissionId: "s111", error: "old runtime rejection" }); |
| 256 | eq(state.pendingSubmissionId, "s112", "old-runtime failure cannot clear the rebuilt runtime pending correlation"); |
| 257 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s112", checkpointTurn: 15 } }); |
| 258 | eq(checkpoint(state, "u112"), 15, "post-rebuild submission maps normally"); |
| 259 | } |
| 260 | |
| 261 | { |
| 262 | let state = reducer(initialState, { |
| 263 | type: "history_page", |
| 264 | mode: "replace", |
| 265 | page: { messages: [{ role: "user", content: "recent", checkpointTurn: 60 }], startTurn: 60, endTurn: 61, totalTurns: 61, hasOlder: true }, |
| 266 | }); |
| 267 | state = submit(state, "live after pagination", 120); |
| 268 | state = reducer(state, { |
| 269 | type: "history_page", |
| 270 | mode: "prepend", |
| 271 | page: { messages: [{ role: "user", content: "older", checkpointTurn: 5 }], startTurn: 5, endTurn: 6, totalTurns: 61, hasOlder: true }, |
| 272 | }); |
| 273 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "s120", checkpointTurn: 61 } }); |
| 274 | eq(checkpoint(state, "u120"), 61, "history prepend preserves the exact live item target"); |
| 275 | eq(state.items.some((item) => item.kind === "user" && item.text === "recent" && item.checkpointTurn === 60), true, "recent history keeps authoritative metadata"); |
| 276 | eq(state.items.some((item) => item.kind === "user" && item.text === "older" && item.checkpointTurn === 5), true, "prepended history keeps authoritative metadata"); |
| 277 | } |
| 278 | |
| 279 | { |
| 280 | let state = reducer(initialState, { |
| 281 | type: "history_page", |
| 282 | mode: "replace", |
| 283 | page: { messages: [{ role: "user", content: "authoritative", checkpointTurn: 7 }], startTurn: 7, endTurn: 8, totalTurns: 8, hasOlder: false }, |
| 284 | }); |
| 285 | state = reducer(state, { type: "event", e: { kind: "turn_done", submissionId: "u999", checkpointTurn: 99 } }); |
| 286 | eq(state.items[0].kind === "user" && state.items[0].checkpointTurn, 7, "unknown local id cannot rewrite hydrated history"); |
| 287 | } |
| 288 | |
| 289 | { |
| 290 | const source = readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), "../lib/useController.ts"), "utf8"); |
| 291 | eq(source.includes("turnUserItemIds"), false, "FIFO ownership state is completely removed"); |
| 292 | eq(source.includes("HistoryCheckpointTurnsForTab(targetTabId)"), false, "TurnDone hot path does not refresh full checkpoint history"); |
| 293 | eq(source.includes('type: "history_checkpoint_turns"'), false, "positional checkpoint merge action remains removed"); |
| 294 | eq(source.includes("void refreshTurnBoundaries(targetTabId)"), true, "TurnDone still refreshes checkpoint metadata"); |
| 295 | } |
| 296 | |
| 297 | console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`); |
| 298 | if (failed > 0) process.exit(1); |
| 299 |