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.
| 80bed05 | 1 | /** |
| 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 | ||
| 8 | import { afterEach, beforeEach, describe, expect, it } from "bun:test"; | |
| 9 | import { reportError } from "../lib/observability"; | |
| 10 | ||
| 11 | const origFetch = globalThis.fetch; | |
| 12 | const origWebhook = process.env.ERROR_WEBHOOK_URL; | |
| 13 | const origSentry = process.env.SENTRY_DSN; | |
| 14 | ||
| 15 | interface CapturedCall { | |
| 16 | url: string; | |
| 17 | init: RequestInit; | |
| 18 | } | |
| 19 | ||
| 20 | function 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 | ||
| 36 | function 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 | ||
| 44 | describe("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 | }); |