Files
projectbrain-Public/backend/test/runnerClaimHealth.test.ts
T

102 lines
4.0 KiB
TypeScript

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> = {}): CompactRunner => ({
id: 1,
name: "validation-1",
status: "online",
busy: false,
disabled: false,
labels: ["linux-validation"],
...input,
});
const job = (input: Partial<CompactRunnerJob> = {}): 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 },
});
});
});