Files
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

612 lines
21 KiB
JavaScript

/**
* @file Tests for the universal webhook delivery layer: payload formatting per
* platform (slack/discord/teams/generic), HMAC signing, target CRUD with secret
* redaction, validation, the synchronous test probe, rule-scoped dispatch,
* disabled-target skipping, retry/backoff with delivery-log recording, and the
* clear-data wipe.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after, beforeEach } = require("node:test");
const assert = require("node:assert/strict");
const path = require("path");
const os = require("os");
const http = require("http");
const crypto = require("crypto");
// Test DB + fast retry tunables must be set BEFORE requiring server modules
// (server/lib/webhooks.js reads the WEBHOOK_* env at module load).
const TEST_DB = path.join(os.tmpdir(), `dashboard-webhooks-test-${Date.now()}-${process.pid}.db`);
process.env.DASHBOARD_DB_PATH = TEST_DB;
process.env.WEBHOOK_MAX_ATTEMPTS = "2";
process.env.WEBHOOK_RETRY_BASE_MS = "10";
process.env.WEBHOOK_TIMEOUT_MS = "3000";
const { createApp, startServer } = require("../index");
const { db, stmts } = require("../db");
const webhooks = require("../lib/webhooks");
const providers = require("../lib/webhook-providers");
let server;
let BASE;
// Mock receiver — records every inbound request; behavior is tunable per-test.
const received = [];
let nextStatus = 200;
let nextBody = "ok"; // response body the mock returns (for body-veto tests)
let failTimes = 0; // respond 500 this many times before honoring nextStatus
let recvServer;
let RECV_URL;
function resetReceiver() {
received.length = 0;
nextStatus = 200;
nextBody = "ok";
failTimes = 0;
}
// Wipe all targets/deliveries between tests so an enabled target from one test
// never receives another test's dispatch (which would also fire real requests
// at example.com URLs from the CRUD tests).
beforeEach(() => {
db.prepare("DELETE FROM webhook_deliveries").run();
db.prepare("DELETE FROM webhook_targets").run();
webhooks.invalidateWebhookCache();
resetReceiver();
});
function fetchJson(urlPath, options = {}) {
return new Promise((resolve, reject) => {
const url = new URL(urlPath, BASE);
const opts = {
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method: options.method || "GET",
headers: { "Content-Type": "application/json", ...options.headers },
};
const req = http.request(opts, (res) => {
let body = "";
res.on("data", (c) => (body += c));
res.on("end", () => {
let parsed;
try {
parsed = JSON.parse(body);
} catch {
parsed = body;
}
resolve({ status: res.statusCode, body: parsed });
});
});
req.on("error", reject);
if (options.body) req.write(JSON.stringify(options.body));
req.end();
});
}
const post = (p, body) => fetchJson(p, { method: "POST", body });
const patch = (p, body) => fetchJson(p, { method: "PATCH", body });
const del = (p) => fetchJson(p, { method: "DELETE" });
const SAMPLE_ALERT = {
id: 42,
rule_id: "rule-abc",
rule_name: "Too many errors",
rule_type: "event_pattern",
session_id: "sess-1",
agent_id: "agent-1",
message: "5 matching events in 2 min (threshold 5)",
details: JSON.stringify({ observed_count: 5 }),
triggered_at: "2026-06-10T12:00:00.000Z",
};
before(async () => {
recvServer = http.createServer((req, res) => {
let b = "";
req.on("data", (c) => (b += c));
req.on("end", () => {
let parsed;
try {
parsed = JSON.parse(b);
} catch {
parsed = b;
}
received.push({ method: req.method, headers: req.headers, body: parsed });
let status = nextStatus;
if (failTimes > 0) {
failTimes -= 1;
status = 500;
}
res.statusCode = status;
res.end(nextBody);
});
});
await new Promise((r) => recvServer.listen(0, "127.0.0.1", r));
RECV_URL = `http://127.0.0.1:${recvServer.address().port}/hook`;
const app = createApp();
server = await startServer(app, 0);
BASE = `http://127.0.0.1:${server.address().port}`;
});
after(() => {
if (server) server.close();
if (recvServer) recvServer.close();
try {
db.close();
} catch {
/* ignore */
}
});
describe("payload formatting", () => {
it("slack: header + section + context blocks with fallback text", () => {
const p = webhooks.formatPayload("slack", SAMPLE_ALERT);
assert.ok(p.text.includes("Too many errors"));
assert.equal(p.blocks[0].type, "header");
assert.equal(p.blocks[1].type, "section");
assert.equal(p.blocks[2].type, "context");
assert.ok(p.blocks[1].text.text.includes("threshold 5"));
});
it("discord: single rich embed with fields", () => {
const p = webhooks.formatPayload("discord", SAMPLE_ALERT);
assert.equal(p.embeds.length, 1);
assert.ok(p.embeds[0].title.includes("Too many errors"));
assert.equal(p.embeds[0].timestamp, SAMPLE_ALERT.triggered_at);
assert.ok(p.embeds[0].fields.some((f) => f.name === "Session"));
});
it("teams: Adaptive Card wrapped in the Workflows message envelope", () => {
const p = webhooks.formatPayload("teams", SAMPLE_ALERT);
assert.equal(p.type, "message");
assert.equal(p.attachments[0].contentType, "application/vnd.microsoft.card.adaptive");
const card = p.attachments[0].content;
assert.equal(card.type, "AdaptiveCard");
const factSet = card.body.find((b) => b.type === "FactSet");
assert.ok(factSet.facts.some((f) => f.title === "Type"));
});
it("generic: stable envelope with parsed details", () => {
const p = webhooks.formatPayload("generic", SAMPLE_ALERT);
assert.equal(p.event, "alert.triggered");
assert.equal(p.alert.rule_name, "Too many errors");
assert.deepEqual(p.alert.details, { observed_count: 5 });
});
it("generic: HMAC signature header when secret set", () => {
const target = { type: "generic", url: "https://x.test", secret: "s3cr3t" };
const { body, headers } = webhooks.buildRequest(target, SAMPLE_ALERT);
const ts = headers["X-Webhook-Timestamp"];
const expected =
"sha256=" + crypto.createHmac("sha256", "s3cr3t").update(`${ts}.${body}`).digest("hex");
assert.equal(headers["X-Webhook-Signature"], expected);
});
it("generic: custom headers cannot clobber Content-Type or signature", () => {
const target = {
type: "generic",
url: "https://x.test",
headers: { "Content-Type": "text/plain", "X-Webhook-Signature": "fake", "X-Custom": "ok" },
};
const { headers } = webhooks.buildRequest(target, SAMPLE_ALERT);
assert.equal(headers["Content-Type"], "application/json");
assert.equal(headers["X-Custom"], "ok");
assert.notEqual(headers["X-Webhook-Signature"], "fake");
});
});
describe("target CRUD + redaction", () => {
it("creates a generic target and never returns the raw url/secret", async () => {
const res = await post("/api/webhooks", {
name: "My endpoint",
type: "generic",
url: "https://example.com/hook/SECRET-TOKEN-1234",
secret: "signing-secret",
headers: { Authorization: "Bearer abc" },
});
assert.equal(res.status, 201);
const t = res.body.target;
assert.equal(t.name, "My endpoint");
assert.equal(t.has_secret, true);
assert.ok(!("secret" in t));
assert.ok(!t.url_preview.includes("SECRET-TOKEN"));
assert.ok(t.url_preview.includes("example.com"));
assert.deepEqual(t.headers, { Authorization: "••••" }); // value masked
});
it("rejects an invalid url and a bad type", async () => {
const r1 = await post("/api/webhooks", { name: "x", type: "generic", url: "not a url" });
assert.equal(r1.status, 400);
const r2 = await post("/api/webhooks", {
name: "x",
type: "carrier-pigeon",
url: "https://x.io",
});
assert.equal(r2.status, 400);
});
it("requires https for slack/discord/teams", async () => {
const r = await post("/api/webhooks", {
name: "x",
type: "slack",
url: "http://insecure.test/x",
});
assert.equal(r.status, 400);
});
it("patches enabled without touching the url/secret", async () => {
const created = await post("/api/webhooks", {
name: "patch-me",
type: "generic",
url: "https://example.com/abc",
secret: "keep-me",
});
const id = created.body.target.id;
const res = await patch(`/api/webhooks/${id}`, { enabled: false });
assert.equal(res.status, 200);
assert.equal(res.body.target.enabled, false);
assert.equal(res.body.target.has_secret, true); // secret preserved
// raw row still has the secret
assert.equal(stmts.getWebhookTarget.get(id).secret, "keep-me");
});
it("deletes a target", async () => {
const created = await post("/api/webhooks", {
name: "delete-me",
type: "generic",
url: "https://example.com/del",
});
const id = created.body.target.id;
const res = await del(`/api/webhooks/${id}`);
assert.equal(res.status, 200);
assert.equal(stmts.getWebhookTarget.get(id), undefined);
});
});
describe("delivery", () => {
it("dispatches a fired alert to an enabled target with the generic payload", async () => {
resetReceiver();
const created = await post("/api/webhooks", {
name: "live",
type: "generic",
url: RECV_URL,
});
await webhooks.dispatchAlert(SAMPLE_ALERT);
assert.equal(received.length, 1);
assert.equal(received[0].body.event, "alert.triggered");
assert.equal(received[0].body.alert.rule_name, "Too many errors");
// a success delivery row was recorded
const last = stmts.lastWebhookDeliveryForTarget.get(created.body.target.id);
assert.equal(last.status, "success");
});
it("skips disabled targets", async () => {
resetReceiver();
const created = await post("/api/webhooks", {
name: "off",
type: "generic",
url: RECV_URL,
enabled: false,
});
await webhooks.dispatchAlert(SAMPLE_ALERT);
assert.equal(received.length, 0);
assert.equal(stmts.lastWebhookDeliveryForTarget.get(created.body.target.id), undefined);
});
it("honors rule_ids scoping", async () => {
resetReceiver();
await post("/api/webhooks", {
name: "scoped",
type: "generic",
url: RECV_URL,
rule_ids: ["some-other-rule"],
});
await webhooks.dispatchAlert(SAMPLE_ALERT); // rule_id "rule-abc" not in scope
assert.equal(received.length, 0);
});
it("retries on 5xx then records success", async () => {
resetReceiver();
failTimes = 1; // first attempt 500, second 200
const created = await post("/api/webhooks", {
name: "retry",
type: "generic",
url: RECV_URL,
});
await webhooks.dispatchAlert(SAMPLE_ALERT);
assert.equal(received.length, 2); // one failed + one retried
const last = stmts.lastWebhookDeliveryForTarget.get(created.body.target.id);
assert.equal(last.status, "success");
assert.equal(last.attempts, 2);
});
it("records a failure when all attempts return 5xx", async () => {
resetReceiver();
nextStatus = 500;
failTimes = 0;
const created = await post("/api/webhooks", {
name: "fail",
type: "generic",
url: RECV_URL,
});
const settled = await webhooks.dispatchAlert(SAMPLE_ALERT);
assert.equal(settled[0].value.ok, false);
const last = stmts.lastWebhookDeliveryForTarget.get(created.body.target.id);
assert.equal(last.status, "failed");
assert.equal(last.status_code, 500);
});
it("does not retry on 4xx", async () => {
resetReceiver();
nextStatus = 400;
const created = await post("/api/webhooks", {
name: "badreq",
type: "generic",
url: RECV_URL,
});
await webhooks.dispatchAlert(SAMPLE_ALERT);
assert.equal(received.length, 1); // no retry
assert.equal(stmts.lastWebhookDeliveryForTarget.get(created.body.target.id).status, "failed");
});
});
describe("test probe + clear-data", () => {
it("POST /:id/test delivers a synthetic alert and reports ok", async () => {
resetReceiver();
const created = await post("/api/webhooks", {
name: "probe",
type: "generic",
url: RECV_URL,
});
const res = await post(`/api/webhooks/${created.body.target.id}/test`);
assert.equal(res.status, 200);
assert.equal(res.body.ok, true);
assert.equal(received.length, 1);
assert.equal(received[0].body.alert.rule_type, "test");
});
it("clear-data wipes the delivery log but keeps targets", async () => {
resetReceiver();
const created = await post("/api/webhooks", { name: "keep", type: "generic", url: RECV_URL });
await webhooks.dispatchAlert(SAMPLE_ALERT);
assert.ok(stmts.lastWebhookDeliveryForTarget.get(created.body.target.id));
await post("/api/settings/clear-data");
assert.equal(stmts.lastWebhookDeliveryForTarget.get(created.body.target.id), undefined);
assert.ok(stmts.getWebhookTarget.get(created.body.target.id)); // target survives
});
});
describe("provider registry", () => {
it("exposes 14 first-class providers (+ generic = 15 types)", () => {
const firstClass = [
"slack",
"discord",
"teams",
"google_chat",
"mattermost",
"rocketchat",
"telegram",
"pagerduty",
"opsgenie",
"splunk_oncall",
"zapier",
"make",
"n8n",
"pipedream",
];
assert.equal(firstClass.length, 14);
for (const t of firstClass) {
assert.ok(providers.WEBHOOK_TYPES.includes(t), `${t} missing`);
}
assert.ok(providers.WEBHOOK_TYPES.includes("generic"));
assert.equal(providers.WEBHOOK_TYPES.length, 15);
});
it("GET /api/webhooks/providers returns redacted metadata", async () => {
const res = await fetchJson("/api/webhooks/providers");
assert.equal(res.status, 200);
const pd = res.body.providers.find((p) => p.type === "pagerduty");
assert.equal(pd.url_required, false); // has default URL
assert.ok(pd.fields.find((f) => f.key === "routing_key" && f.secret));
const slack = res.body.providers.find((p) => p.type === "slack");
assert.equal(slack.url_required, true);
});
});
describe("provider payload formatting", () => {
it("mattermost: Slack-style attachments", () => {
const p = providers.formatPayload("mattermost", SAMPLE_ALERT);
assert.ok(Array.isArray(p.attachments));
assert.ok(p.attachments[0].fields.some((f) => f.title === "Type"));
});
it("rocketchat: text + attachments", () => {
const p = providers.formatPayload("rocketchat", SAMPLE_ALERT);
assert.ok(p.text.includes("Too many errors"));
assert.ok(Array.isArray(p.attachments));
});
it("google_chat: simple text message", () => {
const p = providers.formatPayload("google_chat", SAMPLE_ALERT);
assert.ok(typeof p.text === "string" && p.text.includes("Too many errors"));
});
it("telegram: sendMessage shape with chat_id + HTML escaping", () => {
const p = providers.formatPayload(
"telegram",
{ ...SAMPLE_ALERT, rule_name: "a<b>c" },
{ chat_id: "123" }
);
assert.equal(p.chat_id, "123");
assert.equal(p.parse_mode, "HTML");
assert.ok(p.text.includes("a&lt;b&gt;c")); // escaped
});
it("pagerduty: Events API v2 with routing_key, severity, dedup_key", () => {
const p = providers.formatPayload("pagerduty", SAMPLE_ALERT, {
routing_key: "RK",
severity: "critical",
});
assert.equal(p.routing_key, "RK");
assert.equal(p.event_action, "trigger");
assert.equal(p.payload.severity, "critical");
assert.ok(p.dedup_key.includes(SAMPLE_ALERT.rule_id));
});
it("opsgenie: message + alias; api_key goes in the GenieKey header, not body", () => {
const p = providers.formatPayload("opsgenie", SAMPLE_ALERT, { api_key: "KEY" });
assert.ok(p.message.includes("Too many errors"));
assert.ok(!JSON.stringify(p).includes("KEY")); // key not in body
const headers = providers.resolveAuthHeaders({ type: "opsgenie", config: { api_key: "KEY" } });
assert.equal(headers.Authorization, "GenieKey KEY");
});
it("splunk_oncall: VictorOps message_type + entity", () => {
const p = providers.formatPayload("splunk_oncall", SAMPLE_ALERT, { severity: "CRITICAL" });
assert.equal(p.message_type, "CRITICAL");
assert.ok(p.entity_id.includes(SAMPLE_ALERT.rule_id));
});
it("generic family (zapier) uses the JSON envelope", () => {
const p = providers.formatPayload("zapier", SAMPLE_ALERT);
assert.equal(p.event, "alert.triggered");
});
});
describe("URL resolution", () => {
it("telegram derives its URL from the bot token", () => {
const url = providers.resolveUrl({
type: "telegram",
config: { bot_token: "TOK", chat_id: "1" },
});
assert.equal(url, "https://api.telegram.org/botTOK/sendMessage");
});
it("opsgenie picks the EU host when region=eu", () => {
assert.ok(
providers
.resolveUrl({ type: "opsgenie", config: { region: "eu" } })
.includes("api.eu.opsgenie.com")
);
assert.ok(
providers
.resolveUrl({ type: "opsgenie", config: { region: "us" } })
.includes("api.opsgenie.com")
);
});
it("pagerduty defaults to the Events API URL", () => {
assert.equal(
providers.resolveUrl({ type: "pagerduty", config: {} }),
"https://events.pagerduty.com/v2/enqueue"
);
});
});
describe("Splunk On-Call response-body veto", () => {
it("verifyResponse flags result=failure, trusts everything else", () => {
const vr = providers.PROVIDERS.splunk_oncall.verifyResponse;
assert.equal(vr('{"result":"failure","message":"bad routing key"}').ok, false);
assert.equal(vr('{"result":"success","entity_id":"x"}').ok, true);
assert.equal(vr("").ok, true); // empty body trusted
assert.equal(vr("not json").ok, true); // non-JSON 200 trusted
});
it("deliver() records a 200-with-result:failure as failed (no retry)", async () => {
resetReceiver();
nextStatus = 200;
nextBody = JSON.stringify({ result: "failure", message: "bad routing key" });
// Insert directly (splunk is https-only, can't go through the http-mock route).
const id = "splunk-veto-test";
stmts.insertWebhookTarget.run(
id,
"splunk",
"splunk_oncall",
RECV_URL,
1,
null,
null,
null,
JSON.stringify({ severity: "WARNING" })
);
webhooks.invalidateWebhookCache();
const target = webhooks.normalizeTarget(stmts.getWebhookTarget.get(id));
const res = await webhooks.deliver(target, SAMPLE_ALERT);
assert.equal(res.ok, false);
assert.match(res.error, /failure|bad routing key/i);
assert.equal(received.length, 1); // no retry on a logical rejection
assert.equal(stmts.lastWebhookDeliveryForTarget.get(id).status, "failed");
});
});
describe("provider CRUD + config redaction", () => {
it("creates a telegram target without a URL and redacts the bot token", async () => {
const res = await post("/api/webhooks", {
name: "tg",
type: "telegram",
config: { bot_token: "12345:SECRETTOKEN", chat_id: "999" },
});
assert.equal(res.status, 201);
const t = res.body.target;
assert.equal(t.config.bot_token, "••••"); // redacted
assert.equal(t.config.chat_id, "999"); // shown
assert.ok(!JSON.stringify(t).includes("SECRETTOKEN"));
assert.ok(t.url_preview.includes("api.telegram.org"));
});
it("requires routing_key for pagerduty", async () => {
const res = await post("/api/webhooks", { name: "pd", type: "pagerduty", config: {} });
assert.equal(res.status, 400);
});
it("rejects an unknown severity enum", async () => {
const res = await post("/api/webhooks", {
name: "pd2",
type: "pagerduty",
config: { routing_key: "RK", severity: "nope" },
});
assert.equal(res.status, 400);
});
it("patches opsgenie region without re-sending the api_key", async () => {
const created = await post("/api/webhooks", {
name: "og",
type: "opsgenie",
config: { api_key: "AAA", region: "us" },
});
const id = created.body.target.id;
const res = await patch(`/api/webhooks/${id}`, { config: { region: "eu" } });
assert.equal(res.status, 200);
assert.equal(res.body.target.config.region, "eu");
assert.equal(
stmts.getWebhookTarget.get(id) && JSON.parse(stmts.getWebhookTarget.get(id).config).api_key,
"AAA"
);
});
// pagerduty/opsgenie endpoints are https-only and (opsgenie) derive their own
// URL, so they can't point at the http test mock — assert the built request
// (URL + headers + body) directly instead.
it("buildRequest: pagerduty hits the Events API with the routing key in the body", () => {
const req = webhooks.buildRequest(
{ type: "pagerduty", config: { routing_key: "RK123", severity: "error" } },
SAMPLE_ALERT
);
assert.equal(req.url, "https://events.pagerduty.com/v2/enqueue");
const body = JSON.parse(req.body);
assert.equal(body.routing_key, "RK123");
assert.equal(body.payload.severity, "error");
});
it("buildRequest: opsgenie targets the region host and sets the GenieKey header", () => {
const req = webhooks.buildRequest(
{ type: "opsgenie", config: { api_key: "KEY9", region: "eu" } },
SAMPLE_ALERT
);
assert.ok(req.url.includes("api.eu.opsgenie.com"));
assert.equal(req.headers.Authorization, "GenieKey KEY9");
assert.ok(!req.body.includes("KEY9")); // key only in the header
});
});