diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 12:23:06 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 12:23:06 +0900 |
| commit | c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (patch) | |
| tree | 3f7c2feddbe697a79abd952bb80ed0e01dac0a7a /packages/cache-warming/src/pure.test.ts | |
| parent | f6b45507210e04e9884256b0132900640de4334b (diff) | |
| download | dispatch-c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3.tar.gz dispatch-c2b4c05d91fa88b8d02c055a0e15c22abd8e21f3.zip | |
feat(cache-warming): per-conversation prompt-cache warming + warm() service
Backend-driven warming targeting whatever provider a conversation uses (incl. the
external Claude provider-anthropic). Core engine + on/off + last-cache-% done;
interval-as-view-control pending a ui-contract NumberField (surface-system gap).
Mechanism:
- kernel: expose HostAPI.emit (typed bus event emit; counterpart of on)
- session-orchestrator: turnStarted/turnSettled event hooks (conversationId/cwd/model);
warm() service (cacheWarmHandle) reusing the real-turn assembly (byte-identical prefix,
provider-agnostic), refuses mid-turn, never persists/emits, returns Usage
- cache-warming (new ext): per-conversation timers (arm on settle, cancel on start,
in-flight invalidation), calls warm(), pct=round(clamp(cacheRead/input,0,1)*100),
persists {enabled,intervalMs} (default on/240s), registers a controls surface
- host-bin: register cache-warming; transport-http: HostAPI stub +emit (fan-out)
Honors old-code invariants. 760 vitest + 109 bun = 869 tests; tsc -b EXIT 0; biome clean.
Diffstat (limited to 'packages/cache-warming/src/pure.test.ts')
| -rw-r--r-- | packages/cache-warming/src/pure.test.ts | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/packages/cache-warming/src/pure.test.ts b/packages/cache-warming/src/pure.test.ts new file mode 100644 index 0000000..820260b --- /dev/null +++ b/packages/cache-warming/src/pure.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from "vitest"; +import type { ConversationState } from "./pure.js"; +import { + computeCachePct, + isTokenCurrent, + parseSettings, + serializeSettings, + shouldWarm, +} from "./pure.js"; + +describe("computeCachePct", () => { + it("cacheRead/input rounded and clamped to 0..100", () => { + expect(computeCachePct(1000, 800)).toBe(80); + expect(computeCachePct(1000, 1200)).toBe(100); + expect(computeCachePct(1000, -100)).toBe(0); + expect(computeCachePct(1000, 0)).toBe(0); + expect(computeCachePct(1000, 333)).toBe(33); + }); + + it("zero input tokens → 0", () => { + expect(computeCachePct(0, 500)).toBe(0); + expect(computeCachePct(-1, 500)).toBe(0); + }); +}); + +describe("shouldWarm", () => { + it("returns true when enabled, idle, and token matches", () => { + const state: ConversationState = { + enabled: true, + intervalMs: 240_000, + active: false, + lastPct: null, + token: 5, + }; + expect(shouldWarm(state, 5)).toBe(true); + }); + + it("returns false when disabled", () => { + const state: ConversationState = { + enabled: false, + intervalMs: 240_000, + active: false, + lastPct: null, + token: 5, + }; + expect(shouldWarm(state, 5)).toBe(false); + }); + + it("returns false when active", () => { + const state: ConversationState = { + enabled: true, + intervalMs: 240_000, + active: true, + lastPct: null, + token: 5, + }; + expect(shouldWarm(state, 5)).toBe(false); + }); + + it("returns false when token is superseded", () => { + const state: ConversationState = { + enabled: true, + intervalMs: 240_000, + active: false, + lastPct: null, + token: 5, + }; + expect(shouldWarm(state, 6)).toBe(false); + }); +}); + +describe("isTokenCurrent", () => { + it("returns true when tokens match", () => { + expect(isTokenCurrent(5, 5)).toBe(true); + }); + + it("returns false when tokens differ", () => { + expect(isTokenCurrent(5, 6)).toBe(false); + }); +}); + +describe("parseSettings/serializeSettings round-trip", () => { + it("round-trips enabled + intervalMs", () => { + const original = { enabled: false, intervalMs: 120_000 }; + const serialized = serializeSettings(original); + const parsed = parseSettings(serialized); + expect(parsed).toEqual(original); + }); + + it("returns defaults for null input", () => { + const parsed = parseSettings(null); + expect(parsed).toEqual({ enabled: true, intervalMs: 240_000 }); + }); + + it("returns defaults for malformed JSON", () => { + const parsed = parseSettings("not-json{{{"); + expect(parsed).toEqual({ enabled: true, intervalMs: 240_000 }); + }); + + it("clamps non-positive interval to MIN_INTERVAL_MS", () => { + const parsed = parseSettings(JSON.stringify({ enabled: true, intervalMs: -500 })); + expect(parsed.intervalMs).toBe(1000); + }); + + it("uses default for NaN interval", () => { + const parsed = parseSettings(JSON.stringify({ enabled: true, intervalMs: Number.NaN })); + expect(parsed.intervalMs).toBe(240_000); + }); +}); |
