| 1 | // Run: tsx src/__tests__/use-controller-stream-progress.test.ts |
| 2 | // |
| 3 | // Covers the delivery-mode liveness fixes: |
| 4 | // 1. Partial tool dispatches upsert a running card (instead of being dropped) |
| 5 | // and carry streaming argChars progress. |
| 6 | // 2. usageSeq bumps on every usage event regardless of source, so the right |
| 7 | // panel keeps refreshing during sub-agent runs. |
| 8 | // 3. A mid-turn context snapshot reporting used=0 does not collapse a gauge |
| 9 | // that already shows real usage. |
| 10 | // 4. A retry event repairs stale idle snapshots in either delivery order so |
| 11 | // the turn remains stoppable. |
| 12 | // 5. TPS telemetry accumulates provider-output intervals without tool gaps and |
| 13 | // survives both missing usage and missing turn_done events. |
| 14 | |
| 15 | import { initialState, promptEventClock, reducer } from "../lib/useController"; |
| 16 | import type { WireEvent } from "../lib/types"; |
| 17 | |
| 18 | let passed = 0; |
| 19 | let failed = 0; |
| 20 | |
| 21 | function eq(a: unknown, b: unknown, label: string) { |
| 22 | if (a === b) { |
| 23 | process.stdout.write(` PASS ${label}\n`); |
| 24 | passed += 1; |
| 25 | } else { |
| 26 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`); |
| 27 | failed += 1; |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | function ev(s: typeof initialState, e: WireEvent) { |
| 32 | return reducer(s, { type: "event", e }); |
| 33 | } |
| 34 | |
| 35 | // Completed answer chrome receives the full UI-turn aggregate, including |
| 36 | // auxiliary requests, while the provider-speed calculation stays executor-only. |
| 37 | { |
| 38 | let s = { ...initialState, running: true, turnActive: true, turnStartAt: Date.now() - 29_000 }; |
| 39 | s = ev(s, { kind: "text", messageId: "usage-answer", text: "answer" } as WireEvent); |
| 40 | s = ev(s, { kind: "usage", usage: { |
| 41 | promptTokens: 100, completionTokens: 20, totalTokens: 120, cacheHitTokens: 80, cacheMissTokens: 20, |
| 42 | reasoningTokens: 10, costQuote: { modelRef: "deepseek-official/deepseek-flash" }, |
| 43 | } } as WireEvent); |
| 44 | s = ev(s, { kind: "usage", usage: { |
| 45 | promptTokens: 50, completionTokens: 5, totalTokens: 55, cacheHitTokens: 40, cacheMissTokens: 10, |
| 46 | source: "subagent", costQuote: { modelRef: "deepseek-official/deepseek-flash" }, |
| 47 | } } as WireEvent); |
| 48 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 49 | const answer = [...s.items].reverse().find(item => item.kind === "assistant"); |
| 50 | eq(answer?.kind === "assistant" ? answer.turnUsage?.totalTokens : 0, 175, "turn footer aggregates all usage events"); |
| 51 | eq(answer?.kind === "assistant" ? answer.turnUsage?.cacheReadTokens : 0, 120, "turn footer aggregates cache reads"); |
| 52 | eq(answer?.kind === "assistant" ? answer.turnUsage?.routes?.join(",") : "", "deepseek-official/deepseek-flash", "turn footer deduplicates model routes"); |
| 53 | eq(answer?.kind === "assistant" ? Boolean(answer.createdAt) : false, true, "completed answer receives its display timestamp"); |
| 54 | eq(answer?.kind === "assistant" ? (answer.turnDurationMs ?? 0) >= 29_000 : false, true, "completed answer receives wall-clock turn duration"); |
| 55 | } |
| 56 | |
| 57 | // Desktop keeps ordinary completion receipts off the transcript, but retains |
| 58 | // details for the change panel and surfaces actionable gaps as a short notice. |
| 59 | { |
| 60 | const before = { |
| 61 | ...initialState, |
| 62 | seq: 2, |
| 63 | items: [{ kind: "user" as const, id: "u1", text: "update it" }], |
| 64 | }; |
| 65 | const complete = ev(before, { |
| 66 | kind: "completion_summary", |
| 67 | completion: { |
| 68 | preset: "balanced", |
| 69 | verdict: "complete", |
| 70 | mutations: 3, |
| 71 | checks_passed: 12, |
| 72 | checks_failed: 0, |
| 73 | checks_suppressed: 0, |
| 74 | review: "passed", |
| 75 | gap_kinds: [], |
| 76 | constraint_degraded: false, |
| 77 | floor: "standard", |
| 78 | attention: false, |
| 79 | }, |
| 80 | }); |
| 81 | eq(complete.items.length, before.items.length + 1, "workspace mutations add a neutral change notice"); |
| 82 | const changeNotice = complete.items[complete.items.length - 1]; |
| 83 | eq(changeNotice?.kind === "notice" ? changeNotice.level : "", "info", "workspace mutations use an info notice, not a warning"); |
| 84 | eq(changeNotice?.kind === "notice" ? changeNotice.completionSummary : undefined, complete.completionSummary, "neutral change notice retains its own normalized summary"); |
| 85 | eq(complete.completionSummary?.preset, "balanced", "ordinary completion summary remains available to the change panel"); |
| 86 | |
| 87 | const after = ev(complete, { |
| 88 | kind: "completion_summary", |
| 89 | completion: { |
| 90 | preset: "balanced", |
| 91 | verdict: "partial", |
| 92 | mutations: 3, |
| 93 | checks_passed: 12, |
| 94 | checks_failed: 1, |
| 95 | checks_suppressed: 2, |
| 96 | review: "passed", |
| 97 | gap_kinds: ["stale_check"], |
| 98 | constraint_degraded: true, |
| 99 | floor: "delivery", |
| 100 | attention: true, |
| 101 | }, |
| 102 | }); |
| 103 | eq(after.items.length, complete.items.length, "summary refresh replaces the same result notice"); |
| 104 | const notice = after.items[after.items.length - 1]; |
| 105 | eq(notice?.kind === "notice" ? notice.variant : "", "completion", "quality gap uses the completion notice variant"); |
| 106 | eq(notice?.kind === "notice" ? notice.action : "", "open_changes", "quality gap links to the change panel"); |
| 107 | eq(notice?.kind === "notice" ? notice.completionSummary : undefined, after.completionSummary, "completion notice retains its own normalized summary"); |
| 108 | eq(notice?.kind === "notice" ? notice.text.includes("balanced") : true, false, "compact notice does not expose internal preset values"); |
| 109 | eq(after.completionSummary?.checks_failed, 1, "actionable completion summary is retained for details"); |
| 110 | |
| 111 | const switchedFloor = ev({ |
| 112 | ...complete, |
| 113 | meta: { label: "test", ready: true, eventChannel: "", cwd: "/repo", qualityFloor: "delivery" }, |
| 114 | }, { |
| 115 | kind: "completion_summary", |
| 116 | completion: { |
| 117 | ...complete.completionSummary!, |
| 118 | verdict: "partial", |
| 119 | gap_kinds: ["unverified_change"], |
| 120 | floor: "standard", |
| 121 | attention: false, |
| 122 | }, |
| 123 | }); |
| 124 | const switchedNotice = switchedFloor.items[switchedFloor.items.length - 1]; |
| 125 | eq(switchedNotice?.kind === "notice" ? switchedNotice.level : "", "info", "turn-time standard summary stays neutral after switching to delivery"); |
| 126 | |
| 127 | const suppressed = ev(complete, { |
| 128 | kind: "completion_summary", |
| 129 | completion: { |
| 130 | ...complete.completionSummary!, |
| 131 | mutations: 0, |
| 132 | checks_suppressed: 1, |
| 133 | gap_kinds: ["suppressed_requirement"], |
| 134 | floor: "delivery", |
| 135 | attention: true, |
| 136 | }, |
| 137 | }); |
| 138 | const suppressedNotice = suppressed.items[suppressed.items.length - 1]; |
| 139 | eq(suppressedNotice?.kind === "notice" ? suppressedNotice.title : "", "Turn result", "required suppression retains the fixed result title"); |
| 140 | |
| 141 | const restarted = ev(after, { kind: "turn_started" }); |
| 142 | eq(restarted.completionSummary, undefined, "a new turn clears the previous turn's quality details"); |
| 143 | } |
| 144 | |
| 145 | // --- 1. partial dispatch upserts a running card with argChars --- |
| 146 | { |
| 147 | let s = { ...initialState, running: true, turnActive: true }; |
| 148 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "write_file", readOnly: false, partial: true } } as WireEvent); |
| 149 | const card = s.items.find((it) => it.kind === "tool" && it.id === "c1"); |
| 150 | eq(Boolean(card), true, "partial dispatch creates a running tool card"); |
| 151 | eq(card?.kind === "tool" ? card.status : "", "running", "partial card is running"); |
| 152 | eq(card?.kind === "tool" ? card.args : "x", "", "partial card has no args yet"); |
| 153 | |
| 154 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "write_file", readOnly: false, partial: true, argChars: 8192 } } as WireEvent); |
| 155 | const card2 = s.items.find((it) => it.kind === "tool" && it.id === "c1"); |
| 156 | eq(card2?.kind === "tool" ? card2.argChars : 0, 8192, "arg progress updates the card"); |
| 157 | eq(s.turnArgChars, 8192, "turnArgChars mirrors streaming progress"); |
| 158 | |
| 159 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "write_file", args: '{"path":"a"}', readOnly: false } } as WireEvent); |
| 160 | const card3 = s.items.find((it) => it.kind === "tool" && it.id === "c1"); |
| 161 | eq(card3?.kind === "tool" ? card3.args : "", '{"path":"a"}', "full dispatch merges args into the same card"); |
| 162 | eq(card3?.kind === "tool" ? card3.argChars : 1, undefined, "full dispatch clears argChars"); |
| 163 | eq( |
| 164 | s.items.filter((it) => it.kind === "tool" && it.id === "c1").length, |
| 165 | 1, |
| 166 | "partial + full dispatch never duplicate the card", |
| 167 | ); |
| 168 | |
| 169 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "write_file", args: '{"path":"a"}', readOnly: false, refreshed: true, diff: "@@ -1 +1 @@\n-old\n+new\n", added: 1, removed: 1 } } as WireEvent); |
| 170 | const refreshed = s.items.find((it) => it.kind === "tool" && it.id === "c1"); |
| 171 | eq(refreshed?.kind === "tool" ? refreshed.fileDiff?.diff : "", "@@ -1 +1 @@\n-old\n+new\n", "same-ID refresh replaces the live preview"); |
| 172 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "c1").length, 1, "preview refresh never duplicates the card"); |
| 173 | |
| 174 | s = ev(s, { kind: "tool_dispatch", tool: { |
| 175 | id: "c1", |
| 176 | name: "write_file", |
| 177 | args: '{"path":"a"}', |
| 178 | readOnly: false, |
| 179 | refreshed: true, |
| 180 | resolvedName: "mcp__db__write", |
| 181 | capabilityId: "mcp-tool:db/write", |
| 182 | } } as WireEvent); |
| 183 | const resolved = s.items.find((it) => it.kind === "tool" && it.id === "c1"); |
| 184 | eq(resolved?.kind === "tool" ? resolved.resolvedName : "", "mcp__db__write", "same-ID refresh stores resolved target"); |
| 185 | eq(resolved?.kind === "tool" ? resolved.capabilityId : "", "mcp-tool:db/write", "same-ID refresh stores capability id"); |
| 186 | eq(resolved?.kind === "tool" ? resolved.readOnly : true, false, "same-ID refresh replaces proxy read-only classification"); |
| 187 | |
| 188 | s = ev(s, { kind: "tool_result", tool: { |
| 189 | id: "c1", |
| 190 | name: "use_capability", |
| 191 | args: '{"action":"call","capability_id":"mcp-tool:db/write-v2"}', |
| 192 | readOnly: false, |
| 193 | resolvedName: "mcp__db__write_v2", |
| 194 | capabilityId: "mcp-tool:db/write-v2", |
| 195 | output: "done", |
| 196 | } } as WireEvent); |
| 197 | const completed = s.items.find((it) => it.kind === "tool" && it.id === "c1"); |
| 198 | eq(completed?.kind === "tool" ? completed.resolvedName : "", "mcp__db__write_v2", "tool result also refreshes resolved target"); |
| 199 | eq(completed?.kind === "tool" ? completed.capabilityId : "", "mcp-tool:db/write-v2", "tool result also refreshes capability id"); |
| 200 | eq(completed?.kind === "tool" ? completed.readOnly : true, false, "tool result preserves resolved writer classification"); |
| 201 | |
| 202 | s = ev(s, { kind: "usage", usage: { promptTokens: 100, completionTokens: 50, totalTokens: 150, cacheHitTokens: 0, cacheMissTokens: 0 } } as WireEvent); |
| 203 | eq(s.turnArgChars, 0, "usage event resets the streaming estimate"); |
| 204 | } |
| 205 | |
| 206 | // --- 1b. partial dispatch without an ID never creates an orphan card --- |
| 207 | { |
| 208 | let s = { ...initialState, running: true, turnActive: true }; |
| 209 | // OpenAI-compatible streams can surface the name before the call ID. |
| 210 | s = ev(s, { kind: "tool_dispatch", tool: { name: "write_file", readOnly: false, partial: true, argChars: 2048 } } as WireEvent); |
| 211 | eq(s.items.filter((it) => it.kind === "tool").length, 0, "id-less partial creates no card"); |
| 212 | eq(s.turnArgChars, 2048, "id-less partial still counts streaming progress"); |
| 213 | |
| 214 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "write_file", readOnly: false, partial: true, argChars: 4096 } } as WireEvent); |
| 215 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "write_file", args: '{"path":"a"}', readOnly: false } } as WireEvent); |
| 216 | eq(s.items.filter((it) => it.kind === "tool").length, 1, "late ID yields exactly one card, no orphan"); |
| 217 | const only = s.items.find((it) => it.kind === "tool"); |
| 218 | eq(only?.kind === "tool" ? only.id : "", "c1", "surviving card carries the real call ID"); |
| 219 | } |
| 220 | |
| 221 | // --- 1c. stream_attempt discard rolls back partial tool cards and text --- |
| 222 | { |
| 223 | let s = { ...initialState, running: true, turnActive: true }; |
| 224 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "sa-1", action: "begin", attempt: 1, max: 6 } } as WireEvent); |
| 225 | s = ev(s, { kind: "text", text: "partial half" } as WireEvent); |
| 226 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c1", name: "edit_file", readOnly: false, partial: true, argChars: 6000, attemptId: "sa-1" } } as WireEvent); |
| 227 | // Concurrent background sub-agent tool must not be journaled. |
| 228 | s = ev(s, { kind: "tool_dispatch", tool: { id: "child-1", name: "read_file", readOnly: true, partial: true, parentId: "task-1", attemptId: "sa-1" } } as WireEvent); |
| 229 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "c1").length, 1, "partial edit_file card appears during attempt"); |
| 230 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "child-1").length, 1, "sub-agent partial is still shown"); |
| 231 | eq(s.live?.text, "partial half", "partial text is live during attempt"); |
| 232 | eq(s.turnArgChars, 6000, "arg progress tracked during attempt"); |
| 233 | |
| 234 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "sa-1", action: "discard", attempt: 1, max: 6, reason: "premature_eof" } } as WireEvent); |
| 235 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "c1").length, 0, "discard removes uncommitted parent tool card"); |
| 236 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "child-1").length, 1, "discard keeps concurrent sub-agent tool card"); |
| 237 | eq(s.live?.text ?? "", "", "discard clears attempt text (not concatenate)"); |
| 238 | eq(s.turnArgChars, 0, "discard restores turnArgChars baseline"); |
| 239 | |
| 240 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "sa-2", action: "begin", attempt: 2, max: 6 } } as WireEvent); |
| 241 | s = ev(s, { kind: "text", text: "full answer" } as WireEvent); |
| 242 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c2", name: "edit_file", readOnly: false, partial: true, argChars: 12000, attemptId: "sa-2" } } as WireEvent); |
| 243 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "sa-2", action: "commit", attempt: 2, max: 6 } } as WireEvent); |
| 244 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c2", name: "edit_file", args: '{"path":"a"}', readOnly: false } } as WireEvent); |
| 245 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "c2").length, 1, "final success has committed parent tool card"); |
| 246 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "child-1").length, 1, "sub-agent card still present after commit"); |
| 247 | const committedAssistant = s.items.find((it) => it.kind === "assistant" && it.text === "full answer"); |
| 248 | eq(Boolean(committedAssistant), true, "full dispatch settles the committed attempt text"); |
| 249 | eq(s.live, undefined, "full dispatch closes the compatibility-path assistant segment"); |
| 250 | } |
| 251 | |
| 252 | // --- 1d. stale discard must not clear a newer attempt journal --- |
| 253 | { |
| 254 | let s = { ...initialState, running: true, turnActive: true }; |
| 255 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "sa-new", action: "begin", attempt: 2, max: 6 } } as WireEvent); |
| 256 | s = ev(s, { kind: "tool_dispatch", tool: { id: "c-new", name: "edit_file", readOnly: false, partial: true, attemptId: "sa-new" } } as WireEvent); |
| 257 | eq(s.streamAttemptJournal?.id, "sa-new", "journal tracks current attempt"); |
| 258 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "sa-old", action: "discard", attempt: 1, max: 6, reason: "premature_eof" } } as WireEvent); |
| 259 | eq(s.streamAttemptJournal?.id, "sa-new", "stale discard leaves current journal"); |
| 260 | eq(s.items.filter((it) => it.kind === "tool" && it.id === "c-new").length, 1, "stale discard does not remove current partial card"); |
| 261 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 262 | eq(s.streamAttemptJournal, undefined, "turn_done clears stream attempt journal"); |
| 263 | } |
| 264 | |
| 265 | // --- 1e. one backend turn keeps each provider sampling round in timeline order --- |
| 266 | { |
| 267 | let s = ev({ ...initialState }, { kind: "turn_started", turnId: "multi-round" } as WireEvent); |
| 268 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "round-1", action: "begin", attempt: 1, max: 3 } } as WireEvent); |
| 269 | s = ev(s, { kind: "reasoning", reasoning: "first analysis" } as WireEvent); |
| 270 | s = ev(s, { kind: "message", text: "", reasoning: "first analysis" } as WireEvent); |
| 271 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "round-1", action: "commit", attempt: 1, max: 3 } } as WireEvent); |
| 272 | s = ev(s, { kind: "tool_dispatch", tool: { id: "lookup-1", name: "read_file", args: "{}", readOnly: true } } as WireEvent); |
| 273 | s = ev(s, { kind: "tool_result", tool: { id: "lookup-1", name: "read_file", args: "{}", readOnly: true, output: "ok" } } as WireEvent); |
| 274 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "round-2", action: "begin", attempt: 1, max: 3 } } as WireEvent); |
| 275 | s = ev(s, { kind: "reasoning", reasoning: "second analysis" } as WireEvent); |
| 276 | s = ev(s, { kind: "message", text: "final answer", reasoning: "second analysis" } as WireEvent); |
| 277 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "round-2", action: "commit", attempt: 1, max: 3 } } as WireEvent); |
| 278 | const replayedStart = ev(s, { kind: "turn_started", turnId: "multi-round" } as WireEvent); |
| 279 | eq(replayedStart.items.filter((item) => item.kind === "assistant").length, 2, "a replayed turn_started event does not duplicate settled segments"); |
| 280 | s = replayedStart; |
| 281 | s = ev(s, { kind: "turn_done", turnId: "multi-round" } as WireEvent); |
| 282 | |
| 283 | const timeline = s.items.filter((item) => item.kind === "assistant" || item.kind === "tool"); |
| 284 | eq(timeline.map((item) => item.kind).join(","), "assistant,tool,assistant", "multi-round live order matches persisted history order"); |
| 285 | eq(timeline[0]?.id, "a:multi-round:0", "first sample owns ordinal zero"); |
| 286 | eq(timeline[2]?.id, "a:multi-round:1", "second sample owns a distinct ordinal"); |
| 287 | eq(timeline[0]?.kind === "assistant" ? timeline[0].reasoning : "", "first analysis", "first reasoning is not overwritten"); |
| 288 | eq(timeline[2]?.kind === "assistant" ? timeline[2].text : "", "final answer", "final answer remains in the second segment"); |
| 289 | eq(timeline[2]?.kind === "assistant" ? timeline[2].wasStreamed : undefined, true, "live-origin identity survives completion"); |
| 290 | |
| 291 | } |
| 292 | |
| 293 | // --- 1f. tool-only samples remove their placeholder before the next round --- |
| 294 | { |
| 295 | let s = ev({ ...initialState }, { kind: "turn_started", turnId: "tool-only" } as WireEvent); |
| 296 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "tool-round", action: "begin", attempt: 1, max: 2 } } as WireEvent); |
| 297 | s = ev(s, { kind: "tool_dispatch", tool: { id: "shell-1", name: "bash", readOnly: false, partial: true, attemptId: "tool-round" } } as WireEvent); |
| 298 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "tool-round", action: "commit", attempt: 1, max: 2 } } as WireEvent); |
| 299 | eq(s.items.some((item) => item.kind === "assistant"), false, "tool-only commit removes its empty assistant placeholder"); |
| 300 | s = ev(s, { kind: "tool_dispatch", tool: { id: "shell-1", name: "bash", args: "{}", readOnly: false } } as WireEvent); |
| 301 | s = ev(s, { kind: "tool_result", tool: { id: "shell-1", name: "bash", args: "{}", readOnly: false, output: "ok" } } as WireEvent); |
| 302 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "answer-round", action: "begin", attempt: 1, max: 2 } } as WireEvent); |
| 303 | s = ev(s, { kind: "reasoning", reasoning: "after tool" } as WireEvent); |
| 304 | s = ev(s, { kind: "message", text: "done", reasoning: "after tool" } as WireEvent); |
| 305 | const visible = s.items.filter((item) => item.kind === "tool" || item.kind === "assistant"); |
| 306 | eq(visible.map((item) => item.kind).join(","), "tool,assistant", "next reasoning is appended below the committed tool"); |
| 307 | eq(visible[1]?.id, "a:tool-only:1", "tool-only placeholder ordinal is not reused"); |
| 308 | } |
| 309 | |
| 310 | // --- 1g. discard retries preserve segment identity; legacy full tools retire placeholders --- |
| 311 | { |
| 312 | let s = ev({ ...initialState }, { kind: "turn_started", turnId: "retry-segment" } as WireEvent); |
| 313 | const segmentId = s.currentAssistant; |
| 314 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "bad", action: "begin", attempt: 1, max: 2 } } as WireEvent); |
| 315 | s = ev(s, { kind: "reasoning", reasoning: "discard me" } as WireEvent); |
| 316 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "bad", action: "discard", attempt: 1, max: 2 } } as WireEvent); |
| 317 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "good", action: "begin", attempt: 2, max: 2 } } as WireEvent); |
| 318 | eq(s.currentAssistant, segmentId, "retry reuses the same sampling segment identity"); |
| 319 | eq(s.live?.reasoning, "", "retry starts from the rolled-back reasoning baseline"); |
| 320 | |
| 321 | let legacy = ev({ ...initialState }, { kind: "turn_started", turnId: "legacy" } as WireEvent); |
| 322 | legacy = ev(legacy, { kind: "tool_dispatch", tool: { id: "legacy-tool", name: "bash", args: "{}", readOnly: false } } as WireEvent); |
| 323 | legacy = ev(legacy, { kind: "reasoning", reasoning: "legacy follow-up" } as WireEvent); |
| 324 | const legacyTimeline = legacy.items.filter((item) => item.kind === "tool" || item.kind === "assistant"); |
| 325 | eq(legacyTimeline.map((item) => item.kind).join(","), "tool,assistant", "full dispatch compatibility path retires the empty placeholder"); |
| 326 | eq(legacyTimeline[1]?.id, "a:legacy:1", "legacy follow-up gets the next segment ordinal"); |
| 327 | |
| 328 | let partialLegacy = ev({ ...initialState, activeTurnId: "partial-legacy", running: true, turnActive: true }, { |
| 329 | kind: "tool_dispatch", |
| 330 | tool: { id: "partial-legacy-tool", name: "bash", readOnly: false, partial: true }, |
| 331 | } as WireEvent); |
| 332 | eq(partialLegacy.currentAssistant, "a:partial-legacy:0", "first partial dispatch backfills a sampling segment"); |
| 333 | partialLegacy = ev(partialLegacy, { |
| 334 | kind: "tool_dispatch", |
| 335 | tool: { id: "partial-legacy-tool", name: "bash", args: "{}", readOnly: false }, |
| 336 | } as WireEvent); |
| 337 | eq(partialLegacy.items.some((item) => item.kind === "assistant"), false, "legacy full dispatch removes the partial path's empty segment"); |
| 338 | } |
| 339 | |
| 340 | // --- 1h. terminal duration belongs to the last retained assistant segment --- |
| 341 | { |
| 342 | const originalNow = Date.now; |
| 343 | let now = 100_000; |
| 344 | Date.now = () => now; |
| 345 | try { |
| 346 | let s = ev({ ...initialState }, { kind: "turn_started", turnId: "terminal-duration" } as WireEvent); |
| 347 | now = 101_000; |
| 348 | s = ev(s, { kind: "message", text: "first answer" } as WireEvent); |
| 349 | s = ev(s, { kind: "tool_dispatch", tool: { id: "duration-tool", name: "bash", args: "{}", readOnly: false } } as WireEvent); |
| 350 | s = ev(s, { kind: "tool_result", tool: { id: "duration-tool", name: "bash", args: "{}", readOnly: false, output: "ok" } } as WireEvent); |
| 351 | now = 110_000; |
| 352 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "empty-final", action: "begin", attempt: 1, max: 1 } } as WireEvent); |
| 353 | s = ev(s, { kind: "turn_done", turnId: "terminal-duration", status: "interrupted" } as WireEvent); |
| 354 | |
| 355 | const assistants = s.items.filter((item) => item.kind === "assistant"); |
| 356 | eq(assistants.length, 1, "turn_done removes the empty terminal placeholder"); |
| 357 | eq(assistants[0]?.kind === "assistant" ? assistants[0].workDurationMs : 0, 10_000, "turn_done assigns total work duration to the last visible assistant"); |
| 358 | } finally { |
| 359 | Date.now = originalNow; |
| 360 | } |
| 361 | } |
| 362 | |
| 363 | // --- 2. usageSeq bumps for every source --- |
| 364 | { |
| 365 | let s = { ...initialState, running: true, turnActive: true }; |
| 366 | s = ev(s, { kind: "usage", usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15, cacheHitTokens: 0, cacheMissTokens: 0 } } as WireEvent); |
| 367 | eq(s.usageSeq, 1, "executor usage bumps usageSeq"); |
| 368 | s = ev(s, { kind: "usage", usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15, cacheHitTokens: 0, cacheMissTokens: 0, source: "subagent" } } as WireEvent); |
| 369 | eq(s.usageSeq, 2, "subagent usage bumps usageSeq"); |
| 370 | eq(s.usage?.source ?? "", "", "subagent usage does not replace executor gauge usage"); |
| 371 | } |
| 372 | |
| 373 | // --- 3. context no-regress guard while a turn runs --- |
| 374 | { |
| 375 | let s = { ...initialState, running: true, turnActive: true, context: { used: 14000, window: 1000000, sessionTokens: 20000 } }; |
| 376 | s = reducer(s, { type: "context", context: { used: 0, window: 1000000, sessionTokens: 20000 } } as never); |
| 377 | eq(s.context.used, 14000, "mid-turn used=0 snapshot keeps last known fill"); |
| 378 | |
| 379 | let idle = { ...initialState, context: { used: 14000, window: 1000000, sessionTokens: 20000 } }; |
| 380 | idle = reducer(idle, { type: "context", context: { used: 0, window: 1000000, sessionTokens: 0 } } as never); |
| 381 | eq(idle.context.used, 0, "idle used=0 snapshot applies (genuine reset)"); |
| 382 | } |
| 383 | |
| 384 | // --- 4. retrying is authoritative foreground activity --- |
| 385 | { |
| 386 | let s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 387 | s = reducer(s, { |
| 388 | type: "backend_status", |
| 389 | running: false, |
| 390 | pendingPrompt: false, |
| 391 | backgroundJobs: 0, |
| 392 | cancelRequested: false, |
| 393 | cancellable: false, |
| 394 | }); |
| 395 | eq(s.running, false, "stale idle snapshot reproduces the hidden-stop state"); |
| 396 | |
| 397 | s = ev(s, { kind: "retrying", retryAttempt: 3, retryMax: 10 } as WireEvent); |
| 398 | eq(s.retry?.attempt, 3, "retry status keeps the current attempt"); |
| 399 | eq(s.retry?.max, 10, "retry status keeps the retry budget"); |
| 400 | const recovery = { phase: "headers", next_attempt_at: Date.now() + 60_000, waited_ms: 14_000, waiting: true }; |
| 401 | s = ev(s, { kind: "retrying", retryAttempt: 4, retryMax: 3, recovery } as WireEvent); |
| 402 | eq(s.retry?.recovery?.waiting, true, "continuous wait survives the controller projection"); |
| 403 | eq(s.retry?.recovery?.next_attempt_at, recovery.next_attempt_at, "countdown uses the provider recovery deadline"); |
| 404 | |
| 405 | eq(s.running, true, "retry event restores the active turn"); |
| 406 | eq(s.turnActive, true, "retry event restores the turn epoch"); |
| 407 | eq(s.cancellable, true, "retry event keeps Stop and Escape cancellation available"); |
| 408 | eq(s.turnStartAt > 0, true, "retry event restores timing for a reattached turn"); |
| 409 | |
| 410 | const repaired = s; |
| 411 | const completed = ev(repaired, { kind: "turn_done" } as WireEvent); |
| 412 | eq(completed.running, false, "turn_done still ends the repaired turn"); |
| 413 | eq(completed.retry, undefined, "turn_done clears the retry indicator"); |
| 414 | |
| 415 | const failed = ev(repaired, { kind: "turn_done", err: "shared window overflow" } as WireEvent); |
| 416 | eq(failed.running, false, "terminal context error clears running"); |
| 417 | eq(failed.pendingPrompt, false, "terminal context error clears pending prompt"); |
| 418 | eq(failed.messageAction, undefined, "terminal context error restores message actions"); |
| 419 | |
| 420 | const staleSnapshotAt = promptEventClock(); |
| 421 | s = ev(repaired, { kind: "retrying", retryAttempt: 4, retryMax: 10 } as WireEvent); |
| 422 | s = reducer(s, { |
| 423 | type: "backend_status", |
| 424 | running: false, |
| 425 | pendingPrompt: false, |
| 426 | backgroundJobs: 0, |
| 427 | cancelRequested: false, |
| 428 | cancellable: false, |
| 429 | snapshotAt: staleSnapshotAt, |
| 430 | }); |
| 431 | eq(s.running, true, "idle snapshot fetched before retry cannot hide Stop when it returns later"); |
| 432 | eq(s.turnActive, true, "idle snapshot fetched before retry cannot end the active turn"); |
| 433 | eq(s.cancellable, true, "idle snapshot fetched before retry preserves cancellation"); |
| 434 | eq(s.retry?.attempt, 4, "stale idle snapshot preserves the newer retry status"); |
| 435 | |
| 436 | s = reducer(s, { |
| 437 | type: "backend_status", |
| 438 | running: false, |
| 439 | pendingPrompt: false, |
| 440 | backgroundJobs: 0, |
| 441 | cancelRequested: false, |
| 442 | cancellable: false, |
| 443 | snapshotAt: Number.MAX_SAFE_INTEGER, |
| 444 | }); |
| 445 | eq(s.running, false, "fresh idle snapshot can reconcile a missed turn_done"); |
| 446 | eq(s.retry, undefined, "fresh idle snapshot clears the retry indicator"); |
| 447 | } |
| 448 | |
| 449 | // --- 4b. runtime status sequence is monotonic within a controller epoch --- |
| 450 | { |
| 451 | let s = { ...initialState, running: true, turnActive: true }; |
| 452 | s = reducer(s, { |
| 453 | type: "backend_status", running: true, turnId: "turn-new", runtimeEpoch: "epoch-a", turnEventSeq: 12, |
| 454 | }); |
| 455 | const stale = reducer(s, { |
| 456 | type: "backend_status", running: false, runtimeEpoch: "epoch-a", turnEventSeq: 11, |
| 457 | }); |
| 458 | eq(stale.running, true, "older idle runtime snapshot cannot hide a newer running turn"); |
| 459 | eq(stale.activeTurnId, "turn-new", "older runtime snapshot cannot clear the active turn"); |
| 460 | const duplicate = reducer(s, { |
| 461 | type: "backend_status", running: false, runtimeEpoch: "epoch-a", turnEventSeq: 12, |
| 462 | }); |
| 463 | eq(duplicate.running, true, "duplicate runtime sequence cannot mutate turn state"); |
| 464 | const rebuilt = reducer(stale, { |
| 465 | type: "backend_status", running: false, runtimeEpoch: "epoch-b", turnEventSeq: 1, |
| 466 | }); |
| 467 | eq(rebuilt.running, false, "a new controller epoch may settle from its first snapshot"); |
| 468 | } |
| 469 | |
| 470 | // --- 5. TPS telemetry excludes tool gaps and preserves fallback estimates --- |
| 471 | { |
| 472 | const originalNow = Date.now; |
| 473 | let now = 1_000; |
| 474 | Date.now = () => now; |
| 475 | try { |
| 476 | let s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 477 | now = 1_100; |
| 478 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 479 | now = 2_100; |
| 480 | s = ev(s, { kind: "usage", usage: { promptTokens: 10, completionTokens: 4, totalTokens: 14, cacheHitTokens: 0, cacheMissTokens: 10 } } as WireEvent); |
| 481 | s = ev(s, { kind: "message", text: "abcd" } as WireEvent); |
| 482 | eq(s.turnModelActiveMs, 1_000, "first provider output interval is accumulated"); |
| 483 | eq(s.turnOutputCharsAtUsage, 0, "completed assistant message resets the live-character baseline"); |
| 484 | |
| 485 | // A long tool gap must not lower TPS for the next provider request. |
| 486 | now = 8_000; |
| 487 | s = ev(s, { kind: "text", text: "abcdefgh" } as WireEvent); |
| 488 | now = 9_000; |
| 489 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 490 | eq(s.lastTurnOutputTokens, 6, "missing final usage adds only the in-flight character estimate"); |
| 491 | eq(s.lastTurnModelMs, 2_000, "tool gap is excluded from completed TPS duration"); |
| 492 | eq(s.lastTurnOutputEstimated, true, "missing final usage marks completed TPS as estimated"); |
| 493 | |
| 494 | // Providers that omit per-request usage must still close the first model |
| 495 | // interval before the tool runs. |
| 496 | now = 12_000; |
| 497 | s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 498 | now = 12_100; |
| 499 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 500 | now = 13_100; |
| 501 | s = ev(s, { kind: "message", text: "abcd" } as WireEvent); |
| 502 | s = ev(s, { kind: "tool_dispatch", tool: { id: "missing-usage", name: "read_file", args: "{}", readOnly: true } } as WireEvent); |
| 503 | now = 19_000; |
| 504 | s = ev(s, { kind: "text", text: "efgh" } as WireEvent); |
| 505 | now = 20_000; |
| 506 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 507 | eq(s.lastTurnOutputTokens, 2, "missing usage estimates output across provider requests"); |
| 508 | eq(s.lastTurnModelMs, 2_000, "missing usage still excludes the tool gap"); |
| 509 | |
| 510 | now = 21_000; |
| 511 | s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 512 | now = 21_100; |
| 513 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 514 | now = 22_100; |
| 515 | s = ev(s, { kind: "usage", usage: { promptTokens: 10, completionTokens: 1, totalTokens: 11, cacheHitTokens: 0, cacheMissTokens: 10, estimated: true } } as WireEvent); |
| 516 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 517 | eq(s.lastTurnOutputEstimated, true, "provider-estimated usage marks completed TPS as estimated"); |
| 518 | |
| 519 | now = 23_000; |
| 520 | s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 521 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 522 | now = 24_000; |
| 523 | s = reducer(s, { |
| 524 | type: "backend_status", |
| 525 | running: false, |
| 526 | pendingPrompt: false, |
| 527 | backgroundJobs: 0, |
| 528 | cancelRequested: false, |
| 529 | cancellable: false, |
| 530 | }); |
| 531 | eq(s.lastTurnOutputTokens, 1, "idle reconciliation snapshots fallback output telemetry"); |
| 532 | eq(s.lastTurnModelMs, 1_000, "idle reconciliation closes the active provider interval"); |
| 533 | eq(s.lastTurnOutputEstimated, true, "idle reconciliation preserves the estimated marker"); |
| 534 | } finally { |
| 535 | Date.now = originalNow; |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | // --- 6. TPS telemetry follows executor output-token semantics and retry intervals --- |
| 540 | { |
| 541 | const originalNow = Date.now; |
| 542 | let now = 30_000; |
| 543 | Date.now = () => now; |
| 544 | try { |
| 545 | let s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 546 | now = 30_100; |
| 547 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 548 | now = 31_100; |
| 549 | s = ev(s, { kind: "usage", usage: { |
| 550 | promptTokens: 100, |
| 551 | completionTokens: 20, |
| 552 | reasoningTokens: 10, |
| 553 | totalTokens: 120, |
| 554 | cacheHitTokens: 0, |
| 555 | cacheMissTokens: 100, |
| 556 | source: "executor", |
| 557 | } } as WireEvent); |
| 558 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 559 | eq(s.lastTurnOutputTokens, 20, "reasoning tokens are not added twice to completed TPS"); |
| 560 | eq(s.lastTurnModelMs, 1_000, "reasoning usage preserves the executor output interval"); |
| 561 | |
| 562 | now = 32_000; |
| 563 | s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 564 | now = 32_100; |
| 565 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 566 | now = 32_500; |
| 567 | s = ev(s, { kind: "usage", usage: { |
| 568 | promptTokens: 50, |
| 569 | completionTokens: 100, |
| 570 | totalTokens: 150, |
| 571 | cacheHitTokens: 0, |
| 572 | cacheMissTokens: 50, |
| 573 | source: "subagent", |
| 574 | } } as WireEvent); |
| 575 | now = 33_100; |
| 576 | s = ev(s, { kind: "usage", usage: { |
| 577 | promptTokens: 10, |
| 578 | completionTokens: 10, |
| 579 | totalTokens: 20, |
| 580 | cacheHitTokens: 0, |
| 581 | cacheMissTokens: 10, |
| 582 | source: "executor", |
| 583 | } } as WireEvent); |
| 584 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 585 | eq(s.lastTurnOutputTokens, 10, "subagent usage is excluded from executor TPS tokens"); |
| 586 | eq(s.lastTurnModelMs, 1_000, "subagent usage does not close the executor output interval"); |
| 587 | |
| 588 | now = 34_000; |
| 589 | s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 590 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "tps-a1", action: "begin", attempt: 1, max: 2 } } as WireEvent); |
| 591 | now = 34_100; |
| 592 | s = ev(s, { kind: "text", text: "abcdefgh" } as WireEvent); |
| 593 | now = 35_100; |
| 594 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "tps-a1", action: "discard", attempt: 1, max: 2 } } as WireEvent); |
| 595 | now = 38_000; |
| 596 | s = ev(s, { kind: "stream_attempt", streamAttempt: { id: "tps-a2", action: "begin", attempt: 2, max: 2 } } as WireEvent); |
| 597 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 598 | now = 39_000; |
| 599 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 600 | eq(s.lastTurnModelMs, 2_000, "discarded sampling attempts exclude retry backoff from TPS"); |
| 601 | } finally { |
| 602 | Date.now = originalNow; |
| 603 | } |
| 604 | } |
| 605 | |
| 606 | // --- 7. lastRequestTps pairs the closed interval with the usage tokens --- |
| 607 | { |
| 608 | const originalNow = Date.now; |
| 609 | let now = 50_000; |
| 610 | Date.now = () => now; |
| 611 | try { |
| 612 | let s = ev({ ...initialState }, { kind: "turn_started" } as WireEvent); |
| 613 | now = 50_100; |
| 614 | s = ev(s, { kind: "text", text: "abcd" } as WireEvent); |
| 615 | now = 51_100; |
| 616 | // The message event closes the interval BEFORE the usage event arrives. |
| 617 | s = ev(s, { kind: "message", text: "abcd" } as WireEvent); |
| 618 | now = 51_200; |
| 619 | s = ev(s, { kind: "usage", usage: { |
| 620 | promptTokens: 130, completionTokens: 30, totalTokens: 160, |
| 621 | contextPromptTokens: 100, contextCompletionTokens: 20, |
| 622 | cacheHitTokens: 0, cacheMissTokens: 100, source: "executor", |
| 623 | } } as WireEvent); |
| 624 | eq(s.lastRequestTps, 20, "sampling recovery pairs the interval with latest-attempt tokens"); |
| 625 | |
| 626 | now = 52_000; |
| 627 | s = ev(s, { kind: "text", text: "more" } as WireEvent); |
| 628 | now = 52_050; |
| 629 | s = ev(s, { kind: "message", text: "more" } as WireEvent); |
| 630 | now = 52_100; |
| 631 | s = ev(s, { kind: "usage", usage: { |
| 632 | promptTokens: 5, completionTokens: 50, totalTokens: 55, |
| 633 | cacheHitTokens: 0, cacheMissTokens: 5, source: "subagent", |
| 634 | } } as WireEvent); |
| 635 | eq(s.lastRequestTps, 20, "non-executor usage neither computes nor consumes the pending interval"); |
| 636 | now = 52_300; |
| 637 | s = ev(s, { kind: "usage", usage: { |
| 638 | promptTokens: 10, completionTokens: 30, totalTokens: 40, |
| 639 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 640 | } } as WireEvent); |
| 641 | eq(s.lastRequestTps, null, "intervals under the 500ms gate clear stale request TPS"); |
| 642 | |
| 643 | now = 53_000; |
| 644 | s = ev(s, { kind: "text", text: "second" } as WireEvent); |
| 645 | now = 54_000; |
| 646 | s = ev(s, { kind: "message", text: "second" } as WireEvent); |
| 647 | now = 54_100; |
| 648 | s = ev(s, { kind: "usage", usage: { |
| 649 | promptTokens: 10, completionTokens: 30, totalTokens: 40, |
| 650 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 651 | } } as WireEvent); |
| 652 | eq(s.lastRequestTps, 30, "a later executor usage refreshes the request TPS"); |
| 653 | |
| 654 | now = 55_000; |
| 655 | s = ev(s, { kind: "text", text: "direct" } as WireEvent); |
| 656 | now = 56_000; |
| 657 | s = ev(s, { kind: "usage", usage: { |
| 658 | promptTokens: 10, completionTokens: 40, totalTokens: 50, |
| 659 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 660 | } } as WireEvent); |
| 661 | eq(s.lastRequestTps, 40, "usage measures an interval still open at arrival"); |
| 662 | |
| 663 | now = 57_000; |
| 664 | s = ev(s, { kind: "turn_done" } as WireEvent); |
| 665 | eq(s.lastRequestTps, 40, "request TPS persists across turn boundaries"); |
| 666 | |
| 667 | now = 58_000; |
| 668 | s = ev(s, { kind: "turn_started" } as WireEvent); |
| 669 | now = 58_100; |
| 670 | s = ev(s, { kind: "usage", usage: { |
| 671 | promptTokens: 10, completionTokens: 8, totalTokens: 18, |
| 672 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 673 | } } as WireEvent); |
| 674 | eq(s.lastRequestTps, null, "usage without a provider interval clears stale request TPS"); |
| 675 | now = 58_200; |
| 676 | s = ev(s, { kind: "tool_dispatch", tool: { id: "final-only", name: "read_file", args: "{}", readOnly: true } } as WireEvent); |
| 677 | eq(s.lastRequestTps, null, "a final-only tool dispatch after usage cannot resurrect stale TPS"); |
| 678 | |
| 679 | now = 59_000; |
| 680 | s = ev(s, { kind: "text", text: "toolcall" } as WireEvent); |
| 681 | now = 60_000; |
| 682 | s = ev(s, { kind: "tool_dispatch", tool: { id: "t1", name: "read_file", args: "{}", readOnly: true } } as WireEvent); |
| 683 | now = 60_100; |
| 684 | s = ev(s, { kind: "usage", usage: { |
| 685 | promptTokens: 10, completionTokens: 25, totalTokens: 35, |
| 686 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 687 | } } as WireEvent); |
| 688 | eq(s.lastRequestTps, 25, "tool_dispatch closes the interval the next executor usage pairs with"); |
| 689 | |
| 690 | now = 61_000; |
| 691 | s = ev(s, { kind: "tool_dispatch", tool: { id: "t2", name: "write_file", readOnly: false, partial: true, argChars: 600 } } as WireEvent); |
| 692 | now = 62_000; |
| 693 | s = ev(s, { kind: "usage", usage: { |
| 694 | promptTokens: 10, completionTokens: 30, totalTokens: 40, |
| 695 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 696 | } } as WireEvent); |
| 697 | eq(s.lastRequestTps, 30, "usage closes the interval started by a partial tool dispatch"); |
| 698 | now = 62_100; |
| 699 | s = ev(s, { kind: "tool_dispatch", tool: { id: "t2", name: "write_file", args: "{}", readOnly: false } } as WireEvent); |
| 700 | eq(s.lastRequestTps, 30, "the later full tool dispatch preserves the measured request TPS"); |
| 701 | |
| 702 | now = 63_000; |
| 703 | s = ev(s, { kind: "text", text: "closing" } as WireEvent); |
| 704 | now = 64_000; |
| 705 | s = ev(s, { kind: "message", text: "closing" } as WireEvent); |
| 706 | now = 64_100; |
| 707 | s = ev(s, { kind: "tool_dispatch", tool: { id: "t3", name: "write_file", readOnly: false, partial: true, argChars: 300 } } as WireEvent); |
| 708 | now = 64_700; |
| 709 | s = ev(s, { kind: "tool_dispatch", tool: { id: "t3", name: "write_file", args: "{}", readOnly: false } } as WireEvent); |
| 710 | now = 64_800; |
| 711 | s = ev(s, { kind: "usage", usage: { |
| 712 | promptTokens: 10, completionTokens: 30, totalTokens: 40, |
| 713 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 714 | } } as WireEvent); |
| 715 | // The partial restart begins a new interval; the full dispatch closes it |
| 716 | // and overwrites the message-stashed pending with its own (≥500ms) tail. |
| 717 | eq(s.lastRequestTps, 50, "a full dispatch overwrites a message-stashed pending with its own tail close"); |
| 718 | |
| 719 | now = 65_000; |
| 720 | s = ev(s, { kind: "text", text: "slow" } as WireEvent); |
| 721 | now = 68_000; |
| 722 | s = ev(s, { kind: "message", text: "slow" } as WireEvent); |
| 723 | now = 68_100; |
| 724 | s = ev(s, { kind: "usage", usage: { |
| 725 | promptTokens: 10, completionTokens: 1, totalTokens: 11, |
| 726 | cacheHitTokens: 0, cacheMissTokens: 10, source: "executor", |
| 727 | } } as WireEvent); |
| 728 | eq(s.lastRequestTps, 1 / 3, "slow measurable requests retain their raw sub-one TPS"); |
| 729 | } finally { |
| 730 | Date.now = originalNow; |
| 731 | } |
| 732 | } |
| 733 | |
| 734 | // --- 8. context occupancy uses prompt tokens and keeps legacy fallback semantics --- |
| 735 | { |
| 736 | let s = ev({ |
| 737 | ...initialState, |
| 738 | running: true, |
| 739 | turnActive: true, |
| 740 | context: { ...initialState.context, window: 1_000 }, |
| 741 | }, { kind: "usage", usage: { |
| 742 | promptTokens: 500, |
| 743 | completionTokens: 20, |
| 744 | totalTokens: 520, |
| 745 | contextPromptTokens: 0, |
| 746 | contextCompletionTokens: 20, |
| 747 | source: "executor", |
| 748 | } } as WireEvent); |
| 749 | eq(s.context.used, 500, "completion-only latest usage falls back to aggregate prompt occupancy"); |
| 750 | |
| 751 | s = ev({ ...s, running: true, turnActive: true }, { kind: "usage", usage: { |
| 752 | promptTokens: 700, |
| 753 | completionTokens: 30, |
| 754 | totalTokens: 730, |
| 755 | contextPromptTokens: 450, |
| 756 | contextCompletionTokens: 0, |
| 757 | source: "executor", |
| 758 | } } as WireEvent); |
| 759 | eq(s.context.used, 450, "latest-attempt prompt occupancy excludes completion tokens"); |
| 760 | } |
| 761 | |
| 762 | process.stdout.write(`\n${passed} passed, ${failed} failed\n`); |
| 763 | if (failed > 0) process.exit(1); |
| 764 |