import { describe, expect, it } from "vitest"; import type { CompactRunner, CompactRunnerJob } from "../src/services/activityRuntime.js"; import { evaluateRunnerClaimHealth } from "../src/services/runnerClaimHealth.js"; const runner = (input: Partial = {}): CompactRunner => ({ id: 1, name: "validation-1", status: "online", busy: false, disabled: false, labels: ["linux-validation"], ...input, }); const job = (input: Partial = {}): CompactRunnerJob => ({ id: 99, runId: 9, name: "full", repository: "Jens/projectbrain", branch: "feature", status: "queued", queueState: "runnable", labels: ["linux-validation"], completedSteps: 0, totalSteps: 1, url: "https://gitea.example/jobs/99", createdAt: "2026-09-30T00:00:00.000Z", runnableObservedSince: "2026-09-30T00:00:00.000Z", claimableObservedSince: "2026-09-30T00:00:00.000Z", ...input, }); describe("runner claim health", () => { const now = Date.parse("2026-09-30T00:05:00.000Z"); it("marks an old queued job with a compatible idle runner as polling-stalled without inventing a restart action", () => { const health = evaluateRunnerClaimHealth({ runners: [runner()], jobs: [job()], generatedAt: "2026-09-30T00:05:00.000Z" }, { now, stallSeconds: 120 }); expect(health).toMatchObject({ status: "stalled", recoveryOwner: "appops-runner-fleet", recoveryPolicy: "controller-owned", counts: { pollingStalled: 1 }, oldestWaitSeconds: 300, oldestClaimableWaitSeconds: 300, }); expect(health.jobs[0]).toMatchObject({ state: "polling-stalled", compatibleIdleRunnerNames: ["validation-1"] }); expect(JSON.stringify(health)).not.toContain("container.restart"); }); it("classifies a busy compatible runner as capacity-wait", () => { const health = evaluateRunnerClaimHealth({ runners: [runner({ busy: true })], jobs: [job()], generatedAt: "2026-09-30T00:05:00.000Z" }, { now }); expect(health.jobs[0]).toMatchObject({ state: "capacity-wait", waitSeconds: 300, claimableWaitSeconds: 0 }); expect(health.oldestClaimableWaitSeconds).toBe(0); }); it("reports a label block when no online runner can satisfy the job", () => { const health = evaluateRunnerClaimHealth({ runners: [runner({ labels: ["security"] })], jobs: [job()], generatedAt: "2026-09-30T00:05:00.000Z" }, { now }); expect(health.status).toBe("degraded"); expect(health.counts.labelBlocked).toBe(1); }); it("reports a polling stall independently of how many compatible idle runners exist", () => { const health = evaluateRunnerClaimHealth({ runners: [runner(), runner({ id: 2, name: "validation-2" })], jobs: [job()], generatedAt: "2026-09-30T00:05:00.000Z", }, { now }); expect(health.counts.pollingStalled).toBe(1); expect(health.jobs[0].compatibleIdleRunnerNames).toEqual(["validation-1", "validation-2"]); }); it("keeps a newly queued compatible job inside the normal claim grace window", () => { const health = evaluateRunnerClaimHealth({ runners: [runner()], jobs: [job({ createdAt: "2026-09-30T00:04:30.000Z", runnableObservedSince: "2026-09-30T00:04:30.000Z", claimableObservedSince: "2026-09-30T00:04:30.000Z", })], generatedAt: "2026-09-30T00:05:00.000Z", }, { now, stallSeconds: 120 }); expect(health.status).toBe("healthy"); expect(health.jobs[0].state).toBe("claimable"); }); it("reports incomplete coverage rather than healthy when queued jobs were omitted from the scan", () => { const health = evaluateRunnerClaimHealth({ availability: "live", runners: [runner()], jobs: Array.from({ length: 50 }, (_, index) => job({ id: index + 1, createdAt: "2026-09-30T00:04:30.000Z" })), runnableJobScan: { complete: false, observed: 50, total: 60 }, generatedAt: "2026-09-30T00:05:00.000Z", }, { now }); expect(health).toMatchObject({ status: "incomplete", coverage: { complete: false, observed: 50, total: 60 }, counts: { queued: 50, pollingStalled: 0 }, }); }); });