1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
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<string, string>();
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<string, boolean>).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);
});
});
|