import { beforeEach, describe, expect, it, vi } from "vitest"; // In-memory fake for the settings table — mounted before the module under // test is imported (vi.mock is hoisted). const fakeSettings = new Map(); vi.mock("../../src/db/settings.js", () => ({ getSetting: vi.fn((key: string) => fakeSettings.get(key) ?? null), setSetting: vi.fn((key: string, value: string) => { fakeSettings.set(key, value); }), deleteSetting: vi.fn((key: string) => { fakeSettings.delete(key); }), })); const { clearNtfyConfig, defaultNtfyConfig, loadNtfyConfig, normalizeNtfyConfig, NTFY_CONFIG_KEY, redactNtfyConfig, saveNtfyConfig, } = await import("../../src/notifications/config.js"); describe("defaultNtfyConfig", () => { it("disables notifications and ships sane per-event defaults", () => { const cfg = defaultNtfyConfig(); expect(cfg.enabled).toBe(false); expect(cfg.topic).toBe(""); expect(cfg.authToken).toBe(""); expect(cfg.events["turn-completed"]).toBe(true); expect(cfg.events["turn-error"]).toBe(true); expect(cfg.events["permission-required"]).toBe(true); expect(cfg.events["agent-spawned"]).toBe(false); expect(cfg.notifySubagents).toBe(false); }); }); describe("normalizeNtfyConfig", () => { it("returns defaults for non-object input", () => { expect(normalizeNtfyConfig(null)).toEqual(defaultNtfyConfig()); expect(normalizeNtfyConfig(undefined)).toEqual(defaultNtfyConfig()); expect(normalizeNtfyConfig(42)).toEqual(defaultNtfyConfig()); }); it("fills in missing event toggles with defaults (newly-added types default OFF)", () => { const normalized = normalizeNtfyConfig({ enabled: true, topic: "https://ntfy.sh/x", events: { "turn-completed": false }, }); expect(normalized.events["turn-completed"]).toBe(false); // Defaults preserved for fields the persisted blob doesn't have. expect(normalized.events["turn-error"]).toBe(true); expect(normalized.events["agent-spawned"]).toBe(false); }); it("ignores extraneous fields and wrong-typed values", () => { const normalized = normalizeNtfyConfig({ enabled: "yes", // wrong type ⇒ default topic: 42, // wrong type ⇒ default authToken: null, // wrong type ⇒ default events: { "turn-completed": "no", bogus: true }, extra: "ignored", }); expect(normalized.enabled).toBe(false); expect(normalized.topic).toBe(""); expect(normalized.authToken).toBe(""); expect(normalized.events["turn-completed"]).toBe(true); // default kept expect((normalized.events as Record).bogus).toBeUndefined(); }); }); describe("normalizeNtfyConfig — notifySubagents", () => { it("defaults notifySubagents to false when absent", () => { const normalized = normalizeNtfyConfig({ enabled: true, topic: "https://ntfy.sh/x", }); expect(normalized.notifySubagents).toBe(false); }); it("respects an explicit notifySubagents=true", () => { const normalized = normalizeNtfyConfig({ enabled: true, topic: "https://ntfy.sh/x", notifySubagents: true, }); expect(normalized.notifySubagents).toBe(true); }); it("falls back to default when notifySubagents is wrong-typed", () => { const normalized = normalizeNtfyConfig({ enabled: true, topic: "https://ntfy.sh/x", notifySubagents: "yes" as unknown, }); expect(normalized.notifySubagents).toBe(false); }); }); describe("load/save round-trip", () => { beforeEach(() => { fakeSettings.clear(); }); it("returns defaults when nothing is persisted", () => { expect(loadNtfyConfig()).toEqual(defaultNtfyConfig()); }); it("round-trips a complete config", () => { const cfg = { enabled: true, topic: "https://ntfy.sh/team", authToken: "tk_abc", events: { "turn-completed": false, "turn-error": true, "permission-required": true, "agent-spawned": true, }, notifySubagents: true, } as const; saveNtfyConfig({ ...cfg }); const loaded = loadNtfyConfig(); expect(loaded).toEqual(cfg); // Persisted as a JSON string under the documented key. expect(fakeSettings.has(NTFY_CONFIG_KEY)).toBe(true); }); it("returns defaults when stored JSON is corrupt", () => { fakeSettings.set(NTFY_CONFIG_KEY, "{ not json"); expect(loadNtfyConfig()).toEqual(defaultNtfyConfig()); }); it("clearNtfyConfig removes the persisted entry", () => { saveNtfyConfig({ ...defaultNtfyConfig(), enabled: true, topic: "https://ntfy.sh/x" }); expect(fakeSettings.has(NTFY_CONFIG_KEY)).toBe(true); clearNtfyConfig(); expect(fakeSettings.has(NTFY_CONFIG_KEY)).toBe(false); }); }); describe("redactNtfyConfig", () => { it("strips authToken and surfaces a hasAuthToken flag", () => { const cfg = { ...defaultNtfyConfig(), authToken: "tk_secret" }; const redacted = redactNtfyConfig(cfg); expect(redacted.authToken).toBe(""); expect(redacted.hasAuthToken).toBe(true); }); it("hasAuthToken is false for blank tokens", () => { expect(redactNtfyConfig({ ...defaultNtfyConfig(), authToken: "" }).hasAuthToken).toBe(false); expect(redactNtfyConfig({ ...defaultNtfyConfig(), authToken: " " }).hasAuthToken).toBe(false); }); });