diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 13:08:38 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 13:08:38 +0900 |
| commit | ffbbcf692a97ec8648af39353b49f32896367207 (patch) | |
| tree | 2e2ddfd03d4a868f4a4ba12e20586cc03c37f90a /packages/cache-warming/src/pure.test.ts | |
| parent | 27fd0be36b2f6395249de5aacc86e41fe4e0207f (diff) | |
| download | dispatch-ffbbcf692a97ec8648af39353b49f32896367207.tar.gz dispatch-ffbbcf692a97ec8648af39353b49f32896367207.zip | |
feat(surfaces): NumberField + per-conversation surface scoping; cache-warming controls
Extend the surface framework so cache-warming exposes per-conversation controls:
- ui-contract: add NumberField (settable free-value numeric) to SurfaceField;
add optional conversationId to subscribe/unsubscribe/invoke + surface/update
- surface-registry: SurfaceContext { conversationId? } on getSpec/invoke (backward-compatible)
- transport-ws: thread conversationId; key subscriptions by (surfaceId, conversationId);
tag surface/update replies with conversationId
- cache-warming: per-conversation surface — Toggle(enabled) + Number(interval seconds,
cache-warming/set-interval) + Stat(last cache %); drop the currentConversationId closure
Global surfaces (surface-loaded-extensions) unchanged. 784 vitest + 109 bun = 893 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 | 135 |
1 files changed, 135 insertions, 0 deletions
diff --git a/packages/cache-warming/src/pure.test.ts b/packages/cache-warming/src/pure.test.ts index 820260b..1c912f2 100644 --- a/packages/cache-warming/src/pure.test.ts +++ b/packages/cache-warming/src/pure.test.ts @@ -1,9 +1,15 @@ import { describe, expect, it } from "vitest"; import type { ConversationState } from "./pure.js"; import { + buildConversationSpec, + buildDefaultSpec, computeCachePct, isTokenCurrent, + MIN_INTERVAL_MS, + msToSeconds, + parseIntervalPayload, parseSettings, + secondsToMs, serializeSettings, shouldWarm, } from "./pure.js"; @@ -107,3 +113,132 @@ describe("parseSettings/serializeSettings round-trip", () => { expect(parsed.intervalMs).toBe(240_000); }); }); + +describe("msToSeconds", () => { + it("converts ms to seconds, rounded", () => { + expect(msToSeconds(240_000)).toBe(240); + expect(msToSeconds(1500)).toBe(2); + expect(msToSeconds(1000)).toBe(1); + expect(msToSeconds(0)).toBe(0); + }); +}); + +describe("secondsToMs", () => { + it("converts seconds to ms, floors at MIN_INTERVAL_MS", () => { + expect(secondsToMs(240)).toBe(240_000); + expect(secondsToMs(1)).toBe(1000); + expect(secondsToMs(0.5)).toBe(MIN_INTERVAL_MS); + }); + + it("returns null for NaN / non-positive", () => { + expect(secondsToMs(Number.NaN)).toBeNull(); + expect(secondsToMs(0)).toBeNull(); + expect(secondsToMs(-5)).toBeNull(); + expect(secondsToMs(Number.POSITIVE_INFINITY)).toBeNull(); + }); +}); + +describe("parseIntervalPayload", () => { + it("accepts a bare positive number", () => { + expect(parseIntervalPayload(30)).toBe(30); + expect(parseIntervalPayload(1)).toBe(1); + }); + + it("accepts { value: number }", () => { + expect(parseIntervalPayload({ value: 30 })).toBe(30); + expect(parseIntervalPayload({ value: 1 })).toBe(1); + }); + + it("returns null for NaN / non-positive / wrong shape", () => { + expect(parseIntervalPayload(Number.NaN)).toBeNull(); + expect(parseIntervalPayload(0)).toBeNull(); + expect(parseIntervalPayload(-5)).toBeNull(); + expect(parseIntervalPayload("30")).toBeNull(); + expect(parseIntervalPayload({ value: "30" })).toBeNull(); + expect(parseIntervalPayload({})).toBeNull(); + expect(parseIntervalPayload(null)).toBeNull(); + expect(parseIntervalPayload(undefined)).toBeNull(); + }); +}); + +describe("buildConversationSpec", () => { + it("builds a per-conversation spec with toggle + number(interval) + last-% fields", () => { + const spec = buildConversationSpec(true, 240_000, 80); + expect(spec.id).toBe("cache-warming"); + expect(spec.region).toBe("side"); + expect(spec.title).toBe("Cache Warming"); + expect(spec.fields).toHaveLength(3); + + const toggle = spec.fields[0]; + expect(toggle).toEqual({ + kind: "toggle", + label: "Enabled", + value: true, + action: { actionId: "cache-warming/toggle" }, + }); + + const number = spec.fields[1]; + expect(number).toEqual({ + kind: "number", + label: "Refresh Interval", + value: 240, + min: 1, + step: 1, + unit: "s", + action: { actionId: "cache-warming/set-interval" }, + }); + + const stat = spec.fields[2]; + expect(stat).toEqual({ + kind: "stat", + label: "Last Cache %", + value: "80%", + }); + }); + + it("shows — when lastPct is null", () => { + const spec = buildConversationSpec(true, 240_000, null); + const stat = spec.fields[2]; + expect(stat).toEqual({ + kind: "stat", + label: "Last Cache %", + value: "—", + }); + }); + + it("reflects disabled state", () => { + const spec = buildConversationSpec(false, 120_000, 50); + const toggle = spec.fields[0]; + expect(toggle).toEqual({ + kind: "toggle", + label: "Enabled", + value: false, + action: { actionId: "cache-warming/toggle" }, + }); + const number = spec.fields[1]; + expect(number).toEqual({ + kind: "number", + label: "Refresh Interval", + value: 120, + min: 1, + step: 1, + unit: "s", + action: { actionId: "cache-warming/set-interval" }, + }); + }); +}); + +describe("buildDefaultSpec", () => { + it("returns a default spec with no conversationId", () => { + const spec = buildDefaultSpec(); + expect(spec.id).toBe("cache-warming"); + expect(spec.region).toBe("side"); + expect(spec.title).toBe("Cache Warming"); + expect(spec.fields).toHaveLength(1); + expect(spec.fields[0]).toEqual({ + kind: "stat", + label: "Status", + value: "No conversation focused", + }); + }); +}); |
