import Fastify from "fastify"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { loadEnv } from "../src/config/env.js"; import type { AutonomyRuntimeSnapshot } from "../src/services/autonomyRuntime.js"; import { registerSystemReadinessRoutes } from "../src/routes/systemReadiness.js"; const mocks = vi.hoisted(() => ({ runtime: vi.fn(), query: vi.fn(), worker: vi.fn() })); vi.mock("../src/services/autonomyRuntime.js", () => ({ loadAutonomyRuntime: mocks.runtime })); vi.mock("../src/db/prisma.js", () => ({ prisma: { $queryRaw: mocks.query, project: { count: vi.fn(async () => 1) }, blueprintGraph: { count: vi.fn(async () => 1) }, portfolioRepository: { count: vi.fn(async () => 1) }, giteaRepositoryLink: { count: vi.fn(async () => 1) }, analysisWorkerHeartbeat: { findUnique: mocks.worker }, analysisJob: { count: vi.fn(async () => 0) }, } })); const snapshot = (): AutonomyRuntimeSnapshot => ({ executors: [], projects: [], counts: { connectedExecutors: 0, enabledProjects: 0, claimableProjects: 0, activeProjects: 0, pendingWorkItems: 0, blockedProjects: 0, waitingRoadmapProjects: 0, stalledProjects: 0 }, }); const apps: ReturnType[] = []; const requestReadiness = async (runtime: AutonomyRuntimeSnapshot) => { mocks.runtime.mockResolvedValue(runtime); const app = Fastify(); apps.push(app); await registerSystemReadinessRoutes(app, { env: loadEnv({ NODE_ENV: "test", PROJECTBRAIN_DATA_SOURCE: "database", DATABASE_URL: "postgresql://fixture:fixture@localhost:5432/fixture", PROJECTBRAIN_OWNER_USERNAME: "fixture-owner", PROJECTBRAIN_OWNER_PASSWORD: "fixture-only-owner-password", PROJECTBRAIN_AUTH_SECRET: "fixture-only-auth-secret-at-least-32-characters", }) }); const response = await app.inject({ method: "GET", url: "/api/system/readiness" }); expect(response.statusCode).toBe(200); return response.json().data; }; beforeEach(() => { vi.clearAllMocks(); mocks.query.mockResolvedValue([{ count: 1n }]); mocks.worker.mockResolvedValue({ status: "ready", lastSeenAt: new Date() }); }); afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close())); }); describe("autonomy readiness demand", () => { it.each([false, true])("does not require an executor when all Auto work is paused (historical heartbeat: %s)", async (historical) => { const runtime = snapshot(); if (historical) runtime.executor = { status: "stale", instanceId: "old-web-task", startedAt: "2026-01-01T00:00:00.000Z", lastSeenAt: "2026-01-01T00:00:00.000Z" }; const data = await requestReadiness(runtime); expect(data.status).toBe("ready"); expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "skip" }); expect(data.autonomyExecutor).toMatchObject({ status: "ready", connectedExecutors: 0 }); expect(data.autonomyExecutor.lastSeenAt).toBe(runtime.executor?.lastSeenAt); }); it.each(["enabledProjects", "claimableProjects", "activeProjects", "pendingWorkItems"] as const)( "preserves warning without an executor when %s is positive", async (count) => { const runtime = snapshot(); runtime.counts[count] = 1; const data = await requestReadiness(runtime); expect(data.status).toBe("degraded"); expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "warn" }); expect(data.autonomyExecutor.connectedExecutors).toBe(0); }, ); it("keeps fresh connected executor and pending work ready", async () => { const runtime = snapshot(); runtime.counts.connectedExecutors = runtime.counts.enabledProjects = runtime.counts.pendingWorkItems = 1; const data = await requestReadiness(runtime); expect(data.status).toBe("ready"); expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "pass" }); }); it("retains a stale heartbeat warning when enabled work still needs the executor", async () => { const runtime = snapshot(); runtime.counts.enabledProjects = runtime.counts.pendingWorkItems = 1; runtime.executor = { status: "stale", instanceId: "old-web-task", startedAt: "2026-01-01T00:00:00.000Z", lastSeenAt: "2026-01-01T00:00:00.000Z" }; const data = await requestReadiness(runtime); expect(data.status).toBe("degraded"); expect(data.autonomyExecutor).toMatchObject({ status: "degraded", connectedExecutors: 0, lastSeenAt: runtime.executor.lastSeenAt }); expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "warn" }); }); it("does not suppress other readiness warnings while Auto is paused", async () => { mocks.worker.mockResolvedValue(null); expect((await requestReadiness(snapshot())).status).toBe("degraded"); }); });