import { describe, expect, it, vi } from "vitest"; import { loadEnv } from "../src/config/env.js"; import type { GiteaActionJob } from "../src/integrations/gitea/types.js"; import { compactGiteaRuntime, createGiteaActivityRuntimeLoader, repositoryFromActionJob } from "../src/services/activityRuntime.js"; import { evaluateRunnerClaimHealth } from "../src/services/runnerClaimHealth.js"; const env = loadEnv({ NODE_ENV: "test", DATABASE_URL: "postgresql://example.invalid/projectbrain", PROJECTBRAIN_DATA_SOURCE: "database", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: "correct-horse-battery-staple", PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_GITEA_URL: "https://gitea.example.com", PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters", }); const job = (input: Partial = {}): GiteaActionJob => ({ id: 42, html_url: "https://gitea.example.com/Jens/projectbrain/actions/runs/21/jobs/42", run_id: 21, name: "quality", labels: ["ubuntu-latest"], run_attempt: 1, head_sha: "abc123", head_branch: "main", status: "in_progress", runner_id: 9, runner_name: "itworx-security-runner", steps: [ { name: "Checkout", number: 1, status: "completed", conclusion: "success" }, { name: "Test", number: 2, status: "in_progress" }, ], created_at: "2026-08-28T19:00:00Z", started_at: "2026-08-28T19:01:00Z", completed_at: "1970-01-01T00:00:00Z", ...input, }); describe("compact activity runtime", () => { it("keeps older running jobs visible ahead of a full page of newer queued jobs", () => { const jobs = [job({ id: 1 }), job({ id: 2 }), ...Array.from({ length: 30 }, (_, i) => job({ id: i + 100, status: "queued" }))]; const snapshot = compactGiteaRuntime([], jobs, "2026-09-10T00:00:00Z"); expect(snapshot.jobs.slice(0, 12).slice(0, 2).map((item) => item.id)).toEqual([2, 1]); expect(snapshot.counts).toEqual({ running: 2, runnable: 30, dependencyBlocked: 0 }); expect(snapshot.jobs).toHaveLength(32); }); it("shows queued jobs oldest first so the visible lane reflects waiting age", () => { const snapshot = compactGiteaRuntime([], [ job({ id: 9, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-08-28T19:09:00Z" }), job({ id: 7, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-08-28T19:03:00Z" }), job({ id: 8, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-08-28T19:06:00Z" }), ], "2026-08-28T19:10:00Z"); expect(snapshot.jobs.map((item) => item.id)).toEqual([7, 8, 9]); }); it("puts unknown queued timestamps last and breaks equal creation times by ascending job id", () => { const snapshot = compactGiteaRuntime([], [ job({ id: 9, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-08-28T19:03:00Z" }), job({ id: 4, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-08-28T19:03:00Z" }), job({ id: 7, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-08-28T19:06:00Z" }), job({ id: 2, status: "queued", runner_id: 0, runner_name: undefined, created_at: "not-a-timestamp" }), ], "2026-08-28T19:10:00Z"); expect(snapshot.jobs.map((item) => item.id)).toEqual([4, 9, 7, 2]); }); it("extracts a repository safely from a Gitea job URL", () => { expect(repositoryFromActionJob(job({ html_url: "https://gitea.example.com/Space%20Owner/Repo/actions/runs/1/jobs/2" }))).toBe("Space Owner/Repo"); expect(repositoryFromActionJob(job({ html_url: "not-a-url" }))).toBeUndefined(); }); it("returns only compact runner and current-step data", () => { const snapshot = compactGiteaRuntime([ { id: 9, name: "runner", status: "online", busy: true, disabled: false, ephemeral: false, labels: [{ id: 1, name: "ubuntu-latest", type: "custom" }] }, ], [job()], "2026-08-28T19:02:00Z"); expect(snapshot.runners[0]).toEqual({ id: 9, name: "runner", status: "online", busy: true, disabled: false, labels: ["ubuntu-latest"] }); expect(snapshot.jobs[0]).toMatchObject({ repository: "Jens/projectbrain", currentStep: "Test", queueState: "running", completedSteps: 1, totalSteps: 2 }); expect(snapshot.jobs[0].completedAt).toBeUndefined(); expect(JSON.stringify(snapshot)).not.toContain("head_sha"); expect(compactGiteaRuntime([], [job()], "2026-08-28T19:03:00Z").stateVersion) .toBe(compactGiteaRuntime([], [job()], "2026-08-28T19:04:00Z").stateVersion); }); it("derives a busy runner from an active job when user-level runner discovery omits it", () => { const snapshot = compactGiteaRuntime([], [job()], "2026-08-28T19:02:00Z"); expect(snapshot.runners).toEqual([{ id: 9, name: "itworx-security-runner", status: "observed", busy: true, disabled: false, labels: ["ubuntu-latest"] }]); }); it("does not present dependency-blocked jobs as runnable work", () => { const snapshot = compactGiteaRuntime([], [job({ status: "waiting", runner_id: 0, runner_name: undefined })], "2026-08-28T19:02:00Z"); expect(snapshot.jobs[0].queueState).toBe("dependency-blocked"); expect(snapshot.counts).toEqual({ running: 0, runnable: 0, dependencyBlocked: 1 }); }); it("keeps every active job visible and bounds only completed history", () => { const completed = Array.from({ length: 35 }, (_, index) => job({ id: 100 + index, status: "completed", conclusion: "success" })); const active = Array.from({ length: 38 }, (_, index) => job({ id: 10 + index, status: index < 3 ? "in_progress" : "queued", runner_id: index < 3 ? 9 : 0, runner_name: index < 3 ? "runner" : undefined })); const snapshot = compactGiteaRuntime([], [...completed, ...active], "2026-08-28T19:02:00Z"); expect(snapshot.jobs).toHaveLength(48); expect(snapshot.jobs.filter((item) => item.queueState !== "completed")).toHaveLength(38); expect(snapshot.jobs.filter((item) => item.queueState === "completed")).toHaveLength(10); }); it("coalesces simultaneous refreshes and serves a stale snapshot after a transient failure", async () => { let clock = new Date("2026-08-28T19:00:00Z"); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "runner", status: "online", busy: true, disabled: false, ephemeral: false, labels: [] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "in_progress" ? { total_count: 1, jobs: [job()] } : input.status === undefined ? { total_count: 1, jobs: [job()] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); const [first, simultaneous] = await Promise.all([load(), load()]); expect(first.stateVersion).toBe(simultaneous.stateVersion); expect(listUserActionRunners).toHaveBeenCalledTimes(1); expect(listUserActionJobs).toHaveBeenCalledTimes(4); expect(first.counts).toEqual({ running: 1, runnable: 0, dependencyBlocked: 0 }); clock = new Date("2026-08-28T19:00:16Z"); listUserActionRunners.mockRejectedValue(new Error("temporary upstream failure")); listUserActionJobs.mockRejectedValue(new Error("temporary upstream failure")); const stale = await load(); expect(stale).toMatchObject({ availability: "stale", warning: "temporary upstream failure" }); expect(stale.jobs).toHaveLength(1); }); it("keeps a live snapshot and derives counts when one filtered query fails", async () => { const waitingJob = job({ id: 43, status: "waiting", runner_id: 0, runner_name: undefined }); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 0, runners: [] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => { if (input.status === "pending") throw new Error("unsupported status filter"); if (input.status === undefined) return { total_count: 1, jobs: [waitingJob] }; return { total_count: 0, jobs: [] }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); const snapshot = await load(); expect(snapshot.availability).toBe("live"); expect(snapshot.warning).toContain("Partial Gitea monitoring failure"); expect(snapshot.counts).toEqual({ running: 0, runnable: 0, dependencyBlocked: 1 }); expect(snapshot.jobs[0].queueState).toBe("dependency-blocked"); }); it("keeps runner health unknown when runner discovery fails but queued jobs remain visible", async () => { const queuedJob = job({ status: "queued", runner_id: 0, runner_name: undefined }); const listUserActionRunners = vi.fn().mockRejectedValue(new Error("runner inventory unavailable")); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "queued" ? { total_count: 1, jobs: [queuedJob] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); const snapshot = await load(); const health = evaluateRunnerClaimHealth(snapshot, { now: Date.parse("2026-09-30T00:05:00.000Z") }); expect(snapshot).toMatchObject({ availability: "live", runnerInventoryComplete: false, warning: "Partial Gitea monitoring failure: runner inventory unavailable", runnableJobScan: { complete: true, observed: 1, total: 1 }, }); expect(health).toMatchObject({ status: "incomplete", runnerInventoryComplete: false, coverage: { complete: true, observed: 1, total: 1 }, counts: { queued: 1, unknown: 1, labelBlocked: 0, pollingStalled: 0 }, jobs: [{ state: "unknown", claimableWaitSeconds: 0, requiredLabels: ["ubuntu-latest"], compatibleRunnerNames: [], compatibleIdleRunnerNames: [] }], }); const emptyInventory = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners: vi.fn().mockResolvedValue({ total_count: 0, runners: [] }), listUserActionJobs, }, }); const knownEmptySnapshot = await emptyInventory(); const knownEmptyHealth = evaluateRunnerClaimHealth(knownEmptySnapshot, { now: Date.parse("2026-09-30T00:05:00.000Z") }); expect(knownEmptySnapshot.runnerInventoryComplete).toBe(true); expect(knownEmptyHealth).toMatchObject({ status: "degraded", runnerInventoryComplete: true, counts: { queued: 1, unknown: 0, labelBlocked: 1 }, jobs: [{ state: "label-blocked" }], }); }); it("uses Gitea's documented pending filter for dependency-blocked jobs", async () => { const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 0, runners: [] }); const listUserActionJobs = vi.fn().mockResolvedValue({ total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); await load(); expect(listUserActionJobs).toHaveBeenCalledWith({ status: "pending", limit: 50 }); expect(listUserActionJobs).not.toHaveBeenCalledWith({ status: "waiting", limit: 50 }); }); it("keeps Gitea requested jobs returned by the pending filter visible without a claim-health alarm", async () => { const requestedJob = job({ id: 37_222, run_id: 21_559, name: "quality", status: "requested", runner_id: 0, runner_name: undefined, steps: [], created_at: "2026-10-03T02:20:00.000Z", }); const recentCompleted = Array.from({ length: 30 }, (_, index) => job({ id: 40_000 + index, run_id: 30_000 + index, status: "completed", conclusion: "success", runner_id: 0, runner_name: undefined, created_at: "2026-10-03T02:21:00.000Z", completed_at: "2026-10-03T02:22:00.000Z", })); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => { if (input.status === "pending") return { total_count: 1, jobs: [requestedJob] }; if (input.status === "queued") return { total_count: 0, jobs: [] }; if (input.status === "in_progress") return { total_count: 0, jobs: [] }; return { total_count: recentCompleted.length, jobs: recentCompleted }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); const snapshot = await load(); const requested = snapshot.jobs.find((entry) => entry.id === requestedJob.id); const health = evaluateRunnerClaimHealth(snapshot, { now: Date.parse("2026-10-03T02:24:00.000Z") }); expect(listUserActionJobs).toHaveBeenCalledWith({ status: "pending", limit: 50 }); expect(requested).toMatchObject({ id: 37_222, runId: 21_559, status: "requested", queueState: "dependency-blocked", }); expect(snapshot.counts).toEqual({ running: 0, runnable: 0, dependencyBlocked: 1 }); expect(health).toMatchObject({ status: "healthy", counts: { queued: 0, pollingStalled: 0 }, oldestWaitSeconds: 0, }); }); it("paginates the queued-job health scan so old jobs beyond page one are diagnosed", async () => { let clock = new Date("2026-09-30T00:05:00.000Z"); const queuedJobs = [ ...Array.from({ length: 50 }, (_, index) => job({ id: 1_000 - index, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-30T00:04:30.000Z", })), ...Array.from({ length: 10 }, (_, index) => job({ id: 900 - index, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-30T00:00:00.000Z", })), ]; const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string; limit?: number; page?: number } = {}) => { if (input.status === "queued") { const start = ((input.page ?? 1) - 1) * (input.limit ?? 50); return { total_count: queuedJobs.length, jobs: queuedJobs.slice(start, start + (input.limit ?? 50)) }; } return { total_count: 0, jobs: [] }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); let snapshot = await load(); for (let sample = 0; sample < 4; sample += 1) { clock = new Date(clock.getTime() + 30_000); snapshot = await load(); } const health = evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 120 }); expect(listUserActionJobs).toHaveBeenCalledWith({ status: "queued", limit: 50, page: 1 }); expect(listUserActionJobs).toHaveBeenCalledWith({ status: "queued", limit: 50, page: 2 }); expect(snapshot.runnableJobScan).toEqual({ complete: true, observed: 60, total: 60 }); expect(health).toMatchObject({ status: "stalled", coverage: { complete: true, observed: 60, total: 60 }, counts: { queued: 60, pollingStalled: 60 }, oldestWaitSeconds: 120, }); }); it("does not treat an old database-created job as a long-waiting queued job when first observed", async () => { const clock = new Date("2026-09-30T00:05:00.000Z"); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "queued" ? { total_count: 1, jobs: [job({ status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-29T00:00:00.000Z" })] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); const snapshot = await load(); const health = evaluateRunnerClaimHealth(snapshot, { now: Date.parse("2026-09-30T04:00:00.000Z"), stallSeconds: 120 }); expect(snapshot.jobs[0]).toMatchObject({ createdAt: "2026-09-29T00:00:00.000Z", runnableObservedSince: clock.toISOString(), claimableObservedSince: clock.toISOString(), }); expect(health).toMatchObject({ status: "healthy", counts: { queued: 1, claimable: 1, pollingStalled: 0 }, oldestWaitSeconds: 0, }); }); it("starts polling-stall timing only after compatible idle capacity is observed", async () => { let clock = new Date("2026-09-30T00:00:00.000Z"); let runnerBusy = true; const listUserActionRunners = vi.fn().mockImplementation(async () => ({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: runnerBusy, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] })); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "queued" ? { total_count: 1, jobs: [job({ status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-29T00:00:00.000Z" })] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); let snapshot = await load(); expect(evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 120 }).jobs[0].state).toBe("capacity-wait"); for (let sample = 0; sample < 3; sample += 1) { clock = new Date(clock.getTime() + 30_000); await load(); } runnerBusy = false; clock = new Date(clock.getTime() + 30_000); snapshot = await load(); expect(snapshot.jobs[0]).toMatchObject({ runnableObservedSince: "2026-09-30T00:00:00.000Z", claimableObservedSince: clock.toISOString(), }); expect(evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 120 }).jobs[0].state).toBe("claimable"); for (let sample = 0; sample < 3; sample += 1) { clock = new Date(clock.getTime() + 30_000); snapshot = await load(); expect(evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 120 }).jobs[0].state).toBe("claimable"); } clock = new Date(clock.getTime() + 30_000); snapshot = await load(); const finalHealth = evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 120 }); expect(finalHealth.jobs[0]).toMatchObject({ state: "polling-stalled", waitSeconds: 240, claimableWaitSeconds: 120 }); expect(finalHealth.oldestClaimableWaitSeconds).toBe(120); }); it("does not age a cached health sample with wall-clock time", async () => { let clock = new Date("2026-09-30T00:00:00.000Z"); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "queued" ? { total_count: 1, jobs: [job({ status: "queued", runner_id: 0, runner_name: undefined })] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); await load(); clock = new Date(clock.getTime() + 14_000); const cached = await load(); const health = evaluateRunnerClaimHealth(cached, { now: clock.getTime() + 3_600_000, stallSeconds: 30 }); expect(listUserActionRunners).toHaveBeenCalledTimes(1); expect(health).toMatchObject({ status: "healthy", oldestWaitSeconds: 0, counts: { pollingStalled: 0 } }); }); it("resets observation timing after a long gap between complete samples", async () => { let clock = new Date("2026-09-30T00:00:00.000Z"); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "queued" ? { total_count: 1, jobs: [job({ status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-29T00:00:00.000Z" })] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); await load(); clock = new Date(clock.getTime() + 46_000); const snapshot = await load(); const health = evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 30 }); expect(snapshot.jobs[0].runnableObservedSince).toBe(clock.toISOString()); expect(snapshot.jobs[0].claimableObservedSince).toBe(clock.toISOString()); expect(health).toMatchObject({ status: "healthy", oldestWaitSeconds: 0, counts: { pollingStalled: 0 } }); }); it("resets claimable timing after partial and stale samples", async () => { let clock = new Date("2026-09-30T00:00:00.000Z"); let failSecondPage = false; let failAllJobQueries = false; const queuedJobs = Array.from({ length: 51 }, (_, index) => job({ id: 1_000 - index, status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-29T00:00:00.000Z", })); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string; limit?: number; page?: number } = {}) => { if (failAllJobQueries) throw new Error("job queries unavailable"); if (input.status === "queued") { if (input.page === 2 && failSecondPage) throw new Error("queued page unavailable"); const start = ((input.page ?? 1) - 1) * (input.limit ?? 50); return { total_count: queuedJobs.length, jobs: queuedJobs.slice(start, start + (input.limit ?? 50)) }; } return { total_count: 0, jobs: [] }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); await load(); failSecondPage = true; clock = new Date(clock.getTime() + 30_000); const partial = await load(); expect(partial.runnableJobScan.complete).toBe(false); expect(evaluateRunnerClaimHealth(partial, { now: clock.getTime() }).status).toBe("incomplete"); failSecondPage = false; clock = new Date(clock.getTime() + 15_000); let snapshot = await load(); expect(snapshot.jobs.find((entry) => entry.id === queuedJobs[0].id)?.claimableObservedSince).toBe(clock.toISOString()); failAllJobQueries = true; clock = new Date(clock.getTime() + 15_000); const stale = await load(); expect(stale.availability).toBe("stale"); expect(evaluateRunnerClaimHealth(stale, { now: clock.getTime() }).jobs[0].state).toBe("unknown"); failAllJobQueries = false; clock = new Date(clock.getTime() + 30_000); snapshot = await load(); const health = evaluateRunnerClaimHealth(snapshot, { now: clock.getTime(), stallSeconds: 30 }); expect(snapshot.jobs.find((entry) => entry.id === queuedJobs[0].id)?.claimableObservedSince).toBe(clock.toISOString()); expect(health).toMatchObject({ status: "healthy", counts: { pollingStalled: 0 }, oldestWaitSeconds: 0 }); }); it("restarts runnable timing after a known queued-to-pending-to-queued transition", async () => { let clock = new Date("2026-09-30T00:00:00.000Z"); let queued = true; const queuedJob = job({ status: "queued", runner_id: 0, runner_name: undefined, created_at: "2026-09-29T00:00:00.000Z" }); const pendingJob = { ...queuedJob, status: "pending" }; const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [ { id: 9, name: "validation-1", status: "online", busy: false, disabled: false, ephemeral: false, labels: [ { id: 1, name: "ubuntu-latest", type: "custom" }, ] }, ] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => { if (input.status === "queued") return { total_count: queued ? 1 : 0, jobs: queued ? [queuedJob] : [] }; if (input.status === "pending" || input.status === undefined) return { total_count: queued ? 0 : 1, jobs: queued ? [] : [pendingJob] }; return { total_count: 0, jobs: [] }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock }); const first = await load(); expect(first.jobs[0].runnableObservedSince).toBe(clock.toISOString()); queued = false; clock = new Date(clock.getTime() + 30_000); const pending = await load(); expect(pending.jobs[0].queueState).toBe("dependency-blocked"); queued = true; clock = new Date(clock.getTime() + 30_000); const requeued = await load(); expect(requeued.jobs[0].runnableObservedSince).toBe(clock.toISOString()); expect(requeued.jobs[0].claimableObservedSince).toBe(clock.toISOString()); expect(evaluateRunnerClaimHealth(requeued, { now: clock.getTime(), stallSeconds: 30 }).jobs[0].state).toBe("claimable"); }); it("keeps paginated health scans bounded and marks queues beyond the cap incomplete", async () => { const queuedJobs = Array.from({ length: 501 }, (_, index) => job({ id: 2_000 - index, status: "queued", runner_id: 0, runner_name: undefined, })); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 0, runners: [] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string; limit?: number; page?: number } = {}) => { if (input.status === "queued") { const start = ((input.page ?? 1) - 1) * (input.limit ?? 50); return { total_count: queuedJobs.length, jobs: queuedJobs.slice(start, start + (input.limit ?? 50)) }; } return { total_count: 0, jobs: [] }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); const snapshot = await load(); expect(listUserActionJobs.mock.calls.filter(([input]) => input?.status === "queued")).toHaveLength(10); expect(snapshot.runnableJobScan).toEqual({ complete: false, observed: 500, total: 501 }); expect(evaluateRunnerClaimHealth(snapshot).status).toBe("incomplete"); }); it("does not treat a missing queued total as an empty complete scan", async () => { const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 0, runners: [] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "queued" ? { jobs: [] } as unknown as { total_count: number; jobs: GiteaActionJob[] } : { total_count: 0, jobs: [] }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); const snapshot = await load(); expect(snapshot.runnableJobScan).toEqual({ complete: false, observed: 0, total: null }); expect(snapshot.warning).toContain("invalid queued-job total"); expect(evaluateRunnerClaimHealth(snapshot).status).toBe("incomplete"); }); it("marks the queued health scan incomplete when a later page fails", async () => { const queuedJobs = Array.from({ length: 60 }, (_, index) => job({ id: 1_000 - index, status: "queued" })); const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 0, runners: [] }); const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string; limit?: number; page?: number } = {}) => { if (input.status === "queued") { if (input.page === 2) throw new Error("page two unavailable"); return { total_count: queuedJobs.length, jobs: queuedJobs.slice(0, input.limit ?? 50) }; } return { total_count: 0, jobs: [] }; }); const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } }); const snapshot = await load(); expect(snapshot.runnableJobScan).toEqual({ complete: false, observed: 50, total: 60 }); expect(snapshot.warning).toContain("page two unavailable"); expect(evaluateRunnerClaimHealth(snapshot).status).toBe("incomplete"); }); });