import assert from "node:assert/strict"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, it } from "node:test"; import { readNotifySettings } from "../src/settings.ts"; let tempDir: string | undefined; afterEach(() => { if (tempDir) { rmSync(tempDir, { recursive: true, force: true }); tempDir = undefined; } }); function configPath(contents: string): string { tempDir = mkdtempSync(join(tmpdir(), "pi-notify-settings-")); const path = join(tempDir, "pi-notify.json"); writeFileSync(path, contents); return path; } const expectedDefaults = { enabled: true, title: "π - {project}", body: "{message}", messageSource: "assistant", messageMax: 80, minDurationMs: 3_000, visibility: "always", action: "focus", soundCommand: undefined, }; describe("readNotifySettings", () => { it("returns all defaults when the config file is missing", () => { assert.deepEqual(readNotifySettings("/does/not/exist/pi-notify.json"), expectedDefaults); }); it("returns all defaults for malformed or non-object JSON", () => { for (const contents of ["not json", "[1,2]", "\"hi\"", "42"]) { assert.deepEqual(readNotifySettings(configPath(contents)), expectedDefaults, contents); } }); it("adopts valid values from the config file", () => { const settings = readNotifySettings(configPath(JSON.stringify({ enabled: false, title: "π {status_icon} · {project}", body: "{status}", messageSource: "none", messageMax: 120, minSeconds: 30, visibility: "unfocused", action: "none", soundCommand: "afplay /done.aiff", }))); assert.deepEqual(settings, { enabled: false, title: "π {status_icon} · {project}", body: "{status}", messageSource: "none", messageMax: 120, minDurationMs: 30_000, visibility: "unfocused", action: "none", soundCommand: "afplay /done.aiff", }); }); it("falls back per-field when values have the wrong type or value", () => { const settings = readNotifySettings(configPath(JSON.stringify({ enabled: {}, title: "", body: null, messageSource: ["assistant"], messageMax: "bogus", minSeconds: "bogus", visibility: "bogus", action: {}, }))); assert.deepEqual(settings, expectedDefaults); }); it("clamps messageMax and minSeconds to their bounded ranges", () => { const settings = readNotifySettings(configPath(JSON.stringify({ messageMax: 999, minSeconds: 4000 }))); assert.equal(settings.messageMax, 500); assert.equal(settings.minDurationMs, 3_600_000); const floor = readNotifySettings(configPath(JSON.stringify({ messageMax: -5, minSeconds: -1 }))); assert.equal(floor.messageMax, 0); assert.equal(floor.minDurationMs, 0); }); it("treats an empty or whitespace soundCommand as absent and trims values", () => { for (const value of ["", " "]) { assert.equal(readNotifySettings(configPath(JSON.stringify({ soundCommand: value }))).soundCommand, undefined); } assert.equal( readNotifySettings(configPath(JSON.stringify({ soundCommand: " afplay /done.aiff " }))).soundCommand, "afplay /done.aiff", ); }); });