import type { StorageNamespace } from "@dispatch/kernel"; import { describe, expect, it } from "vitest"; import { applyConfigUpdate, createHeartbeatConfigStore, DEFAULT_HEARTBEAT_CONFIG, } from "./config-store.js"; function createMemoryStorage(): StorageNamespace { const data = new Map(); return { get: async (key) => data.get(key) ?? null, set: async (key, value) => { data.set(key, value); }, delete: async (key) => { data.delete(key); }, has: async (key) => data.has(key), keys: async (prefix) => { const all = [...data.keys()]; if (prefix === undefined) return all; return all.filter((k) => k.startsWith(prefix)); }, }; } describe("applyConfigUpdate (pure)", () => { it("leaves omitted fields unchanged from the default", () => { const next = applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, {}); expect(next).toEqual(DEFAULT_HEARTBEAT_CONFIG); }); it("applies provided fields", () => { const next = applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { enabled: true, systemPrompt: "you are a monitor", taskPrompt: "check for stuck chats", model: "opencode/gpt-4o", }); expect(next.enabled).toBe(true); expect(next.systemPrompt).toBe("you are a monitor"); expect(next.taskPrompt).toBe("check for stuck chats"); expect(next.model).toBe("opencode/gpt-4o"); // Untouched fields keep defaults. expect(next.intervalMinutes).toBe(30); expect(next.reasoningEffort).toBeNull(); }); it("clamps intervalMinutes to a minimum of 1", () => { expect( applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { intervalMinutes: 0 }).intervalMinutes, ).toBe(1); expect( applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { intervalMinutes: -5 }).intervalMinutes, ).toBe(1); expect( applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { intervalMinutes: 7 }).intervalMinutes, ).toBe(7); }); it("truncates a non-integer interval to an integer", () => { expect( applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { intervalMinutes: 12.9 }).intervalMinutes, ).toBe(12); }); it("clears reasoningEffort when null is passed", () => { const withEffort = applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { reasoningEffort: "high" }); expect(withEffort.reasoningEffort).toBe("high"); const cleared = applyConfigUpdate(withEffort, { reasoningEffort: null }); expect(cleared.reasoningEffort).toBeNull(); }); it("treats an absent reasoningEffort as unchanged (distinct from null)", () => { const withEffort = applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { reasoningEffort: "low" }); const untouched = applyConfigUpdate(withEffort, { enabled: true }); expect(untouched.reasoningEffort).toBe("low"); }); it("defaults inactiveOnly to true (the heartbeat is quiet by default while the workspace is busy)", () => { expect(DEFAULT_HEARTBEAT_CONFIG.inactiveOnly).toBe(true); }); it("applies an inactiveOnly update", () => { const next = applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { inactiveOnly: false }); expect(next.inactiveOnly).toBe(false); }); it("leaves inactiveOnly unchanged when absent from the update", () => { const off = applyConfigUpdate(DEFAULT_HEARTBEAT_CONFIG, { inactiveOnly: false }); const untouched = applyConfigUpdate(off, { enabled: true }); expect(untouched.inactiveOnly).toBe(false); }); }); describe("createHeartbeatConfigStore", () => { it("returns the default config for an unknown workspace", async () => { const store = createHeartbeatConfigStore(createMemoryStorage()); expect(await store.get("ws-1")).toEqual(DEFAULT_HEARTBEAT_CONFIG); }); it("persists and round-trips an update", async () => { const storage = createMemoryStorage(); const store = createHeartbeatConfigStore(storage); const next = await store.update("ws-1", { enabled: true, intervalMinutes: 5 }); expect(next.enabled).toBe(true); expect(next.intervalMinutes).toBe(5); // A fresh store over the same storage reads the persisted value. const store2 = createHeartbeatConfigStore(storage); expect(await store2.get("ws-1")).toEqual(next); }); it("applies a partial update on top of existing config", async () => { const storage = createMemoryStorage(); const store = createHeartbeatConfigStore(storage); await store.update("ws-1", { enabled: true, systemPrompt: "a", taskPrompt: "b" }); const next = await store.update("ws-1", { taskPrompt: "c" }); expect(next.enabled).toBe(true); expect(next.systemPrompt).toBe("a"); expect(next.taskPrompt).toBe("c"); }); it("round-trips inactiveOnly through persistence", async () => { const storage = createMemoryStorage(); const store = createHeartbeatConfigStore(storage); const next = await store.update("ws-1", { inactiveOnly: false }); expect(next.inactiveOnly).toBe(false); const store2 = createHeartbeatConfigStore(storage); expect((await store2.get("ws-1")).inactiveOnly).toBe(false); }); it("defaults inactiveOnly to true for legacy configs persisted before the field existed", async () => { const storage = createMemoryStorage(); // Simulate a config written by an older build that had no inactiveOnly // field (a pre-inactive-only heartbeat config). await storage.set("config:ws-1", JSON.stringify({ enabled: true, intervalMinutes: 5 })); const store = createHeartbeatConfigStore(storage); const config = await store.get("ws-1"); expect(config.enabled).toBe(true); expect(config.intervalMinutes).toBe(5); // The missing field defaults ON (feature on by default) — never `undefined`. expect(config.inactiveOnly).toBe(true); }); it("lists persisted workspace ids", async () => { const store = createHeartbeatConfigStore(createMemoryStorage()); await store.update("ws-b", { enabled: true }); await store.update("ws-a", { enabled: false }); expect(await store.listWorkspaceIds()).toEqual(["ws-a", "ws-b"]); }); });