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

observability.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.

observability.test.tsBlame93 lines · 1 contributor
80bed05Claude1/**
2 * Observability layer (src/lib/observability.ts).
3 *
4 * `reportError` MUST never throw, regardless of env configuration or whether
5 * the configured webhook is reachable. These tests pin that contract.
6 */
7
8import { afterEach, beforeEach, describe, expect, it } from "bun:test";
9import { reportError } from "../lib/observability";
10
11const origFetch = globalThis.fetch;
12const origWebhook = process.env.ERROR_WEBHOOK_URL;
13const origSentry = process.env.SENTRY_DSN;
14
15interface CapturedCall {
16 url: string;
17 init: RequestInit;
18}
19
20function installFetch(
21 impl: (url: string, init: RequestInit) => Promise<Response>
22): CapturedCall[] {
23 const calls: CapturedCall[] = [];
24 // @ts-expect-error — override global fetch
25 globalThis.fetch = async (
26 input: RequestInfo | URL,
27 init: RequestInit = {}
28 ): Promise<Response> => {
29 const url = String(input);
30 calls.push({ url, init });
31 return impl(url, init);
32 };
33 return calls;
34}
35
36function restore(): void {
37 globalThis.fetch = origFetch;
38 if (origWebhook === undefined) delete process.env.ERROR_WEBHOOK_URL;
39 else process.env.ERROR_WEBHOOK_URL = origWebhook;
40 if (origSentry === undefined) delete process.env.SENTRY_DSN;
41 else process.env.SENTRY_DSN = origSentry;
42}
43
44describe("lib/observability — reportError", () => {
45 beforeEach(() => {
46 delete process.env.ERROR_WEBHOOK_URL;
47 delete process.env.SENTRY_DSN;
48 });
49
50 afterEach(() => {
51 restore();
52 });
53
54 it("does not throw when no env vars are set (logs only)", () => {
55 expect(() => reportError(new Error("boom"))).not.toThrow();
56 expect(() => reportError(new Error("boom"), { path: "/x" })).not.toThrow();
57 // Non-Error inputs must also be safe.
58 expect(() => reportError("string error")).not.toThrow();
59 expect(() => reportError({ weird: true })).not.toThrow();
60 expect(() => reportError(undefined)).not.toThrow();
61 });
62
63 it("does not throw when ERROR_WEBHOOK_URL is set but fetch rejects", async () => {
64 process.env.ERROR_WEBHOOK_URL = "http://127.0.0.1:1/unreachable";
65 const calls = installFetch(async () => {
66 throw new Error("ECONNREFUSED");
67 });
68
69 expect(() =>
70 reportError(new Error("prod bug"), { requestId: "r1", path: "/p", method: "GET" })
71 ).not.toThrow();
72
73 // Give the fire-and-forget promise a tick to run and its .catch to execute.
74 await new Promise((r) => setTimeout(r, 10));
75
76 expect(calls.length).toBe(1);
77 expect(calls[0]!.url).toBe("http://127.0.0.1:1/unreachable");
78 expect(calls[0]!.init.method).toBe("POST");
79 const body = JSON.parse(String(calls[0]!.init.body));
80 expect(body.message).toBe("prod bug");
81 expect(body.context).toEqual({ requestId: "r1", path: "/p", method: "GET" });
82 expect(typeof body.timestamp).toBe("string");
83 });
84
85 it("does not throw when fetch itself throws synchronously", () => {
86 process.env.ERROR_WEBHOOK_URL = "http://example.test/hook";
87 // @ts-expect-error — override to throw synchronously
88 globalThis.fetch = () => {
89 throw new Error("synchronous boom");
90 };
91 expect(() => reportError(new Error("err"))).not.toThrow();
92 });
93});