Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

webhook-fire-wiring.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

webhook-fire-wiring.test.tsBlame116 lines · 1 contributor
a74f4edccanty labs1/**
2 * fireWebhooks() (src/routes/webhooks.tsx) — the retry/dead-letter-backed
3 * webhook dispatcher that, before this session, had ZERO callers anywhere
4 * in the codebase despite a full UI for configuring webhooks and a live
5 * "last delivery" status pill. Users could configure a webhook and it would
6 * never fire. Now wired into push (post-receive.ts), issue create/comment/
7 * close/reopen (issues.tsx), PR create/comment/merge(x2)/close (pulls.tsx +
8 * pr-merge.ts), and star (web.tsx) — see those files' `fireWebhooks(...)`
9 * call sites for the wiring itself (grep is the static proof of wiring;
10 * scripts/agent-journey.ts's webhook-delivery-row assertion is the live
11 * end-to-end proof; this file proves fireWebhooks()'s own selection logic).
12 *
13 * Pure dependency injection (loadHooks/enqueueWebhookDelivery/
14 * drainPendingDeliveries in deps) — no mock.module() on ../db or
15 * ../lib/webhook-delivery. An earlier version of this file used
16 * mock.module("../db", ...) and it leaked globally across the whole `bun
17 * test` run, breaking an unrelated markdown-rendering test in web.tsx that
18 * also queries ../db. Same class of bug as pr-workflow-sync.test.ts's
19 * original mock-module leak earlier this session — same fix.
20 */
21
22import { beforeEach, describe, expect, it } from "bun:test";
23import { fireWebhooks } from "../routes/webhooks";
24
25interface FakeHook {
26 id: string;
27 secret: string;
28 isActive: boolean;
29 events: string; // comma-separated, matches the real schema shape
30}
31
32let _hooks: FakeHook[] = [];
33let _selectShouldThrow = false;
34let _enqueueCalls: Array<{ webhookId: string; event: string; payload: unknown }> = [];
35let _drainCalls = 0;
36
37const fakeDeps = {
38 loadHooks: async (_repositoryId: string) => {
39 if (_selectShouldThrow) throw new Error("db unavailable");
40 return _hooks;
41 },
42 enqueueWebhookDelivery: async (opts: any) => {
43 _enqueueCalls.push(opts);
44 return "delivery-id-1";
45 },
46 drainPendingDeliveries: async () => {
47 _drainCalls++;
48 },
49} as any;
50
51beforeEach(() => {
52 _hooks = [];
53 _selectShouldThrow = false;
54 _enqueueCalls = [];
55 _drainCalls = 0;
56});
57
58describe("fireWebhooks", () => {
59 it("enqueues a delivery for an active hook subscribed to the event", async () => {
60 _hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "push,pr" }];
61 await fireWebhooks("repo-1", "push", { branch: "main" }, fakeDeps);
62
63 expect(_enqueueCalls).toHaveLength(1);
64 expect(_enqueueCalls[0]).toMatchObject({ webhookId: "hook-1", event: "push" });
65 expect(_enqueueCalls[0]!.payload).toEqual({ branch: "main" });
66 });
67
68 it("skips a hook not subscribed to the fired event", async () => {
69 _hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "issue,star" }];
70 await fireWebhooks("repo-1", "push", { branch: "main" }, fakeDeps);
71 expect(_enqueueCalls).toHaveLength(0);
72 });
73
74 it("skips an inactive hook even if subscribed to the event", async () => {
75 _hooks = [{ id: "hook-1", secret: "s", isActive: false, events: "push" }];
76 await fireWebhooks("repo-1", "push", {}, fakeDeps);
77 expect(_enqueueCalls).toHaveLength(0);
78 });
79
80 it("enqueues one delivery per matching hook when several are configured", async () => {
81 _hooks = [
82 { id: "hook-1", secret: "s", isActive: true, events: "pr" },
83 { id: "hook-2", secret: "s", isActive: true, events: "push,pr" },
84 { id: "hook-3", secret: "s", isActive: true, events: "star" },
85 ];
86 await fireWebhooks("repo-1", "pr", { action: "opened", number: 1 }, fakeDeps);
87
88 expect(_enqueueCalls).toHaveLength(2);
89 expect(_enqueueCalls.map((c) => c.webhookId).sort()).toEqual(["hook-1", "hook-2"]);
90 });
91
92 it("kicks the delivery worker only when something was actually enqueued", async () => {
93 _hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "push" }];
94 await fireWebhooks("repo-1", "push", {}, fakeDeps);
95 expect(_drainCalls).toBe(1);
96
97 _drainCalls = 0;
98 _hooks = [{ id: "hook-1", secret: "s", isActive: true, events: "issue" }];
99 await fireWebhooks("repo-1", "push", {}, fakeDeps);
100 expect(_drainCalls).toBe(0);
101 });
102
103 it("never throws when the hook lookup fails — fail-open, matches logActivity()'s contract", async () => {
104 _selectShouldThrow = true;
105 await expect(fireWebhooks("repo-1", "push", {}, fakeDeps)).resolves.toBeUndefined();
106 expect(_enqueueCalls).toHaveLength(0);
107 });
108
109 it("real default deps resolve without throwing (production wiring sanity check)", async () => {
110 // No deps override — exercises the real db-backed default. A repo with
111 // no configured webhooks should resolve cleanly with zero enqueues.
112 await expect(
113 fireWebhooks("00000000-0000-0000-0000-000000000000", "push", {})
114 ).resolves.toBeUndefined();
115 });
116});