Blame · Line-by-line history
status-uptime.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.
| 7f0c2bd | 1 | /** |
| 2 | * Public /status 24h uptime. | |
| 3 | * | |
| 4 | * The figure was `(uptimeMs / WINDOW_MS) * 100` — the age of the current Bun | |
| 5 | * worker as a fraction of 24 hours. A deploy is not an outage, so every deploy | |
| 6 | * reset the public status page to a near-zero number. Observed live on | |
| 7 | * 2026-07-28, minutes after a routine deploy: | |
| 8 | * | |
| 9 | * <div class="status-hero-stat-num">0.0%</div> | |
| 10 | * <div class="status-hero-stat-label">24h uptime</div> | |
| 11 | * | |
| 12 | * directly beside "All systems operational" and a service table where every | |
| 13 | * row read 100.0%. With the 60s auto-deploy timer this was the page's normal | |
| 14 | * state, not an edge case — anyone checking the status page during a deploy | |
| 15 | * saw 0% uptime on a healthy system. | |
| 16 | * | |
| 17 | * It now derives from recorded incidents. | |
| 18 | */ | |
| 19 | ||
| 20 | import { describe, expect, it } from "bun:test"; | |
| 21 | import { uptimePctFromIncidents, UPTIME_WINDOW_MS } from "../routes/status"; | |
| 22 | ||
| 23 | const NOW = new Date("2026-07-28T12:00:00.000Z").getTime(); | |
| 24 | const HOUR = 3_600_000; | |
| 25 | ||
| 26 | describe("no incidents means full uptime", () => { | |
| 27 | it("reports 100 with an empty incident list", () => { | |
| 28 | // The core regression: a freshly-restarted process with a clean incident | |
| 29 | // log must read 100%, not "how long have I been alive". | |
| 30 | expect(uptimePctFromIncidents([], NOW)).toBe(100); | |
| 31 | }); | |
| 32 | ||
| 33 | it("ignores incidents that ended before the window", () => { | |
| 34 | const old = [ | |
| 35 | { | |
| 36 | startedAt: new Date(NOW - 40 * HOUR), | |
| 37 | resolvedAt: new Date(NOW - 30 * HOUR), | |
| 38 | }, | |
| 39 | ]; | |
| 40 | expect(uptimePctFromIncidents(old, NOW)).toBe(100); | |
| 41 | }); | |
| 42 | }); | |
| 43 | ||
| 44 | describe("downtime is subtracted accurately", () => { | |
| 45 | it("a 1h resolved incident costs 1/24th", () => { | |
| 46 | const rows = [ | |
| 47 | { startedAt: new Date(NOW - 5 * HOUR), resolvedAt: new Date(NOW - 4 * HOUR) }, | |
| 48 | ]; | |
| 49 | const pct = uptimePctFromIncidents(rows, NOW); | |
| 50 | expect(pct).toBeCloseTo((23 / 24) * 100, 5); | |
| 51 | }); | |
| 52 | ||
| 53 | it("an unresolved incident counts as ongoing up to now", () => { | |
| 54 | const rows = [{ startedAt: new Date(NOW - 2 * HOUR), resolvedAt: null }]; | |
| 55 | expect(uptimePctFromIncidents(rows, NOW)).toBeCloseTo((22 / 24) * 100, 5); | |
| 56 | }); | |
| 57 | ||
| 58 | it("clamps an incident that began before the window to the window edge", () => { | |
| 59 | // Started 30h ago, resolved 20h ago → only 4h falls inside the window. | |
| 60 | const rows = [ | |
| 61 | { startedAt: new Date(NOW - 30 * HOUR), resolvedAt: new Date(NOW - 20 * HOUR) }, | |
| 62 | ]; | |
| 63 | expect(uptimePctFromIncidents(rows, NOW)).toBeCloseTo((20 / 24) * 100, 5); | |
| 64 | }); | |
| 65 | }); | |
| 66 | ||
| 67 | describe("overlapping incidents are merged, not double-counted", () => { | |
| 68 | it("two fully overlapping incidents count once", () => { | |
| 69 | const rows = [ | |
| 70 | { startedAt: new Date(NOW - 6 * HOUR), resolvedAt: new Date(NOW - 4 * HOUR) }, | |
| 71 | { startedAt: new Date(NOW - 5 * HOUR), resolvedAt: new Date(NOW - 4 * HOUR) }, | |
| 72 | ]; | |
| 73 | // 2h of real downtime, not 3h. | |
| 74 | expect(uptimePctFromIncidents(rows, NOW)).toBeCloseTo((22 / 24) * 100, 5); | |
| 75 | }); | |
| 76 | ||
| 77 | it("partially overlapping incidents merge into one span", () => { | |
| 78 | const rows = [ | |
| 79 | { startedAt: new Date(NOW - 6 * HOUR), resolvedAt: new Date(NOW - 4 * HOUR) }, | |
| 80 | { startedAt: new Date(NOW - 5 * HOUR), resolvedAt: new Date(NOW - 3 * HOUR) }, | |
| 81 | ]; | |
| 82 | // Union is 6h ago → 3h ago = 3h. | |
| 83 | expect(uptimePctFromIncidents(rows, NOW)).toBeCloseTo((21 / 24) * 100, 5); | |
| 84 | }); | |
| 85 | ||
| 86 | it("never goes below 0 even if incidents blanket the window many times over", () => { | |
| 87 | const rows = Array.from({ length: 10 }, () => ({ | |
| 88 | startedAt: new Date(NOW - 48 * HOUR), | |
| 89 | resolvedAt: null, | |
| 90 | })); | |
| 91 | expect(uptimePctFromIncidents(rows, NOW)).toBe(0); | |
| 92 | }); | |
| 93 | ||
| 94 | it("is order-independent", () => { | |
| 95 | const a = { startedAt: new Date(NOW - 3 * HOUR), resolvedAt: new Date(NOW - 2 * HOUR) }; | |
| 96 | const b = { startedAt: new Date(NOW - 9 * HOUR), resolvedAt: new Date(NOW - 8 * HOUR) }; | |
| 97 | expect(uptimePctFromIncidents([a, b], NOW)).toBeCloseTo( | |
| 98 | uptimePctFromIncidents([b, a], NOW), | |
| 99 | 10 | |
| 100 | ); | |
| 101 | }); | |
| 102 | }); | |
| 103 | ||
| 104 | describe("malformed rows do not corrupt the figure", () => { | |
| 105 | it("skips unparseable timestamps rather than producing NaN", () => { | |
| 106 | const rows = [ | |
| 107 | { startedAt: "not-a-date", resolvedAt: null }, | |
| 108 | { startedAt: new Date(NOW - 2 * HOUR), resolvedAt: new Date(NOW - 1 * HOUR) }, | |
| 109 | ]; | |
| 110 | const pct = uptimePctFromIncidents(rows, NOW); | |
| 111 | expect(Number.isFinite(pct)).toBe(true); | |
| 112 | expect(pct).toBeCloseTo((23 / 24) * 100, 5); | |
| 113 | }); | |
| 114 | ||
| 115 | it("skips a resolvedAt earlier than its startedAt", () => { | |
| 116 | const rows = [ | |
| 117 | { startedAt: new Date(NOW - 1 * HOUR), resolvedAt: new Date(NOW - 5 * HOUR) }, | |
| 118 | ]; | |
| 119 | expect(uptimePctFromIncidents(rows, NOW)).toBe(100); | |
| 120 | }); | |
| 121 | }); | |
| 122 | ||
| 123 | describe("window constant", () => { | |
| 124 | it("is 24 hours", () => { | |
| 125 | expect(UPTIME_WINDOW_MS).toBe(24 * 60 * 60 * 1000); | |
| 126 | }); | |
| 127 | }); |