fix(runtime): harden process startup and health

This commit is contained in:
Marc-André Bombeck committed 2026-07-21 00:05:03 +02:00
1 parent 914e7658db
commit 9adeb8fc19
16 files changed
+653 -275

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
interface WorkerStatusFixture {
running: boolean;
startedAt: string | null;
lastHeartbeat: string | null;
lastReminderCheck: string | null;
lastWithingsSync: string | null;
lastInsightsRun: string | null;
jobsProcessed: number;
errors: number;
}
const { queryRaw, getWorkerStatus, getGlobalBoss, getSession } = vi.hoisted(
() => ({
queryRaw: vi.fn(async () => 1),
getWorkerStatus: vi.fn((): WorkerStatusFixture => ({
running: false,
startedAt: null,
lastHeartbeat: null,
lastReminderCheck: null,
lastWithingsSync: null,
lastInsightsRun: null,
jobsProcessed: 0,
errors: 0,
})),
getGlobalBoss: vi.fn((): object | null => null),
getSession: vi.fn(async () => null),
}),
);
vi.mock("@/lib/db", () => ({
prisma: { $queryRaw: queryRaw },
}));
vi.mock("@/lib/jobs/worker-status", () => ({ getWorkerStatus }));
vi.mock("@/lib/jobs/boss-instance", () => ({ getGlobalBoss }));
vi.mock("@/lib/auth/session", () => ({ getSession }));
vi.mock("@/lib/api-handler", () => ({
apiHandler: <T extends (...args: unknown[]) => unknown>(fn: T) => fn,
}));
vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() }));
import { GET } from "../route";
async function health() {
const response = await (GET as unknown as () => Promise<Response>)();
return { response, body: (await response.json()) as { status: string } };
}
beforeEach(() => {
vi.clearAllMocks();
queryRaw.mockResolvedValue(1);
getWorkerStatus.mockReturnValue({
running: false,
startedAt: null,
lastHeartbeat: null,
lastReminderCheck: null,
lastWithingsSync: null,
lastInsightsRun: null,
jobsProcessed: 0,
errors: 0,
});
getGlobalBoss.mockReturnValue(null);
getSession.mockResolvedValue(null);
});
describe("GET /api/health process topology", () => {
it("web mode requires DB and producer readiness, not an in-process worker", async () => {
vi.stubEnv("HEALTHLOG_PROCESS_TYPE", "web");
getGlobalBoss.mockReturnValue({});
const { response, body } = await health();
expect(response.status).toBe(200);
expect(body).toEqual({ status: "ok" });
});
it("worker mode requires DB and worker readiness, not web producer readiness", async () => {
vi.stubEnv("HEALTHLOG_PROCESS_TYPE", "worker");
getWorkerStatus.mockReturnValue({
running: true,
startedAt: "2026-07-20T00:00:00.000Z",
lastHeartbeat: "2026-07-20T00:00:00.000Z",
lastReminderCheck: null,
lastWithingsSync: null,
lastInsightsRun: null,
jobsProcessed: 0,
errors: 0,
});
const { response, body } = await health();
expect(response.status).toBe(200);
expect(body).toEqual({ status: "ok" });
});
it("all mode requires both producer and worker readiness", async () => {
vi.stubEnv("HEALTHLOG_PROCESS_TYPE", "all");
getWorkerStatus.mockReturnValue({
running: true,
startedAt: "2026-07-20T00:00:00.000Z",
lastHeartbeat: "2026-07-20T00:00:00.000Z",
lastReminderCheck: null,
lastWithingsSync: null,
lastInsightsRun: null,
jobsProcessed: 0,
errors: 0,
});
const missingProducer = await health();
expect(missingProducer.response.status).toBe(503);
expect(missingProducer.body).toEqual({ status: "degraded" });
getGlobalBoss.mockReturnValue({});
const ready = await health();
expect(ready.response.status).toBe(200);
expect(ready.body).toEqual({ status: "ok" });
});
});
+9 -1
View File
@@ -1,5 +1,7 @@
import { prisma } from "@/lib/db";
import { getWorkerStatus } from "@/lib/jobs/worker-status";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import { shouldRunWeb, shouldRunWorker } from "@/lib/process-type";
import { getSession } from "@/lib/auth/session";
import { NextResponse } from "next/server";
import { apiHandler } from "@/lib/api-handler";
@@ -19,7 +21,13 @@ export const GET = apiHandler(async () => {
}
const worker = getWorkerStatus();
const status = dbOk && worker.running ? "ok" : "degraded";
const producerReady = getGlobalBoss() !== null;
const status =
dbOk &&
(!shouldRunWeb() || producerReady) &&
(!shouldRunWorker() || worker.running)
? "ok"
: "degraded";
const statusCode = status === "ok" ? 200 : 503;
const cacheHeaders = {