From 88e742a129dc3e2cead7fd74339b342dd4d3089e Mon Sep 17 00:00:00 2001 From: dotta Date: Mon, 30 Mar 2026 13:17:34 -0500 Subject: [PATCH] Fix health DB connectivity probe Co-Authored-By: Paperclip --- server/src/__tests__/health.test.ts | 46 +++++++++++++++++++++++++++-- server/src/routes/health.ts | 11 +++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/server/src/__tests__/health.test.ts b/server/src/__tests__/health.test.ts index 1511b95e..8f80eec6 100644 --- a/server/src/__tests__/health.test.ts +++ b/server/src/__tests__/health.test.ts @@ -1,16 +1,56 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import express from "express"; import request from "supertest"; +import type { Db } from "@paperclipai/db"; import { healthRoutes } from "../routes/health.js"; +import * as devServerStatus from "../dev-server-status.js"; import { serverVersion } from "../version.js"; describe("GET /health", () => { - const app = express(); - app.use("/health", healthRoutes()); + beforeEach(() => { + vi.spyOn(devServerStatus, "readPersistedDevServerStatus").mockReturnValue(undefined); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); it("returns 200 with status ok", async () => { + const app = express(); + app.use("/health", healthRoutes()); + const res = await request(app).get("/health"); expect(res.status).toBe(200); expect(res.body).toEqual({ status: "ok", version: serverVersion }); }); + + it("returns 200 when the database probe succeeds", async () => { + const db = { + execute: vi.fn().mockResolvedValue([{ "?column?": 1 }]), + } as unknown as Db; + const app = express(); + app.use("/health", healthRoutes(db)); + + const res = await request(app).get("/health"); + + expect(res.status).toBe(200); + expect(res.body).toMatchObject({ status: "ok", version: serverVersion }); + }); + + it("returns 503 when the database probe fails", async () => { + const db = { + execute: vi.fn().mockRejectedValue(new Error("connect ECONNREFUSED")), + } as unknown as Db; + const app = express(); + app.use("/health", healthRoutes(db)); + + const res = await request(app).get("/health"); + + expect(res.status).toBe(503); + expect(res.body).toEqual({ + status: "unhealthy", + version: serverVersion, + error: "database_unreachable", + }); + }); }); diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts index 0bf6e92f..795eb9a5 100644 --- a/server/src/routes/health.ts +++ b/server/src/routes/health.ts @@ -29,6 +29,17 @@ export function healthRoutes( return; } + try { + await db.execute(sql`SELECT 1`); + } catch { + res.status(503).json({ + status: "unhealthy", + version: serverVersion, + error: "database_unreachable", + }); + return; + } + let bootstrapStatus: "ready" | "bootstrap_pending" = "ready"; let bootstrapInviteActive = false; if (opts.deploymentMode === "authenticated") {