summaryrefslogtreecommitdiffhomepage
path: root/packages/cache-warming/src/pure.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 12:23:06 +0900
committerAdam Malczewski <[email protected]>2026-06-11 12:23:06 +0900
commitc2b4c05d91fa88b8d02c055a0e15c22abd8e21f3 (patch)
tree3f7c2feddbe697a79abd952bb80ed0e01dac0a7a /packages/cache-warming/src/pure.test.ts
parentf6b45507210e04e9884256b0132900640de4334b (diff)
downloaddispatch-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.ts109
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);
+ });
+});