fix(health): report real per-dependency status instead of hardcoded ok:true #18

Merged
admin merged 1 commits from fix/honest-health-check into main 2026-08-11 12:31:36 +00:00
10 changed files with 627 additions and 16 deletions
+35 -4
View File
@@ -1,12 +1,43 @@
import { NextResponse } from "next/server";
import { checkAllDependencies, hasDegradedDependency } from "@/lib/health-checks";
// GET /api/health — platform liveness (docs/04). DB/queue wired when provisioned.
// GET /api/health — liveness (docs/04, BACKLOG P0-3).
//
// Fixed finding: this used to hardcode `ok: true` and only ever check
// *presence* of DATABASE_URL/REDIS_URL, never whether anything was actually
// reachable -- "an uptime monitor watching HTTP 200 would show green for a
// dying instance" (docs/architecture/ARCHITECTURE.md). Each dependency below
// now reports a real state: "ok" (live round trip succeeded), "error"
// (configured but unreachable -- see `detail`), "not_configured", or
// "not_integrated" (env var present but no client code reads it -- honest
// about `queue`, which has no wired Redis client in this repo).
//
// This route stays a LIVENESS check: it always answers 200 as long as the
// process itself can run the handler (that's what "liveness" means -- an
// external dependency outage is not a reason to restart this instance). The
// body's `ok` field is the honest signal instead: false whenever a
// dependency that IS configured is failing. For a readiness gate that DOES
// fail closed on missing/broken dependencies, see GET /api/ready.
export async function GET() {
const deps = await checkAllDependencies();
const degraded = hasDegradedDependency(deps);
return NextResponse.json({
ok: true,
ok: !degraded,
service: "0pt-web",
db: Boolean(process.env.DATABASE_URL) ? "configured" : "not_configured",
queue: Boolean(process.env.REDIS_URL) ? "configured" : "not_configured",
db: deps.db.status,
queue: deps.queue.status,
engine: deps.engine.status,
stripe: deps.stripe.status,
...(degraded
? {
detail: Object.fromEntries(
Object.entries(deps)
.filter(([, v]) => v.status === "error")
.map(([k, v]) => [k, v.detail]),
),
}
: {}),
ts: new Date().toISOString(),
});
}
+32
View File
@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { checkAllDependencies, isReady } from "@/lib/health-checks";
// GET /api/ready — readiness (BACKLOG P0-3, CD-04/OBS-04: "no /ready route
// existed at all").
//
// Unlike GET /api/health (liveness -- always 200 while the process can
// respond), this fails closed: 503 whenever a load-bearing dependency isn't
// both configured AND actually reachable. Point a load balancer / Coolify
// readiness probe at this route, not /api/health, to stop routing traffic to
// an instance whose database is down without restarting it.
//
// Today the only load-bearing dependency is the database (see
// `lib/health-checks.ts#isReady`) -- engine/stripe/queue are optional
// integrations 0pt is designed to run without.
export async function GET() {
const deps = await checkAllDependencies();
const ready = isReady(deps);
return NextResponse.json(
{
ready,
service: "0pt-web",
db: deps.db.status,
queue: deps.queue.status,
engine: deps.engine.status,
stripe: deps.stripe.status,
ts: new Date().toISOString(),
},
{ status: ready ? 200 : 503 },
);
}
+75 -5
View File
@@ -22,15 +22,35 @@ paths:
/api/health:
get:
operationId: 0pt.health
summary: Liveness + dependency state
summary: Liveness + real per-dependency state
x-siax-status: live
security: []
responses:
"200":
description: OK — db/queue rapporterar "configured"/"not_configured"
description: Process is alive. Body reports honest per-dependency status (ok/error/not_configured/not_integrated) — see HealthResponse.
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
/api/ready:
get:
operationId: 0pt.ready
summary: Readiness — fails closed when a load-bearing dependency is down
x-siax-status: live
security: []
responses:
"200":
description: Ready to serve real traffic.
content:
application/json:
schema:
$ref: "#/components/schemas/ReadyResponse"
"503":
description: Not ready — a load-bearing dependency is not configured/reachable.
content:
application/json:
schema:
$ref: "#/components/schemas/ReadyResponse"
/api/estimates:
post:
operationId: 0pt.estimate_order
@@ -401,7 +421,18 @@ components:
type: string
HealthResponse:
type: object
required: [ok, service, db, queue, ts]
description: >
db/engine/stripe report a real dependency probe (BACKLOG P0-3): "ok"
means a live round trip succeeded, "error" means it was configured
but unreachable (see `detail`), "not_configured" means the env var
is unset. `queue` can only ever be "not_configured" or
"not_integrated" -- no Redis client is wired in this repo, so
presence of REDIS_URL is reported honestly rather than faking a
connectivity check. `ok` is true only when no configured dependency
is in "error". This is a LIVENESS check: HTTP status stays 200
whenever the process can respond, regardless of dependency state --
see /api/ready for the readiness gate that fails closed.
required: [ok, service, db, queue, engine, stripe, ts]
properties:
ok:
type: boolean
@@ -409,10 +440,49 @@ components:
type: string
db:
type: string
enum: [configured, not_configured]
enum: [ok, error, not_configured]
queue:
type: string
enum: [configured, not_configured]
enum: [not_configured, not_integrated]
engine:
type: string
enum: [ok, error, not_configured]
stripe:
type: string
enum: [ok, error, not_configured]
detail:
type: object
description: Present only when `ok` is false -- one-line error message per failing dependency.
additionalProperties:
type: string
ts:
type: string
format: date-time
ReadyResponse:
type: object
description: >
Readiness (BACKLOG P0-3: "no /ready route existed"). Fails closed --
503 whenever a load-bearing dependency (today: only `db`) is not
both configured and reachable. Point a load balancer / Coolify
readiness probe here, not at /api/health.
required: [ready, service, db, queue, engine, stripe, ts]
properties:
ready:
type: boolean
service:
type: string
db:
type: string
enum: [ok, error, not_configured]
queue:
type: string
enum: [not_configured, not_integrated]
engine:
type: string
enum: [ok, error, not_configured]
stripe:
type: string
enum: [ok, error, not_configured]
ts:
type: string
format: date-time
+15 -5
View File
@@ -132,6 +132,7 @@ Risknivå = SIAX L0–L5-taxonomin, bedömd av mig utifrån vad handlern gör.
| Operation | Metod/sökväg | Risknivå | Kräver godkännande |
|---|---|---|---|
| Liveness | `GET /api/health` | L0 | Nej (avsiktligt) |
| Readiness | `GET /api/ready` | L0 | Nej (avsiktligt) |
| Prisestimat | `POST /api/estimates` | L1 | Nej |
| Katalog / källor / fordon | `GET /api/{catalog,sources,vehicles}` | L1 | Nej |
| Sanktions-/PEP-screening | `GET /api/screen?q=` | L2 | Nej — proxar mot OpenSanctions |
@@ -240,11 +241,20 @@ inget sådant jobb, ingen TTL, ingen `DELETE`-väg och ingen radering finns i ko
## 6. Drift
### Hälsokontroll
`GET /api/health` → `{ok, service:"0pt-web", db, queue, ts}`. Den svarar **alltid
`ok:true`** oavsett om databasen finns — den rapporterar bara `"not_configured"`
i ett fält. En uptime-monitor som tittar på HTTP 200 kommer därför visa grönt
för en instans utan databas, vilket är exakt vad som gäller idag. Engine har ett
separat `GET /health`.
**Fixat (BACKLOG P0-3):** `GET /api/health` → `{ok, service:"0pt-web", db, queue,
engine, stripe, ts}`. `db`/`engine`/`stripe` rapporterar nu en riktig
beroendeprobe (`lib/health-checks.ts`): `"ok"` = lyckad live-anrop, `"error"` =
konfigurerad men onåbar (se `detail`), `"not_configured"` = env-variabel ej
satt. `queue` kan bara vara `"not_configured"`/`"not_integrated"` — ingen
Redis-klient finns i repot, så en satt `REDIS_URL` rapporteras ärligt i stället
för att låtsas testa en anslutning som aldrig görs. `ok` är `true` endast när
ingen konfigurerad dependency ligger i `"error"`. HTTP-statusen är fortsatt
alltid 200 (liveness — processen svarar) oavsett dependency-läge; en extern
beroendekrasch ska inte omstarta instansen. Ny `GET /api/ready` failar stängt
(503) när databasen (den enda lastbärande dependencyn idag) inte både är
konfigurerad och nåbar — peka en Coolify/lastbalanserar-readiness-probe dit,
inte mot `/api/health`. Engine har ett separat `GET /health` som `checkEngine()`
nu faktiskt anropar (tidigare kollades bara `ENGINE_URL`-närvaro).
### Loggar
Ingen strukturerad loggning. `console.warn` i `src/auth/context.ts` är den enda
+2 -1
View File
@@ -7,7 +7,8 @@
| SLO | Mål | Nuläge (2026-08-10) |
|---|---|---|
| `/api/health` (liveness) | 99,9 % | ✅ 200 (men alltid `ok:true` — readiness saknas, se D2) |
| `/api/health` (liveness) | 99,9 % | ✅ 200; `ok` speglar nu en riktig per-dependency-probe (fixat BACKLOG P0-3) |
| `/api/ready` (readiness) | 99,5 % | ✅ ny (BACKLOG P0-3) — 503 när `db` inte är konfigurerad+nåbar |
| Orderflöde `/api/orders` | 99,5 % | ⚠️ fungerar utan DB (in-memory), data försvinner vid restart |
| Capability-API `/api/0pt/*` | 99,5 % | ⚠️ live 200 anonymt (lucka S1) — målbild: 401 utan token |
| Engine (FastAPI, när deployad) | 99,0 % | ej deployad (EJ VERIFIERAT) |
+174
View File
@@ -0,0 +1,174 @@
// Dependency health probes shared by GET /api/health (liveness) and
// GET /api/ready (readiness). See docs/architecture/ARCHITECTURE.md
// "Hälsokontroll" and BACKLOG P0-3 for the finding this fixes: the old
// /api/health handler only ever checked *presence* of an env var and
// hardcoded `ok: true` regardless of whether anything was actually
// reachable -- "an uptime monitor watching HTTP 200 would show green for a
// dying instance". Each check below returns one of four honest states:
//
// "ok" -- configured AND a live round trip succeeded.
// "error" -- configured but the round trip failed (bad creds,
// host unreachable, timeout, ...). detail carries the
// underlying error message.
// "not_configured" -- no env var set. Not a failure by itself (0pt is
// designed to build/boot with everything unset).
// "not_integrated" -- env var IS set, but no client code in this repo
// actually uses it (currently only REDIS_URL/queue --
// see .env.example). Reporting "ok" here would be a
// fabricated check; reporting "not_configured" would
// hide that a value was provided. This is its own
// honest state rather than either of those two.
import { dbUrl, getSql } from "./db";
import { getStripe } from "./stripe";
import { hasEngine } from "./engine";
export type DepStatus = "ok" | "error" | "not_configured" | "not_integrated";
export interface DepResult {
status: DepStatus;
detail?: string;
latencyMs?: number;
}
export const DEFAULT_TIMEOUT_MS = 2000;
function errMessage(err: unknown): string {
return err instanceof Error ? err.message : String(err);
}
function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
promise.then(
(v) => {
clearTimeout(timer);
resolve(v);
},
(e) => {
clearTimeout(timer);
reject(e);
},
);
});
}
// Minimal shape of the one call site needs from the postgres client --
// matches `postgres`'s tagged-template callable, but kept narrow so tests
// can inject a fake without constructing a real `postgres.Sql`.
export type SqlPing = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown>;
/**
* Postgres reachability. `select 1` is the standard zero-cost liveness query
* -- it never touches application tables, so it can't fail on schema drift.
*/
export async function checkDatabase(
sqlClient: SqlPing | null = getSql() as unknown as SqlPing | null,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<DepResult> {
if (!dbUrl() || !sqlClient) return { status: "not_configured" };
const start = Date.now();
try {
await withTimeout(sqlClient`select 1`, timeoutMs, "database ping");
return { status: "ok", latencyMs: Date.now() - start };
} catch (err) {
return { status: "error", detail: errMessage(err), latencyMs: Date.now() - start };
}
}
/**
* FastAPI data engine (services/api) reachability via its own `GET /health`
* (services/api/orchestrator/main.py). Uses a real HTTP round trip, not just
* `hasEngine()` presence -- ENGINE_URL pointing at a dead host used to read
* as healthy.
*/
export async function checkEngine(
fetchImpl: typeof fetch = fetch,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<DepResult> {
if (!hasEngine()) return { status: "not_configured" };
const base = process.env.ENGINE_URL as string;
const start = Date.now();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetchImpl(`${base}/health`, {
signal: controller.signal,
cache: "no-store",
headers: { "X-Internal-Token": process.env.INTERNAL_TOKEN ?? "" },
});
if (!res.ok) {
return { status: "error", detail: `HTTP ${res.status}`, latencyMs: Date.now() - start };
}
return { status: "ok", latencyMs: Date.now() - start };
} catch (err) {
return { status: "error", detail: errMessage(err), latencyMs: Date.now() - start };
} finally {
clearTimeout(timer);
}
}
// Minimal shape of the one Stripe call this needs -- matches
// `Stripe#balance.retrieve`, kept narrow so tests can inject a fake instead
// of constructing a real Stripe client.
export interface StripeBalanceClient {
balance: { retrieve(): Promise<unknown> };
}
/**
* Stripe reachability. `balance.retrieve()` is Stripe's documented
* lightweight "is this key valid and can I reach the API" call -- it makes
* one real API round trip (catching e.g. a revoked/placeholder key) without
* mutating anything or touching a specific resource.
*/
export async function checkStripe(
client: StripeBalanceClient | null = getStripe() as unknown as StripeBalanceClient | null,
timeoutMs: number = DEFAULT_TIMEOUT_MS,
): Promise<DepResult> {
if (!client) return { status: "not_configured" };
const start = Date.now();
try {
await withTimeout(client.balance.retrieve(), timeoutMs, "stripe balance.retrieve");
return { status: "ok", latencyMs: Date.now() - start };
} catch (err) {
return { status: "error", detail: errMessage(err), latencyMs: Date.now() - start };
}
}
/**
* Redis/queue: presence-only, honestly. No queue client library is a
* dependency of this package (grep-verified against package.json) and no
* code path in this repo reads REDIS_URL to open a connection -- so there is
* no real check to run. Reporting "ok" would fabricate a probe that doesn't
* exist; this reports "not_integrated" instead of silently upgrading to a
* pretend pass.
*/
export function checkQueue(): DepResult {
return { status: process.env.REDIS_URL ? "not_integrated" : "not_configured" };
}
export interface DependencyResults {
db: DepResult;
engine: DepResult;
stripe: DepResult;
queue: DepResult;
}
export async function checkAllDependencies(): Promise<DependencyResults> {
const [db, engine, stripe] = await Promise.all([checkDatabase(), checkEngine(), checkStripe()]);
return { db, engine, stripe, queue: checkQueue() };
}
/** True when any dependency that IS configured/integrated is broken. */
export function hasDegradedDependency(deps: DependencyResults): boolean {
return Object.values(deps).some((d) => d.status === "error");
}
/**
* Readiness (unlike liveness) treats "not configured" for the deps this app
* cannot serve real traffic without as not-ready too. Only `db` is load-bearing
* today -- engine/stripe/queue are optional integrations the app is designed
* to run without (see .env.example).
*/
export function isReady(deps: DependencyResults): boolean {
return deps.db.status === "ok";
}
+5 -1
View File
@@ -33,7 +33,11 @@ export const config = {
matcher: ['/api/0pt/:path*'],
};
const EXEMPT_PATTERNS: RegExp[] = [/^\/api\/health(\/|$)/, /^\/api\/admin\/metrics(\/|$)/];
const EXEMPT_PATTERNS: RegExp[] = [
/^\/api\/health(\/|$)/,
/^\/api\/ready(\/|$)/,
/^\/api\/admin\/metrics(\/|$)/,
];
export async function middleware(req: NextRequest): Promise<NextResponse> {
const path = req.nextUrl.pathname;
+70
View File
@@ -0,0 +1,70 @@
/**
* Route-level regression test for GET /api/health (BACKLOG P0-3): the old
* handler hardcoded `ok: true` regardless of dependency state. These tests
* cover the default (nothing configured) baseline against the real route
* handler; per-dependency ok/error branching is covered at the unit level in
* tests/lib/health-checks.test.ts (dependency-injected, no network/DB
* needed).
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { GET as healthGET } from "@/app/api/health/route";
describe("GET /api/health", () => {
const savedEnv = {
DATABASE_URL: process.env.DATABASE_URL,
POSTGRES_URL: process.env.POSTGRES_URL,
ENGINE_URL: process.env.ENGINE_URL,
STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
REDIS_URL: process.env.REDIS_URL,
};
beforeEach(() => {
delete process.env.DATABASE_URL;
delete process.env.POSTGRES_URL;
delete process.env.ENGINE_URL;
delete process.env.STRIPE_SECRET_KEY;
delete process.env.REDIS_URL;
});
afterEach(() => {
for (const [k, v] of Object.entries(savedEnv)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
});
it("REGRESSION: always returns HTTP 200 (liveness), but `ok` now reflects a real per-dependency check rather than a hardcoded true", async () => {
const res = await healthGET();
expect(res.status).toBe(200);
const body = await res.json();
// With nothing configured, nothing can be degraded -- ok:true here is
// now an honest conclusion, not a hardcoded literal.
expect(body.ok).toBe(true);
expect(body.service).toBe("0pt-web");
});
it("reports not_configured (not the old flat 'configured'/'not_configured' guess) for every unset dependency", async () => {
const res = await healthGET();
const body = await res.json();
expect(body.db).toBe("not_configured");
expect(body.engine).toBe("not_configured");
expect(body.stripe).toBe("not_configured");
expect(body.queue).toBe("not_configured");
});
it("reports queue as not_integrated (never a fabricated ok) when REDIS_URL is set but unused", async () => {
process.env.REDIS_URL = "redis://localhost:6379";
const res = await healthGET();
const body = await res.json();
expect(body.queue).toBe("not_integrated");
// Not wiring a real check for an unused integration is not itself a
// degradation -- overall liveness stays ok.
expect(body.ok).toBe(true);
});
it("response includes a ts timestamp", async () => {
const res = await healthGET();
const body = await res.json();
expect(new Date(body.ts).toString()).not.toBe("Invalid Date");
});
});
+34
View File
@@ -0,0 +1,34 @@
/**
* Route-level test for GET /api/ready (BACKLOG P0-3: "no /ready route
* existed at all"). Unlike /api/health, readiness fails closed: 503 when the
* load-bearing dependency (db) is not both configured and reachable.
*/
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { GET as readyGET } from "@/app/api/ready/route";
describe("GET /api/ready", () => {
const savedEnv = {
DATABASE_URL: process.env.DATABASE_URL,
POSTGRES_URL: process.env.POSTGRES_URL,
};
beforeEach(() => {
delete process.env.DATABASE_URL;
delete process.env.POSTGRES_URL;
});
afterEach(() => {
for (const [k, v] of Object.entries(savedEnv)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
});
it("fails closed with 503 when the database is not configured", async () => {
const res = await readyGET();
expect(res.status).toBe(503);
const body = await res.json();
expect(body.ready).toBe(false);
expect(body.db).toBe("not_configured");
});
});
+185
View File
@@ -0,0 +1,185 @@
/**
* Unit tests for the dependency probes behind GET /api/health + GET
* /api/ready (BACKLOG P0-3). Each check is exercised via dependency
* injection (fake sql/fetch/stripe clients) rather than real network I/O,
* matching the fake-over-mock-global convention used elsewhere in this repo
* (e.g. tests/p0/document-intelligence-adapter.test.ts).
*/
import { describe, it, expect, afterEach } from "vitest";
import {
checkDatabase,
checkEngine,
checkStripe,
checkQueue,
hasDegradedDependency,
isReady,
type DependencyResults,
type SqlPing,
} from "@/lib/health-checks";
function jsonResponse(body: unknown, status = 200): Response {
return {
ok: status < 400,
status,
json: async () => body,
} as unknown as Response;
}
describe("checkDatabase", () => {
const saved = process.env.DATABASE_URL;
afterEach(() => {
if (saved === undefined) delete process.env.DATABASE_URL;
else process.env.DATABASE_URL = saved;
});
it("reports not_configured when DATABASE_URL is unset, without calling the client", async () => {
delete process.env.DATABASE_URL;
let called = false;
const fakeSql: SqlPing = async () => {
called = true;
return [{ "?column?": 1 }];
};
const result = await checkDatabase(fakeSql);
expect(result.status).toBe("not_configured");
expect(called).toBe(false);
});
it("reports ok when the ping round trip succeeds", async () => {
process.env.DATABASE_URL = "postgres://user:pass@localhost:5432/opt";
const fakeSql: SqlPing = async () => [{ "?column?": 1 }];
const result = await checkDatabase(fakeSql);
expect(result.status).toBe("ok");
expect(typeof result.latencyMs).toBe("number");
});
it("reports error with the driver message when the ping fails", async () => {
process.env.DATABASE_URL = "postgres://user:pass@localhost:5432/opt";
const fakeSql: SqlPing = async () => {
throw new Error("ECONNREFUSED 127.0.0.1:5432");
};
const result = await checkDatabase(fakeSql);
expect(result.status).toBe("error");
expect(result.detail).toContain("ECONNREFUSED");
});
it("reports error on timeout rather than hanging", async () => {
process.env.DATABASE_URL = "postgres://user:pass@localhost:5432/opt";
const fakeSql: SqlPing = () => new Promise(() => {}); // never resolves
const result = await checkDatabase(fakeSql, 20);
expect(result.status).toBe("error");
expect(result.detail).toContain("timed out");
});
it("reports not_configured when DATABASE_URL is set but no client is available", async () => {
process.env.DATABASE_URL = "postgres://user:pass@localhost:5432/opt";
const result = await checkDatabase(null);
expect(result.status).toBe("not_configured");
});
});
describe("checkEngine", () => {
const saved = process.env.ENGINE_URL;
afterEach(() => {
if (saved === undefined) delete process.env.ENGINE_URL;
else process.env.ENGINE_URL = saved;
});
it("reports not_configured when ENGINE_URL is unset", async () => {
delete process.env.ENGINE_URL;
const result = await checkEngine(async () => jsonResponse({ ok: true }));
expect(result.status).toBe("not_configured");
});
it("reports ok when the engine's /health responds 2xx", async () => {
process.env.ENGINE_URL = "http://engine.local:8000";
const fetchMock = (async (url: string) => {
expect(url).toBe("http://engine.local:8000/health");
return jsonResponse({ ok: true });
}) as unknown as typeof fetch;
const result = await checkEngine(fetchMock);
expect(result.status).toBe("ok");
});
it("reports error when the engine responds non-2xx", async () => {
process.env.ENGINE_URL = "http://engine.local:8000";
const result = await checkEngine(async () => jsonResponse({}, 500));
expect(result.status).toBe("error");
expect(result.detail).toContain("500");
});
it("reports error when the fetch throws (host unreachable)", async () => {
process.env.ENGINE_URL = "http://engine.local:8000";
const result = await checkEngine(async () => {
throw new Error("fetch failed");
});
expect(result.status).toBe("error");
});
});
describe("checkStripe", () => {
it("reports not_configured when no client is available (no STRIPE_SECRET_KEY)", async () => {
const result = await checkStripe(null);
expect(result.status).toBe("not_configured");
});
it("reports ok when balance.retrieve() succeeds", async () => {
const result = await checkStripe({ balance: { retrieve: async () => ({ available: [] }) } });
expect(result.status).toBe("ok");
});
it("reports error when balance.retrieve() rejects (e.g. revoked key)", async () => {
const result = await checkStripe({
balance: {
retrieve: async () => {
throw new Error("Invalid API Key provided");
},
},
});
expect(result.status).toBe("error");
expect(result.detail).toContain("Invalid API Key");
});
});
describe("checkQueue", () => {
const saved = process.env.REDIS_URL;
afterEach(() => {
if (saved === undefined) delete process.env.REDIS_URL;
else process.env.REDIS_URL = saved;
});
it("never claims ok — only not_configured or not_integrated (no Redis client wired in this repo)", () => {
delete process.env.REDIS_URL;
expect(checkQueue().status).toBe("not_configured");
process.env.REDIS_URL = "redis://localhost:6379";
expect(checkQueue().status).toBe("not_integrated");
});
});
describe("hasDegradedDependency / isReady", () => {
function deps(overrides: Partial<DependencyResults> = {}): DependencyResults {
return {
db: { status: "not_configured" },
engine: { status: "not_configured" },
stripe: { status: "not_configured" },
queue: { status: "not_configured" },
...overrides,
};
}
it("hasDegradedDependency is false when nothing is configured", () => {
expect(hasDegradedDependency(deps())).toBe(false);
});
it("hasDegradedDependency is true when any dependency errored", () => {
expect(hasDegradedDependency(deps({ engine: { status: "error", detail: "boom" } }))).toBe(true);
});
it("isReady is false when db is not ok, even if nothing else is broken", () => {
expect(isReady(deps())).toBe(false);
expect(isReady(deps({ db: { status: "error" } }))).toBe(false);
});
it("isReady is true once db is ok, regardless of optional integrations", () => {
expect(isReady(deps({ db: { status: "ok" } }))).toBe(true);
});
});