@calltelemetry/openclaw-linear 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -17
- package/index.ts +18 -22
- package/openclaw.plugin.json +35 -2
- package/package.json +1 -1
- package/prompts.yaml +47 -0
- package/src/api/linear-api.ts +180 -9
- package/src/infra/cli.ts +214 -0
- package/src/infra/doctor.test.ts +399 -0
- package/src/infra/doctor.ts +759 -0
- package/src/infra/notify.test.ts +357 -108
- package/src/infra/notify.ts +114 -35
- package/src/pipeline/planner.test.ts +334 -0
- package/src/pipeline/planner.ts +282 -0
- package/src/pipeline/planning-state.test.ts +236 -0
- package/src/pipeline/planning-state.ts +216 -0
- package/src/pipeline/webhook.ts +69 -17
- package/src/tools/planner-tools.test.ts +535 -0
- package/src/tools/planner-tools.ts +450 -0
package/src/infra/notify.test.ts
CHANGED
|
@@ -1,169 +1,418 @@
|
|
|
1
1
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
2
|
import {
|
|
3
3
|
createNoopNotifier,
|
|
4
|
-
|
|
4
|
+
createNotifierFromConfig,
|
|
5
|
+
formatMessage,
|
|
6
|
+
sendToTarget,
|
|
7
|
+
parseNotificationsConfig,
|
|
8
|
+
type NotifyKind,
|
|
5
9
|
type NotifyPayload,
|
|
10
|
+
type NotifyTarget,
|
|
6
11
|
} from "./notify.js";
|
|
7
12
|
|
|
8
13
|
// ---------------------------------------------------------------------------
|
|
9
|
-
//
|
|
14
|
+
// formatMessage
|
|
10
15
|
// ---------------------------------------------------------------------------
|
|
11
16
|
|
|
12
|
-
describe("
|
|
13
|
-
it("returns function that resolves without error", async () => {
|
|
14
|
-
const notify = createNoopNotifier();
|
|
15
|
-
await expect(notify("dispatch", {
|
|
16
|
-
identifier: "API-1",
|
|
17
|
-
title: "test",
|
|
18
|
-
status: "dispatched",
|
|
19
|
-
})).resolves.toBeUndefined();
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Discord notifier
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
describe("createDiscordNotifier", () => {
|
|
28
|
-
const botToken = "test-bot-token";
|
|
29
|
-
const channelId = "123456";
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
vi.restoreAllMocks();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function stubFetch(): { getCalls: () => { url: string; body: any }[] } {
|
|
36
|
-
const calls: { url: string; body: any }[] = [];
|
|
37
|
-
vi.stubGlobal("fetch", vi.fn(async (url: string, opts: any) => {
|
|
38
|
-
calls.push({ url, body: JSON.parse(opts.body) });
|
|
39
|
-
return { ok: true, status: 200 } as Response;
|
|
40
|
-
}));
|
|
41
|
-
return { getCalls: () => calls };
|
|
42
|
-
}
|
|
43
|
-
|
|
17
|
+
describe("formatMessage", () => {
|
|
44
18
|
const basePayload: NotifyPayload = {
|
|
45
19
|
identifier: "API-42",
|
|
46
20
|
title: "Fix auth",
|
|
47
21
|
status: "dispatched",
|
|
48
22
|
};
|
|
49
23
|
|
|
50
|
-
it("formats dispatch message",
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
expect(msg).toContain("dispatched");
|
|
58
|
-
expect(msg).toContain("Fix auth");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("formats working message with attempt", async () => {
|
|
62
|
-
const { getCalls } = stubFetch();
|
|
63
|
-
const notify = createDiscordNotifier(botToken, channelId);
|
|
64
|
-
await notify("working", { ...basePayload, status: "working", attempt: 1 });
|
|
65
|
-
const msg = getCalls()[0].body.content;
|
|
24
|
+
it("formats dispatch message", () => {
|
|
25
|
+
const msg = formatMessage("dispatch", basePayload);
|
|
26
|
+
expect(msg).toBe("API-42 dispatched — Fix auth");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("formats working message with attempt", () => {
|
|
30
|
+
const msg = formatMessage("working", { ...basePayload, attempt: 1 });
|
|
66
31
|
expect(msg).toContain("worker started");
|
|
67
32
|
expect(msg).toContain("attempt 1");
|
|
68
33
|
});
|
|
69
34
|
|
|
70
|
-
it("formats
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
35
|
+
it("formats auditing message", () => {
|
|
36
|
+
const msg = formatMessage("auditing", basePayload);
|
|
37
|
+
expect(msg).toContain("audit in progress");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("formats audit_pass message", () => {
|
|
41
|
+
const msg = formatMessage("audit_pass", basePayload);
|
|
75
42
|
expect(msg).toContain("passed audit");
|
|
76
43
|
expect(msg).toContain("PR ready");
|
|
77
44
|
});
|
|
78
45
|
|
|
79
|
-
it("formats audit_fail message with gaps",
|
|
80
|
-
const
|
|
81
|
-
const notify = createDiscordNotifier(botToken, channelId);
|
|
82
|
-
await notify("audit_fail", {
|
|
46
|
+
it("formats audit_fail message with gaps", () => {
|
|
47
|
+
const msg = formatMessage("audit_fail", {
|
|
83
48
|
...basePayload,
|
|
84
|
-
status: "working",
|
|
85
49
|
attempt: 1,
|
|
86
50
|
verdict: { pass: false, gaps: ["no tests", "missing validation"] },
|
|
87
51
|
});
|
|
88
|
-
const msg = getCalls()[0].body.content;
|
|
89
52
|
expect(msg).toContain("failed audit");
|
|
53
|
+
expect(msg).toContain("attempt 1");
|
|
90
54
|
expect(msg).toContain("no tests");
|
|
91
55
|
expect(msg).toContain("missing validation");
|
|
92
56
|
});
|
|
93
57
|
|
|
94
|
-
it("formats
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
58
|
+
it("formats audit_fail with default gaps text", () => {
|
|
59
|
+
const msg = formatMessage("audit_fail", {
|
|
60
|
+
...basePayload,
|
|
61
|
+
attempt: 0,
|
|
62
|
+
verdict: { pass: false },
|
|
63
|
+
});
|
|
64
|
+
expect(msg).toContain("unspecified");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("formats escalation message with reason", () => {
|
|
68
|
+
const msg = formatMessage("escalation", {
|
|
98
69
|
...basePayload,
|
|
99
|
-
status: "stuck",
|
|
100
70
|
reason: "audit failed 3x",
|
|
101
71
|
});
|
|
102
|
-
const msg = getCalls()[0].body.content;
|
|
103
72
|
expect(msg).toContain("needs human review");
|
|
104
73
|
expect(msg).toContain("audit failed 3x");
|
|
105
74
|
});
|
|
106
75
|
|
|
107
|
-
it("formats
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
76
|
+
it("formats stuck message", () => {
|
|
77
|
+
const msg = formatMessage("stuck", {
|
|
78
|
+
...basePayload,
|
|
79
|
+
reason: "stale 2h",
|
|
80
|
+
});
|
|
81
|
+
expect(msg).toContain("stuck");
|
|
82
|
+
expect(msg).toContain("stale 2h");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("formats watchdog_kill with attempt", () => {
|
|
86
|
+
const msg = formatMessage("watchdog_kill", {
|
|
111
87
|
...basePayload,
|
|
112
|
-
status: "stuck",
|
|
113
88
|
attempt: 0,
|
|
114
89
|
reason: "no I/O for 120s",
|
|
115
90
|
});
|
|
116
|
-
const msg = getCalls()[0].body.content;
|
|
117
91
|
expect(msg).toContain("killed by watchdog");
|
|
118
92
|
expect(msg).toContain("no I/O for 120s");
|
|
119
|
-
expect(msg).toContain("attempt 0");
|
|
93
|
+
expect(msg).toContain("Retrying (attempt 0)");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("formats watchdog_kill without attempt", () => {
|
|
97
|
+
const msg = formatMessage("watchdog_kill", {
|
|
98
|
+
...basePayload,
|
|
99
|
+
reason: "timeout",
|
|
100
|
+
});
|
|
101
|
+
expect(msg).toContain("Will retry.");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("handles unknown kind via default case", () => {
|
|
105
|
+
const msg = formatMessage("unknown_kind" as NotifyKind, basePayload);
|
|
106
|
+
expect(msg).toContain("API-42");
|
|
107
|
+
expect(msg).toContain("unknown_kind");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// sendToTarget
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
describe("sendToTarget", () => {
|
|
116
|
+
function mockRuntime(): any {
|
|
117
|
+
return {
|
|
118
|
+
channel: {
|
|
119
|
+
discord: {
|
|
120
|
+
sendMessageDiscord: vi.fn(async () => {}),
|
|
121
|
+
},
|
|
122
|
+
slack: {
|
|
123
|
+
sendMessageSlack: vi.fn(async () => ({ messageId: "ts-1", channelId: "C999" })),
|
|
124
|
+
},
|
|
125
|
+
telegram: {
|
|
126
|
+
sendMessageTelegram: vi.fn(async () => {}),
|
|
127
|
+
},
|
|
128
|
+
signal: {
|
|
129
|
+
sendMessageSignal: vi.fn(async () => {}),
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
afterEach(() => {
|
|
136
|
+
vi.restoreAllMocks();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("routes discord target to sendMessageDiscord", async () => {
|
|
140
|
+
const runtime = mockRuntime();
|
|
141
|
+
const target: NotifyTarget = { channel: "discord", target: "123456" };
|
|
142
|
+
await sendToTarget(target, "test message", runtime);
|
|
143
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledWith("123456", "test message");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("routes slack target to sendMessageSlack with accountId", async () => {
|
|
147
|
+
const runtime = mockRuntime();
|
|
148
|
+
const target: NotifyTarget = { channel: "slack", target: "C-100", accountId: "acct-x" };
|
|
149
|
+
await sendToTarget(target, "test message", runtime);
|
|
150
|
+
expect(runtime.channel.slack.sendMessageSlack).toHaveBeenCalledWith(
|
|
151
|
+
"C-100",
|
|
152
|
+
"test message",
|
|
153
|
+
{ accountId: "acct-x" },
|
|
154
|
+
);
|
|
120
155
|
});
|
|
121
156
|
|
|
122
|
-
it("
|
|
123
|
-
|
|
124
|
-
|
|
157
|
+
it("routes slack target without accountId", async () => {
|
|
158
|
+
const runtime = mockRuntime();
|
|
159
|
+
const target: NotifyTarget = { channel: "slack", target: "C-200" };
|
|
160
|
+
await sendToTarget(target, "test message", runtime);
|
|
161
|
+
expect(runtime.channel.slack.sendMessageSlack).toHaveBeenCalledWith(
|
|
162
|
+
"C-200",
|
|
163
|
+
"test message",
|
|
164
|
+
{ accountId: undefined },
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("routes telegram target to sendMessageTelegram with silent", async () => {
|
|
169
|
+
const runtime = mockRuntime();
|
|
170
|
+
const target: NotifyTarget = { channel: "telegram", target: "-100388" };
|
|
171
|
+
await sendToTarget(target, "test message", runtime);
|
|
172
|
+
expect(runtime.channel.telegram.sendMessageTelegram).toHaveBeenCalledWith(
|
|
173
|
+
"-100388",
|
|
174
|
+
"test message",
|
|
175
|
+
{ silent: true },
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("routes signal target to sendMessageSignal", async () => {
|
|
180
|
+
const runtime = mockRuntime();
|
|
181
|
+
const target: NotifyTarget = { channel: "signal", target: "+1234567890" };
|
|
182
|
+
await sendToTarget(target, "test message", runtime);
|
|
183
|
+
expect(runtime.channel.signal.sendMessageSignal).toHaveBeenCalledWith("+1234567890", "test message");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("falls back to CLI for unknown channels", async () => {
|
|
187
|
+
const runtime = mockRuntime();
|
|
188
|
+
const target: NotifyTarget = { channel: "matrix", target: "!room:server" };
|
|
189
|
+
|
|
190
|
+
const { execFileSync } = await import("node:child_process");
|
|
191
|
+
vi.mock("node:child_process", () => ({
|
|
192
|
+
execFileSync: vi.fn(),
|
|
125
193
|
}));
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
|
|
195
|
+
// Since the dynamic import is already cached, we test that it doesn't call any known channel
|
|
196
|
+
// and doesn't throw for an unknown channel type
|
|
197
|
+
try {
|
|
198
|
+
await sendToTarget(target, "test message", runtime);
|
|
199
|
+
} catch {
|
|
200
|
+
// CLI fallback may fail in test env — that's expected
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// None of the known channels should have been called
|
|
204
|
+
expect(runtime.channel.discord.sendMessageDiscord).not.toHaveBeenCalled();
|
|
205
|
+
expect(runtime.channel.slack.sendMessageSlack).not.toHaveBeenCalled();
|
|
206
|
+
expect(runtime.channel.telegram.sendMessageTelegram).not.toHaveBeenCalled();
|
|
207
|
+
expect(runtime.channel.signal.sendMessageSignal).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// parseNotificationsConfig
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
describe("parseNotificationsConfig", () => {
|
|
216
|
+
it("returns empty targets for undefined config", () => {
|
|
217
|
+
const config = parseNotificationsConfig(undefined);
|
|
218
|
+
expect(config.targets).toEqual([]);
|
|
219
|
+
expect(config.events).toEqual({});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("returns empty targets for config without notifications", () => {
|
|
223
|
+
const config = parseNotificationsConfig({ enabled: true });
|
|
224
|
+
expect(config.targets).toEqual([]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("parses targets and events", () => {
|
|
228
|
+
const config = parseNotificationsConfig({
|
|
229
|
+
notifications: {
|
|
230
|
+
targets: [{ channel: "discord", target: "123" }],
|
|
231
|
+
events: { auditing: false },
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
expect(config.targets).toHaveLength(1);
|
|
235
|
+
expect(config.targets![0].channel).toBe("discord");
|
|
236
|
+
expect(config.events?.auditing).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// createNotifierFromConfig
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
describe("createNotifierFromConfig", () => {
|
|
245
|
+
function mockRuntime(): any {
|
|
246
|
+
return {
|
|
247
|
+
channel: {
|
|
248
|
+
discord: {
|
|
249
|
+
sendMessageDiscord: vi.fn(async () => {}),
|
|
250
|
+
},
|
|
251
|
+
slack: {
|
|
252
|
+
sendMessageSlack: vi.fn(async () => ({ messageId: "ts-1", channelId: "C999" })),
|
|
253
|
+
},
|
|
254
|
+
telegram: {
|
|
255
|
+
sendMessageTelegram: vi.fn(async () => {}),
|
|
256
|
+
},
|
|
257
|
+
signal: {
|
|
258
|
+
sendMessageSignal: vi.fn(async () => {}),
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const basePayload: NotifyPayload = {
|
|
265
|
+
identifier: "CFG-1",
|
|
266
|
+
title: "Config test",
|
|
267
|
+
status: "dispatched",
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
afterEach(() => {
|
|
271
|
+
vi.restoreAllMocks();
|
|
131
272
|
});
|
|
132
273
|
|
|
133
|
-
it("
|
|
134
|
-
const
|
|
135
|
-
const notify =
|
|
274
|
+
it("returns noop when no targets configured", async () => {
|
|
275
|
+
const runtime = mockRuntime();
|
|
276
|
+
const notify = createNotifierFromConfig({}, runtime);
|
|
136
277
|
await notify("dispatch", basePayload);
|
|
137
|
-
expect(
|
|
278
|
+
expect(runtime.channel.discord.sendMessageDiscord).not.toHaveBeenCalled();
|
|
279
|
+
expect(runtime.channel.slack.sendMessageSlack).not.toHaveBeenCalled();
|
|
138
280
|
});
|
|
139
281
|
|
|
140
|
-
it("
|
|
141
|
-
const
|
|
142
|
-
const notify =
|
|
143
|
-
await notify("
|
|
144
|
-
|
|
145
|
-
expect(msg).toContain("audit in progress");
|
|
282
|
+
it("returns noop when targets array is empty", async () => {
|
|
283
|
+
const runtime = mockRuntime();
|
|
284
|
+
const notify = createNotifierFromConfig({ notifications: { targets: [] } }, runtime);
|
|
285
|
+
await notify("dispatch", basePayload);
|
|
286
|
+
expect(runtime.channel.discord.sendMessageDiscord).not.toHaveBeenCalled();
|
|
146
287
|
});
|
|
147
288
|
|
|
148
|
-
it("
|
|
149
|
-
const
|
|
150
|
-
const notify =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
289
|
+
it("sends to single discord target", async () => {
|
|
290
|
+
const runtime = mockRuntime();
|
|
291
|
+
const notify = createNotifierFromConfig({
|
|
292
|
+
notifications: {
|
|
293
|
+
targets: [{ channel: "discord", target: "D-100" }],
|
|
294
|
+
},
|
|
295
|
+
}, runtime);
|
|
296
|
+
await notify("dispatch", basePayload);
|
|
297
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledOnce();
|
|
298
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledWith(
|
|
299
|
+
"D-100",
|
|
300
|
+
expect.stringContaining("CFG-1"),
|
|
301
|
+
);
|
|
155
302
|
});
|
|
156
303
|
|
|
157
|
-
it("
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
304
|
+
it("sends to single slack target with accountId", async () => {
|
|
305
|
+
const runtime = mockRuntime();
|
|
306
|
+
const notify = createNotifierFromConfig({
|
|
307
|
+
notifications: {
|
|
308
|
+
targets: [{ channel: "slack", target: "C-200", accountId: "acct-x" }],
|
|
309
|
+
},
|
|
310
|
+
}, runtime);
|
|
311
|
+
await notify("audit_pass", basePayload);
|
|
312
|
+
expect(runtime.channel.slack.sendMessageSlack).toHaveBeenCalledOnce();
|
|
313
|
+
const [, , opts] = runtime.channel.slack.sendMessageSlack.mock.calls[0];
|
|
314
|
+
expect(opts.accountId).toBe("acct-x");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("sends to telegram target", async () => {
|
|
318
|
+
const runtime = mockRuntime();
|
|
319
|
+
const notify = createNotifierFromConfig({
|
|
320
|
+
notifications: {
|
|
321
|
+
targets: [{ channel: "telegram", target: "-100388" }],
|
|
322
|
+
},
|
|
323
|
+
}, runtime);
|
|
324
|
+
await notify("working", { ...basePayload, attempt: 1 });
|
|
325
|
+
expect(runtime.channel.telegram.sendMessageTelegram).toHaveBeenCalledOnce();
|
|
326
|
+
expect(runtime.channel.telegram.sendMessageTelegram).toHaveBeenCalledWith(
|
|
327
|
+
"-100388",
|
|
328
|
+
expect.stringContaining("worker started"),
|
|
329
|
+
{ silent: true },
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("fans out to multiple targets", async () => {
|
|
334
|
+
const runtime = mockRuntime();
|
|
335
|
+
const notify = createNotifierFromConfig({
|
|
336
|
+
notifications: {
|
|
337
|
+
targets: [
|
|
338
|
+
{ channel: "discord", target: "D-100" },
|
|
339
|
+
{ channel: "slack", target: "C-200" },
|
|
340
|
+
{ channel: "telegram", target: "-100388" },
|
|
341
|
+
],
|
|
342
|
+
},
|
|
343
|
+
}, runtime);
|
|
344
|
+
await notify("dispatch", basePayload);
|
|
345
|
+
|
|
346
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledOnce();
|
|
347
|
+
expect(runtime.channel.slack.sendMessageSlack).toHaveBeenCalledOnce();
|
|
348
|
+
expect(runtime.channel.telegram.sendMessageTelegram).toHaveBeenCalledOnce();
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("isolates failures between targets", async () => {
|
|
352
|
+
const runtime = mockRuntime();
|
|
353
|
+
runtime.channel.slack.sendMessageSlack = vi.fn(async () => {
|
|
354
|
+
throw new Error("Slack down");
|
|
355
|
+
});
|
|
163
356
|
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
357
|
+
|
|
358
|
+
const notify = createNotifierFromConfig({
|
|
359
|
+
notifications: {
|
|
360
|
+
targets: [
|
|
361
|
+
{ channel: "discord", target: "D-100" },
|
|
362
|
+
{ channel: "slack", target: "C-200" },
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
}, runtime);
|
|
366
|
+
await expect(notify("escalation", basePayload)).resolves.toBeUndefined();
|
|
367
|
+
|
|
368
|
+
// Discord should still succeed
|
|
369
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledOnce();
|
|
167
370
|
consoleSpy.mockRestore();
|
|
168
371
|
});
|
|
372
|
+
|
|
373
|
+
it("skips suppressed events", async () => {
|
|
374
|
+
const runtime = mockRuntime();
|
|
375
|
+
const notify = createNotifierFromConfig({
|
|
376
|
+
notifications: {
|
|
377
|
+
targets: [{ channel: "discord", target: "D-100" }],
|
|
378
|
+
events: { auditing: false },
|
|
379
|
+
},
|
|
380
|
+
}, runtime);
|
|
381
|
+
|
|
382
|
+
// Suppressed event — should not send
|
|
383
|
+
await notify("auditing", basePayload);
|
|
384
|
+
expect(runtime.channel.discord.sendMessageDiscord).not.toHaveBeenCalled();
|
|
385
|
+
|
|
386
|
+
// Non-suppressed event — should send
|
|
387
|
+
await notify("dispatch", basePayload);
|
|
388
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledOnce();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("sends events that are explicitly enabled", async () => {
|
|
392
|
+
const runtime = mockRuntime();
|
|
393
|
+
const notify = createNotifierFromConfig({
|
|
394
|
+
notifications: {
|
|
395
|
+
targets: [{ channel: "discord", target: "D-100" }],
|
|
396
|
+
events: { dispatch: true, auditing: false },
|
|
397
|
+
},
|
|
398
|
+
}, runtime);
|
|
399
|
+
|
|
400
|
+
await notify("dispatch", basePayload);
|
|
401
|
+
expect(runtime.channel.discord.sendMessageDiscord).toHaveBeenCalledOnce();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// ---------------------------------------------------------------------------
|
|
406
|
+
// createNoopNotifier
|
|
407
|
+
// ---------------------------------------------------------------------------
|
|
408
|
+
|
|
409
|
+
describe("createNoopNotifier", () => {
|
|
410
|
+
it("returns function that resolves without error", async () => {
|
|
411
|
+
const notify = createNoopNotifier();
|
|
412
|
+
await expect(notify("dispatch", {
|
|
413
|
+
identifier: "API-1",
|
|
414
|
+
title: "test",
|
|
415
|
+
status: "dispatched",
|
|
416
|
+
})).resolves.toBeUndefined();
|
|
417
|
+
});
|
|
169
418
|
});
|