diff options
| author | Adam Malczewski <[email protected]> | 2026-06-02 13:34:33 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-02 13:34:33 +0900 |
| commit | 48c120e5cd400b2e2b8afae0afcc7c8bc4d2ccb4 (patch) | |
| tree | 2c434aeba0db7d6ec5b87e2f7fe2c81352f0888c /packages/api/tests | |
| parent | b734eb96bf0af267fdfbef85df51940ca0b4e8c7 (diff) | |
| download | dispatch-48c120e5cd400b2e2b8afae0afcc7c8bc4d2ccb4.tar.gz dispatch-48c120e5cd400b2e2b8afae0afcc7c8bc4d2ccb4.zip | |
fix: reconcile live cacheStats to DB truth on turn-sealed
Addresses the live-accumulator overshoot a Gemini review surfaced: the
frontend adds every streamed usage event to cacheStats, but a rate-limited
fallback attempt's usage is discarded server-side (never persisted). Live
numbers overshot until a reload re-seeded from the DB aggregate.
Fix: turn-sealed (emitted AFTER the atomic usage-row write) now carries the
authoritative getUsageStatsForTab aggregate. The store REPLACES (not adds)
cacheStats with it every turn — landing the just-sealed turn's usage AND
self-healing any live drift, including the discarded-fallback overshoot. No
extra round-trip (piggybacks turn-sealed); idempotent in the happy path.
- core: add UsageStats type; getUsageStatsForTab returns it; turn-sealed gains
optional usageStats field.
- api: agent-manager reads getUsageStatsForTab post-flush and attaches it to
the turn-sealed emit (try/catch: omit on DB error).
- frontend: turn-sealed handler replaces cacheStats (undefined ⇒ untouched
back-compat; null ⇒ clear).
Tests: frontend reconcile/self-heal/back-compat/null-clear; api turn-sealed
carries aggregate. 509 -> 514 passing; typecheck + biome green.
Diffstat (limited to 'packages/api/tests')
| -rw-r--r-- | packages/api/tests/agent-manager.test.ts | 39 |
1 files changed, 39 insertions, 0 deletions
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts index 95fb558..6d7d66f 100644 --- a/packages/api/tests/agent-manager.test.ts +++ b/packages/api/tests/agent-manager.test.ts @@ -104,6 +104,13 @@ function resetAppendChunksCalls(): void { appendChunksCalls.length = 0; } +// Seedable return value for the mocked getUsageStatsForTab — what the backend +// reads (post-write) to attach to the `turn-sealed` event. +const fakeUsageStatsByTab = new Map<string, unknown>(); +function resetFakeUsageStats(): void { + fakeUsageStatsByTab.clear(); +} + // Allow tests to swap in a custom `run` generator (e.g. to simulate // a fallback failure mid-stream). Returning to undefined restores // the default. @@ -371,6 +378,9 @@ vi.mock("@dispatch/core", () => ({ getMessagesForTab(tabId: string) { return fakeMessagesByTab.get(tabId) ?? []; }, + getUsageStatsForTab(tabId: string) { + return fakeUsageStatsByTab.get(tabId) ?? null; + }, appendEventToChunks: appendEventToChunksSpy, applySystemEvent(_messages: unknown[], _event: unknown) { return { messageId: "mock-system-msg" }; @@ -421,6 +431,7 @@ describe("AgentManager", () => { setRunImpl(null); appendEventToChunksSpy.mockClear(); resetAppendChunksCalls(); + resetFakeUsageStats(); }); it("initial status is idle", () => { @@ -1402,6 +1413,34 @@ describe("AgentManager", () => { }); }); + it("attaches the DB usage aggregate to the turn-sealed event for live reconciliation", async () => { + const manager = new AgentManager(); + const aggregate = { + inputTokens: 222, + outputTokens: 22, + cacheReadTokens: 100, + cacheWriteTokens: 5, + requests: 1, + last: { inputTokens: 222, outputTokens: 22, cacheReadTokens: 100, cacheWriteTokens: 5 }, + }; + fakeUsageStatsByTab.set("tab-sealed-usage", aggregate); + + const events: AgentEvent[] = []; + manager.onEvent((event) => { + events.push(event); + }); + + await manager.processMessage("tab-sealed-usage", "go"); + + const sealed = events.find((e) => e.type === "turn-sealed") as + | Extract<AgentEvent, { type: "turn-sealed" }> + | undefined; + expect(sealed).toBeDefined(); + // The aggregate read AFTER the write is carried on the event so the + // frontend can REPLACE its live cacheStats with the DB truth. + expect(sealed?.usageStats).toEqual(aggregate); + }); + it("emits usage rows in the SAME appendChunks call as the turn's content (one atomic write)", async () => { const manager = new AgentManager(); setRunImpl(async function* () { |
