379 lines
14 KiB
JavaScript
379 lines
14 KiB
JavaScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import assert from "node:assert/strict";
|
|
import { afterEach, describe, it } from "vitest";
|
|
|
|
import { formatHealthBaselineResult, verifyHealthBaseline } from "./verify-health-baseline.mjs";
|
|
|
|
const VALID_DOCKERFILE = `FROM node:20-bookworm-slim AS base
|
|
FROM base AS build
|
|
FROM base AS worker
|
|
HEALTHCHECK CMD node apps/worker/dist/health.js
|
|
CMD [ "node", "apps/worker/dist/index.js" ]
|
|
FROM base AS api
|
|
HEALTHCHECK CMD curl -fsS "http://127.0.0.1:\${API_PORT:-\${PORT}}/healthz"
|
|
CMD [ "pnpm", "--filter", "@toolctrl/api", "start" ]
|
|
FROM base AS web
|
|
HEALTHCHECK CMD curl -fsS "http://127.0.0.1:\${PORT}/healthz"
|
|
ENTRYPOINT [ "./start.sh" ]
|
|
`;
|
|
|
|
const VALID_WEB_HEALTHCHECK = `export const loader = async () => {
|
|
return new Response("OK");
|
|
};
|
|
`;
|
|
|
|
const VALID_WEB_HEALTHZ = `export const loader = async () => {
|
|
return Response.json({ status: "ok", service: "web", revision: process.env.APP_REVISION || "unknown" }, { status: 200 });
|
|
};
|
|
`;
|
|
|
|
const VALID_WEB_READYZ = `import { db } from "~/utils/db.server";
|
|
|
|
export const loader = async () => {
|
|
try {
|
|
await db.appConfiguration.count();
|
|
return Response.json({ status: "ready", service: "web" }, { status: 200 });
|
|
} catch {
|
|
return Response.json({ status: "not_ready", service: "web", error: "database_unavailable" }, { status: 503 });
|
|
}
|
|
};
|
|
`;
|
|
|
|
const VALID_API_APP = `import { Hono } from "hono";
|
|
import { getApiRuntimeConfig } from "@toolctrl/config";
|
|
|
|
export const app = new Hono();
|
|
|
|
function apiHealthResponse(c) {
|
|
return c.json({
|
|
status: "ok",
|
|
service: "api",
|
|
revision: getApiRuntimeConfig().revision,
|
|
requestId: c.get("requestId"),
|
|
});
|
|
}
|
|
|
|
app.get("/healthz", apiHealthResponse);
|
|
|
|
app.get("/readyz", async (c) => {
|
|
try {
|
|
await apiAppDependencies.checkReadiness();
|
|
return c.json({
|
|
status: "ready",
|
|
service: "api",
|
|
requestId: c.get("requestId"),
|
|
});
|
|
} catch {
|
|
return c.json(
|
|
{
|
|
status: "not_ready",
|
|
service: "api",
|
|
requestId: c.get("requestId"),
|
|
error: {
|
|
code: "dependency_unavailable",
|
|
message: "A required dependency is unavailable.",
|
|
},
|
|
},
|
|
503
|
|
);
|
|
}
|
|
});
|
|
|
|
app.get("/v1/health", apiHealthResponse);
|
|
`;
|
|
|
|
const VALID_WORKER_HEALTH = `import { getDbClient } from "@toolctrl/db/client.server";
|
|
|
|
export async function buildWorkerHealthReport(env = process.env) {
|
|
await getDbClient().$queryRaw\`SELECT 1\`;
|
|
return {
|
|
status: "ready",
|
|
revision: env.APP_REVISION || "unknown",
|
|
redisConfigured: true,
|
|
redisSource: "REDIS_URL",
|
|
redisParseStatus: "valid",
|
|
databaseConfigured: true,
|
|
databaseStatus: "ready",
|
|
errors: [],
|
|
};
|
|
}
|
|
|
|
export async function runWorkerHealthCli(io = process) {
|
|
const report = await buildWorkerHealthReport();
|
|
io.stdout.write(\`${"${JSON.stringify(report)}"}\\n\`);
|
|
return 0;
|
|
}
|
|
`;
|
|
|
|
const fixtures = [];
|
|
|
|
function writeFile(rootDir, relativePath, content) {
|
|
const absolutePath = path.join(rootDir, relativePath);
|
|
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
|
|
fs.writeFileSync(absolutePath, content);
|
|
}
|
|
|
|
function writeJson(rootDir, relativePath, value) {
|
|
writeFile(rootDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function createFixture(overrides = {}) {
|
|
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "health-baseline-"));
|
|
fixtures.push(rootDir);
|
|
|
|
writeFile(rootDir, "Dockerfile", overrides.dockerfile ?? VALID_DOCKERFILE);
|
|
writeFile(rootDir, "apps/web/app/routes/healthcheck.tsx", overrides.webHealthcheck ?? VALID_WEB_HEALTHCHECK);
|
|
writeFile(rootDir, "apps/web/app/routes/healthz.tsx", overrides.webHealthz ?? VALID_WEB_HEALTHZ);
|
|
writeFile(rootDir, "apps/web/app/routes/readyz.tsx", overrides.webReadyz ?? VALID_WEB_READYZ);
|
|
writeFile(rootDir, "apps/api/src/app.ts", overrides.apiApp ?? VALID_API_APP);
|
|
writeFile(rootDir, "apps/worker/src/health.ts", overrides.workerHealth ?? VALID_WORKER_HEALTH);
|
|
writeJson(
|
|
rootDir,
|
|
"apps/worker/package.json",
|
|
overrides.workerPackage ?? {
|
|
name: "@toolctrl/worker",
|
|
scripts: {
|
|
health: "node dist/health.js",
|
|
},
|
|
},
|
|
);
|
|
|
|
if (overrides.remove) {
|
|
for (const relativePath of overrides.remove) {
|
|
fs.rmSync(path.join(rootDir, relativePath), { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
return rootDir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
while (fixtures.length > 0) {
|
|
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("verify-health-baseline", () => {
|
|
it("passes enforced health surfaces without web/API gap observations", () => {
|
|
const rootDir = createFixture();
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.deepEqual(result.failures, []);
|
|
assert.ok(result.passes.some((pass) => pass === "apps/web/app/routes/healthcheck.tsx: legacy liveness healthcheck avoids DB and self-fetch"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/web/app/routes/healthz.tsx: liveness route does not require DB"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/web/app/routes/healthz.tsx: liveness route reports ok web service identity"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/web/app/routes/readyz.tsx: readiness imports and checks db"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/api/src/app.ts: /healthz reports service \"api\""));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/api/src/app.ts: /healthz includes requestId"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/api/src/app.ts: /readyz returns 503 not-ready dependency diagnostics"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/api/src/app.ts: /v1/health reports service \"api\""));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/api/src/app.ts: /v1/health includes requestId"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/worker/package.json: script \"health\" is node dist/health.js"));
|
|
assert.ok(result.passes.some((pass) => pass === "apps/worker/src/health.ts: worker readiness checks DB"));
|
|
assert.ok(result.passes.some((pass) => pass === "Dockerfile: worker target contains HEALTHCHECK using compiled worker health"));
|
|
assert.doesNotMatch(result.observations.join("\n"), /Health Baseline target .*\/healthz route is not present yet/);
|
|
assert.doesNotMatch(result.observations.join("\n"), /Health Baseline target .*\/readyz route is not present yet/);
|
|
assert.doesNotMatch(result.observations.join("\n"), /apps\/worker\/src\/health\.ts: worker readiness does not check DB yet/);
|
|
});
|
|
|
|
it("fails when an existing web healthcheck surface regresses", () => {
|
|
const rootDir = createFixture({ remove: ["apps/web/app/routes/healthcheck.tsx"] });
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/routes\/healthcheck\.tsx: missing existing web healthcheck route/);
|
|
});
|
|
|
|
it("fails when legacy web healthcheck consumes DB or self-fetches", () => {
|
|
const rootDir = createFixture({
|
|
webHealthcheck: `import { db } from "~/utils/db.server";
|
|
export const loader = async () => {
|
|
await Promise.all([db.appConfiguration.count(), fetch("http://localhost/")]);
|
|
return new Response("OK");
|
|
};
|
|
`,
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /legacy healthcheck must not import\/check DB or self-fetch the app/);
|
|
});
|
|
|
|
it("fails when API /v1/health stops reporting service and requestId", () => {
|
|
const rootDir = createFixture({
|
|
apiApp: `import { Hono } from "hono";
|
|
export const app = new Hono();
|
|
app.get("/v1/health", (c) => {
|
|
return c.json({ status: "ok" });
|
|
});
|
|
`,
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/api\/src\/app\.ts: \/v1\/health must report service "api"/);
|
|
assert.match(result.failures.join("\n"), /apps\/api\/src\/app\.ts: \/v1\/health must include requestId/);
|
|
});
|
|
|
|
it("fails when API /v1/health route is missing", () => {
|
|
const rootDir = createFixture({
|
|
apiApp: VALID_API_APP.replace('app.get("/v1/health", apiHealthResponse);\n', ""),
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/api\/src\/app\.ts: missing GET \/v1\/health route/);
|
|
});
|
|
|
|
it("fails when web /healthz is missing", () => {
|
|
const rootDir = createFixture({ remove: ["apps/web/app/routes/healthz.tsx"] });
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/routes\/healthz\.tsx: missing web \/healthz liveness route/);
|
|
});
|
|
|
|
it("fails when web /readyz is missing", () => {
|
|
const rootDir = createFixture({ remove: ["apps/web/app/routes/readyz.tsx"] });
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/routes\/readyz\.tsx: missing web \/readyz readiness route/);
|
|
});
|
|
|
|
it("fails when web /healthz imports db", () => {
|
|
const rootDir = createFixture({
|
|
webHealthz: `import { db } from "~/utils/db.server";
|
|
export const loader = async () => {
|
|
await db.appConfiguration.count();
|
|
return Response.json({ status: "ok", service: "web" }, { status: 200 });
|
|
};
|
|
`,
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/routes\/healthz\.tsx: liveness route must not import or check db/);
|
|
});
|
|
|
|
it("fails when web /healthz reports the wrong service identity", () => {
|
|
const rootDir = createFixture({
|
|
webHealthz: `export const loader = async () => {
|
|
return Response.json({ status: "ok", service: "oops", revision: process.env.APP_REVISION || "unknown" }, { status: 200 });
|
|
};
|
|
`,
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/routes\/healthz\.tsx: liveness route must report status ok and service web/);
|
|
});
|
|
|
|
it("fails when web /readyz does not check db", () => {
|
|
const rootDir = createFixture({
|
|
webReadyz: `export const loader = async () => {
|
|
return Response.json({ status: "ready", service: "web" }, { status: 200 });
|
|
};
|
|
`,
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /app\/routes\/readyz\.tsx: readiness route must import and check db/);
|
|
});
|
|
|
|
it("fails when API /healthz is missing", () => {
|
|
const rootDir = createFixture({
|
|
apiApp: VALID_API_APP.replace('app.get("/healthz", apiHealthResponse);\n\n', ""),
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/api\/src\/app\.ts: missing GET \/healthz route/);
|
|
});
|
|
|
|
it("fails when API /readyz is missing", () => {
|
|
const rootDir = createFixture({
|
|
apiApp: VALID_API_APP.replace(/app\.get\("\/readyz"[\s\S]*?\n\}\);\n\n/, ""),
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/api\/src\/app\.ts: missing GET \/readyz route/);
|
|
});
|
|
|
|
it("fails when API /readyz lacks 503 dependency diagnostics", () => {
|
|
const rootDir = createFixture({
|
|
apiApp: VALID_API_APP.replace("status: \"not_ready\",", "status: \"ready\",").replace("code: \"dependency_unavailable\",", "code: \"unavailable\","),
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/api\/src\/app\.ts: \/readyz must return 503 not_ready dependency diagnostics/);
|
|
});
|
|
|
|
it("fails when worker health exposes Redis secrets", () => {
|
|
const rootDir = createFixture({
|
|
workerHealth: VALID_WORKER_HEALTH.replace("redisSource: \"REDIS_URL\",", "redisUrl: process.env.REDIS_URL,"),
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/worker\/src\/health\.ts: health JSON must not expose Redis URL or password values/);
|
|
});
|
|
|
|
it("allows helper parameters named redisUrl when the health report omits secrets", () => {
|
|
const rootDir = createFixture({
|
|
workerHealth: VALID_WORKER_HEALTH.replace(
|
|
"export async function runWorkerHealthCli(io = process) {",
|
|
"function probeRedis(redisUrl: string) { return Boolean(redisUrl); }\nexport async function runWorkerHealthCli(io = process) {"
|
|
),
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
|
|
assert.equal(result.failures.filter((failure) => /health JSON must not expose Redis URL/.test(failure)).length, 0);
|
|
});
|
|
|
|
it("fails when worker health omits DB readiness", () => {
|
|
const rootDir = createFixture({
|
|
workerHealth: `export function buildWorkerHealthReport() {
|
|
return {
|
|
status: "ready",
|
|
redisConfigured: true,
|
|
redisSource: "REDIS_URL",
|
|
redisParseStatus: "valid",
|
|
errors: [],
|
|
};
|
|
}
|
|
|
|
export function runWorkerHealthCli(io = process) {
|
|
const report = buildWorkerHealthReport();
|
|
io.stdout.write(\`${"${JSON.stringify(report)}"}\\n\`);
|
|
return 0;
|
|
}
|
|
`,
|
|
});
|
|
|
|
const result = verifyHealthBaseline(rootDir);
|
|
const output = formatHealthBaselineResult(result);
|
|
|
|
assert.match(result.failures.join("\n"), /apps\/worker\/src\/health\.ts: worker readiness must check DB/);
|
|
assert.match(output, /^FAIL Health baseline verification/);
|
|
assert.match(output, /FAIL apps\/worker\/src\/health\.ts: worker readiness must check DB/);
|
|
});
|
|
|
|
it("keeps web/API endpoint gaps out of passing output", () => {
|
|
const result = verifyHealthBaseline(createFixture());
|
|
const output = formatHealthBaselineResult(result);
|
|
|
|
assert.match(output, /^PASS Health baseline verification/);
|
|
assert.doesNotMatch(output, /Health Baseline target .*\/healthz route is not present yet/);
|
|
assert.doesNotMatch(output, /Health Baseline target .*\/readyz route is not present yet/);
|
|
assert.match(output, /Summary: \d+ passed, \d+ observed, 0 failed/);
|
|
});
|
|
});
|