CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
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 | /**
* fireWebhooks() (src/routes/webhooks.tsx) — the retry/dead-letter-backed
* webhook dispatcher that, before this session, had ZERO callers anywhere
* in the codebase despite a full UI for configuring webhooks and a live
* "last delivery" status pill. Users could configure a webhook and it would
* never fire. Now wired into push (post-receive.ts), issue create/comment/
* close/reopen (issues.tsx), PR create/comment/merge(x2)/close (pulls.tsx +
* pr-merge.ts), and star (web.tsx) — see those files' `fireWebhooks(...)`
* call sites for the wiring itself (grep is the static proof of wiring;
* scripts/agent-journey.ts's webhook-delivery-row assertion is the live
* end-to-end proof; this file proves fireWebhooks()'s own selection logic).
*
* Pure dependency injection (loadHooks/enqueueWebhookDelivery/
* drainPendingDeliveries in deps) — no mock.module() on ../db or
* ../lib/webhook-delivery. An earlier version of this file used
* mock.module("../db", ...) and it leaked globally across the whole `bun
* test` run, breaking an unrelated markdown-rendering test in web.tsx that
* also queries ../db. Same class of bug as pr-workflow-sync.test.ts's
* original mock-module leak earlier this session — same fix.
*/
import { beforeEach, describe, expect, it } from "bun:test";
import { fireWebhooks } from "../routes/webhooks";
interface FakeHook {
id: string;
secret: string;
isActive: boolean;
events: string; // comma-separated, matches the real schema shape
}
let _hooks: FakeHook[] = [];
let _selectShouldThrow = false;
let _enqueueCalls: Array<{ webhookId: string; event: string; payload: unknown }> = [];
let _drainCalls = 0;
const fakeDeps = {
loadHooks: async (_repositoryId: string) => {
if (_selectShouldThrow) throw new Error("db unavailable");
return _hooks;
},
enqueueWebhookDelivery: async (opts: any) => {
_enqueueCalls.push(opts);
return "delivery-id-1";
},
drainPendingDeliveries: async () => {
_drainCalls++;
},
} as any;
beforeEach(() => {
_hooks = [];
_selectShouldThrow = false;
_enqueueCalls = [];
_drainCalls = 0;
});
describe("fireWebhooks", () => {
it("enqueues a delivery for an active hook subscribed to the event", async () => {
_hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "push,pr" }];
await fireWebhooks("repo-1", "push", { branch: "main" }, fakeDeps);
expect(_enqueueCalls).toHaveLength(1);
expect(_enqueueCalls[0]).toMatchObject({ webhookId: "hook-1", event: "push" });
expect(_enqueueCalls[0]!.payload).toEqual({ branch: "main" });
});
it("skips a hook not subscribed to the fired event", async () => {
_hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "issue,star" }];
await fireWebhooks("repo-1", "push", { branch: "main" }, fakeDeps);
expect(_enqueueCalls).toHaveLength(0);
});
it("skips an inactive hook even if subscribed to the event", async () => {
_hooks = [{ id: "hook-1", secret: "s", isActive: false, events: "push" }];
await fireWebhooks("repo-1", "push", {}, fakeDeps);
expect(_enqueueCalls).toHaveLength(0);
});
it("enqueues one delivery per matching hook when several are configured", async () => {
_hooks = [
{ id: "hook-1", secret: "s", isActive: true, events: "pr" },
{ id: "hook-2", secret: "s", isActive: true, events: "push,pr" },
{ id: "hook-3", secret: "s", isActive: true, events: "star" },
];
await fireWebhooks("repo-1", "pr", { action: "opened", number: 1 }, fakeDeps);
expect(_enqueueCalls).toHaveLength(2);
expect(_enqueueCalls.map((c) => c.webhookId).sort()).toEqual(["hook-1", "hook-2"]);
});
it("kicks the delivery worker only when something was actually enqueued", async () => {
_hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "push" }];
await fireWebhooks("repo-1", "push", {}, fakeDeps);
expect(_drainCalls).toBe(1);
_drainCalls = 0;
_hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "issue" }];
await fireWebhooks("repo-1", "push", {}, fakeDeps);
expect(_drainCalls).toBe(0);
});
it("never throws when the hook lookup fails — fail-open, matches logActivity()'s contract", async () => {
_selectShouldThrow = true;
await expect(fireWebhooks("repo-1", "push", {}, fakeDeps)).resolves.toBeUndefined();
expect(_enqueueCalls).toHaveLength(0);
});
it("real default deps resolve without throwing (production wiring sanity check)", async () => {
// No deps override — exercises the real db-backed default. A repo with
// no configured webhooks should resolve cleanly with zero enqueues.
await expect(
fireWebhooks("00000000-0000-0000-0000-000000000000", "push", {})
).resolves.toBeUndefined();
});
});
|