summaryrefslogtreecommitdiffhomepage
path: root/packages/cache-warming/src/pure.test.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-11 13:08:38 +0900
committerAdam Malczewski <[email protected]>2026-06-11 13:08:38 +0900
commitffbbcf692a97ec8648af39353b49f32896367207 (patch)
tree2e2ddfd03d4a868f4a4ba12e20586cc03c37f90a /packages/cache-warming/src/pure.test.ts
parent27fd0be36b2f6395249de5aacc86e41fe4e0207f (diff)
downloaddispatch-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.ts135
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",
+ });
+ });
+});