diff options
Diffstat (limited to 'src/features/cache-warming/logic/view-model.test.ts')
| -rw-r--r-- | src/features/cache-warming/logic/view-model.test.ts | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/src/features/cache-warming/logic/view-model.test.ts b/src/features/cache-warming/logic/view-model.test.ts new file mode 100644 index 0000000..3d6f6d0 --- /dev/null +++ b/src/features/cache-warming/logic/view-model.test.ts @@ -0,0 +1,220 @@ +import type { SurfaceSpec } from "@dispatch/ui-contract"; +import { describe, expect, it } from "vitest"; +import { + clampMinutes, + clampSeconds, + colorClass, + formatCountdown, + formatWarmLabel, + fromMinSec, + initialWarmingState, + observeWarm, + parseControls, + parsePct, + secondsUntilNext, + statusForPct, + toMinSec, +} from "./view-model"; + +const spec = (fields: SurfaceSpec["fields"]): SurfaceSpec => ({ + id: "cache-warming", + region: "side", + title: "Cache Warming", + fields, +}); + +describe("parsePct", () => { + it("parses a percentage string", () => { + expect(parsePct("100%")).toBe(100); + expect(parsePct("93 %")).toBe(93); + expect(parsePct("0%")).toBe(0); + }); + it("returns null for a dash / non-numeric", () => { + expect(parsePct("—")).toBeNull(); + expect(parsePct("n/a")).toBeNull(); + }); +}); + +describe("parseControls", () => { + it("returns empty defaults for a null spec", () => { + const c = parseControls(null); + expect(c).toEqual({ + enabled: false, + toggleActionId: null, + intervalSeconds: 0, + setIntervalActionId: null, + lastPct: null, + retentionPct: null, + nextWarmAt: null, + lastWarmAt: null, + }); + }); + + it("extracts toggle / number / both stats / timer by kind", () => { + const c = parseControls( + spec([ + { + kind: "toggle", + label: "Enabled", + value: true, + action: { actionId: "cache-warming/toggle" }, + }, + { + kind: "number", + label: "Interval", + value: 240, + unit: "s", + action: { actionId: "cache-warming/set-interval" }, + }, + { kind: "stat", label: "Last cache rate", value: "61%" }, + { kind: "stat", label: "Cache retention", value: "100%" }, + { + kind: "custom", + rendererId: "cache-warming-timer", + payload: { nextWarmAt: 1_700_000_240_000, lastWarmAt: 1_700_000_000_000 }, + }, + ]), + ); + expect(c).toEqual({ + enabled: true, + toggleActionId: "cache-warming/toggle", + intervalSeconds: 240, + setIntervalActionId: "cache-warming/set-interval", + lastPct: 61, + retentionPct: 100, + nextWarmAt: 1_700_000_240_000, + lastWarmAt: 1_700_000_000_000, + }); + }); + + it("tells the retention stat apart from the rate stat by label", () => { + const c = parseControls( + spec([ + { kind: "stat", label: "Cache retention", value: "100%" }, + { kind: "stat", label: "Last cache rate", value: "61%" }, + ]), + ); + expect(c.retentionPct).toBe(100); + expect(c.lastPct).toBe(61); + }); + + it("treats a '—' stat as no pct", () => { + const c = parseControls(spec([{ kind: "stat", label: "Last cache rate", value: "—" }])); + expect(c.lastPct).toBeNull(); + }); + + it("ignores an unknown custom renderer and a malformed timer payload", () => { + const c = parseControls( + spec([ + { kind: "custom", rendererId: "something-else", payload: { nextWarmAt: 5 } }, + { kind: "custom", rendererId: "cache-warming-timer", payload: "nope" }, + ]), + ); + expect(c.nextWarmAt).toBeNull(); + expect(c.lastWarmAt).toBeNull(); + }); +}); + +describe("interval ↔ min/sec", () => { + it("clampSeconds caps at 0..59", () => { + expect(clampSeconds(75)).toBe(59); + expect(clampSeconds(-3)).toBe(0); + expect(clampSeconds(30)).toBe(30); + expect(clampSeconds(Number.NaN)).toBe(0); + }); + it("clampMinutes floors at 0", () => { + expect(clampMinutes(-1)).toBe(0); + expect(clampMinutes(4)).toBe(4); + }); + it("toMinSec splits total seconds", () => { + expect(toMinSec(240)).toEqual({ minutes: 4, seconds: 0 }); + expect(toMinSec(125)).toEqual({ minutes: 2, seconds: 5 }); + expect(toMinSec(45)).toEqual({ minutes: 0, seconds: 45 }); + }); + it("fromMinSec combines (clamping seconds to 59)", () => { + expect(fromMinSec(4, 0)).toBe(240); + expect(fromMinSec(2, 5)).toBe(125); + expect(fromMinSec(1, 75)).toBe(119); // 75s clamped to 59 + }); +}); + +describe("status + formatting", () => { + it("statusForPct buckets high/mid/low", () => { + expect(statusForPct(100)).toBe("success"); + expect(statusForPct(80)).toBe("success"); + expect(statusForPct(60)).toBe("warning"); + expect(statusForPct(40)).toBe("warning"); + expect(statusForPct(10)).toBe("error"); + }); + it("colorClass maps to literal DaisyUI classes", () => { + expect(colorClass("success")).toBe("text-success"); + expect(colorClass("warning")).toBe("text-warning"); + expect(colorClass("error")).toBe("text-error"); + }); + it("formatWarmLabel matches the manual-warm phrasing", () => { + expect(formatWarmLabel(100)).toBe("Warmed — 100% cache hit"); + expect(formatWarmLabel(92.6)).toBe("Warmed — 93% cache hit"); + }); + it("formatCountdown renders s and m:ss", () => { + expect(formatCountdown(9)).toBe("9s"); + expect(formatCountdown(59)).toBe("59s"); + expect(formatCountdown(60)).toBe("1:00"); + expect(formatCountdown(185)).toBe("3:05"); + expect(formatCountdown(-5)).toBe("0s"); + }); +}); + +describe("warming history reducer (observeWarm)", () => { + it("starts empty", () => { + const s = initialWarmingState(); + expect(s.history).toEqual([]); + expect(s.lastWarmAt).toBeNull(); + }); + + it("records a new entry on each new authoritative lastWarmAt", () => { + let s = initialWarmingState(); + s = observeWarm(s, 1000, 100); + s = observeWarm(s, 2000, 90); + expect(s.history).toEqual([ + { pct: 90, at: 2000 }, + { pct: 100, at: 1000 }, + ]); + expect(s.lastWarmAt).toBe(2000); + }); + + it("de-duplicates on the timestamp, not the pct (a re-pushed surface → no dup)", () => { + let s = initialWarmingState(); + s = observeWarm(s, 1000, 100); // warm + s = observeWarm(s, 1000, 100); // toggle/interval re-push, same lastWarmAt → skip + expect(s.history).toHaveLength(1); + }); + + it("records two warms with the SAME pct (distinct timestamps both count)", () => { + let s = initialWarmingState(); + s = observeWarm(s, 1000, 100); + s = observeWarm(s, 2000, 100); + expect(s.history.map((e) => e.at)).toEqual([2000, 1000]); + }); + + it("ignores a null lastWarmAt; a null pct advances the key without an entry", () => { + let s = initialWarmingState(); + s = observeWarm(s, null, 100); + expect(s.history).toEqual([]); + s = observeWarm(s, 1000, null); + expect(s.history).toEqual([]); + expect(s.lastWarmAt).toBe(1000); + }); +}); + +describe("secondsUntilNext (authoritative, from nextWarmAt)", () => { + it("is null when nothing is scheduled (nextWarmAt null)", () => { + expect(secondsUntilNext(null, 5000)).toBeNull(); + }); + + it("counts down to nextWarmAt, floored at 0", () => { + expect(secondsUntilNext(10_000, 10_000)).toBe(0); + expect(secondsUntilNext(250_000, 10_000)).toBe(240); + expect(secondsUntilNext(70_000, 10_000)).toBe(60); + expect(secondsUntilNext(5_000, 999_999)).toBe(0); // already past + }); +}); |
