import { describe, expect, it } from "vitest"; import type { RuntimeJob, RuntimeRunner } from "@/shared/api/portfolioTypes"; import { getActivityOperationSections, latestCompletedRuntimeJobByRunner, oldestRunnableRuntimeJob, prioritizeRuntimeJobs, runtimeCompatibleIdleRunnerCount, runtimeJobAgeMinutes, runtimeJobDetail, runtimeJobGroups, runtimeJobLabel, runtimeJobTimingLabel, runtimeQueueExplanation, runtimeRunnerCapacity, runtimeRunnerCapacityPresentation } from "@/shared/lib/activityRuntimePresentation"; const job = (queueState: RuntimeJob["queueState"], input: Partial = {}): RuntimeJob => ({ id: 1, name: "quality", status: queueState === "runnable" ? "queued" : queueState === "dependency-blocked" ? "pending" : queueState === "running" ? "in_progress" : "completed", queueState, completedSteps: 0, totalSteps: 1, url: "https://gitea.example/jobs/1", createdAt: "2026-09-01T12:00:00Z", ...input, }); describe("activity runtime presentation", () => { it("keeps running jobs ahead of newer queued jobs before the display limit", () => { const jobs = [...Array.from({ length: 30 }, (_, i) => job("runnable", { id: 100 + i })), job("running", { id: 1 }), job("running", { id: 2 })]; expect(prioritizeRuntimeJobs(jobs).slice(0, 12).slice(0, 2).map((item) => item.id)).toEqual([2, 1]); expect(jobs[0].id).toBe(100); expect(prioritizeRuntimeJobs([job("completed", { id: 9 }), job("dependency-blocked", { id: 8 }), job("runnable", { id: 7 }), job("running", { id: 6 })]).map((item) => item.id)).toEqual([6, 7, 8, 9]); }); it("orders queued work by actual waiting age rather than newest id", () => { const ordered = prioritizeRuntimeJobs([ job("runnable", { id: 9, createdAt: "2026-09-01T12:09:00Z" }), job("runnable", { id: 7, createdAt: "2026-09-01T12:03:00Z" }), job("runnable", { id: 8, createdAt: "2026-09-01T12:06:00Z" }), ]); expect(ordered.map((item) => item.id)).toEqual([7, 8, 9]); }); it("uses oldest creation time, ascending id ties, and unknown timestamps last for queued jobs", () => { const ordered = prioritizeRuntimeJobs([ job("runnable", { id: 9, createdAt: "2026-09-01T12:03:00Z" }), job("runnable", { id: 4, createdAt: "2026-09-01T12:03:00Z" }), job("runnable", { id: 7, createdAt: "2026-09-01T12:06:00Z" }), job("runnable", { id: 2, createdAt: "not-a-timestamp" }), ]); expect(ordered.map((item) => item.id)).toEqual([4, 9, 7, 2]); }); it("does not present dependency-blocked work as the oldest runnable job", () => { const blocked = job("dependency-blocked", { id: 5, status: "requested", createdAt: "2026-09-01T12:00:00Z" }); expect(runtimeJobLabel(blocked)).toBe("Wacht op eerdere job"); expect(runtimeJobDetail(blocked)).toBe("Start zodra de vereiste eerdere job is afgerond"); expect(oldestRunnableRuntimeJob([ blocked, job("dependency-blocked", { id: 4, createdAt: "2026-09-01T11:00:00Z" }), ])).toBeUndefined(); }); it("selects the oldest visible runnable job while ignoring dependency-blocked jobs", () => { const selected = oldestRunnableRuntimeJob([ job("dependency-blocked", { id: 1, createdAt: "2026-09-01T11:00:00Z" }), job("runnable", { id: 9, createdAt: "2026-09-01T12:09:00Z" }), job("runnable", { id: 4, createdAt: "2026-09-01T12:03:00Z" }), ]); expect(selected?.id).toBe(4); }); it("reports waiting age and compatible idle runner capacity", () => { const queued = job("runnable", { labels: ["linux-validation"], createdAt: "2026-09-01T12:00:00Z" }); const runners: RuntimeRunner[] = [ { id: 1, name: "validation", status: "online", busy: false, disabled: false, labels: ["linux-validation"] }, { id: 2, name: "security", status: "online", busy: false, disabled: false, labels: ["security"] }, { id: 3, name: "busy-validation", status: "online", busy: true, disabled: false, labels: ["linux-validation"] }, ]; const now = Date.parse("2026-09-01T12:07:30Z"); expect(runtimeJobAgeMinutes(queued, now)).toBe(7); expect(runtimeJobTimingLabel(queued, now)).toBe("Wacht 7 min"); expect(runtimeCompatibleIdleRunnerCount(queued, runners)).toBe(1); expect(runtimeCompatibleIdleRunnerCount(job("runnable", { labels: ["missing"] }), runners)).toBe(0); }); it("never presents a queued job as completed", () => { expect(runtimeJobLabel(job("runnable"))).toBe("Wacht op runner"); expect(runtimeQueueExplanation(38, 0, 0)).toContain("nog geen afgeronde jobs"); }); it("keeps capacity unknown when runner discovery is incomplete", () => { const display = runtimeRunnerCapacityPresentation(runtimeRunnerCapacity([]), false); expect(display).toEqual({ value: "?", label: "runnercapaciteit onbekend" }); const explanation = runtimeQueueExplanation(2, 0, 0, false); expect(explanation).toContain("capaciteit is onbekend"); expect(explanation).not.toContain("alle online runners zijn momenteel bezig"); }); it("does not describe a known-empty runner inventory as busy capacity", () => { const explanation = runtimeQueueExplanation(2, 0, 0, true, 0); expect(explanation).toContain("geen online runners beschikbaar"); expect(explanation).not.toContain("alle online runners zijn momenteel bezig"); }); it("describes positive online capacity with no idle runners as busy", () => { expect(runtimeQueueExplanation(2, 0, 0, true, 3)).toContain("alle online runners zijn momenteel bezig"); }); it("explains persistent queueing when free runners exist", () => { expect(runtimeQueueExplanation(2, 0, 1)).toContain("1 vrije runner"); expect(runtimeQueueExplanation(2, 0, 2)).toContain("joblabels"); }); it("keeps runner and dependency queues separate", () => { const groups = runtimeJobGroups([job("running"), job("runnable", { id: 2 }), job("dependency-blocked", { id: 3 })]); expect(groups.running).toHaveLength(1); expect(groups.runnable).toHaveLength(1); expect(groups.dependencyBlocked).toHaveLength(1); }); it("classifies current work before queues and historical outcomes", () => { const sections = getActivityOperationSections([ job("completed", { id: 1, conclusion: "success" }), job("dependency-blocked", { id: 2 }), job("runnable", { id: 3 }), job("running", { id: 4 }), job("completed", { id: 5, conclusion: "failure" }), ]); expect(sections.active.map((item) => item.id)).toEqual([4]); expect(sections.waitingForRunner.map((item) => item.id)).toEqual([3]); expect(sections.waitingForDependency.map((item) => item.id)).toEqual([2]); expect(sections.recentCompleted.map((item) => item.id)).toEqual([1]); expect(sections.recentFailed.map((item) => item.id)).toEqual([5]); }); it("counts only online non-busy runners as free capacity", () => { const runners: RuntimeRunner[] = [ { id: 1, name: "busy", status: "online", busy: true, disabled: false, labels: [] }, { id: 2, name: "idle", status: "online", busy: false, disabled: false, labels: [] }, { id: 3, name: "offline", status: "offline", busy: false, disabled: false, labels: [] }, { id: 4, name: "disabled", status: "online", busy: false, disabled: true, labels: [] }, ]; expect(runtimeRunnerCapacity(runners)).toEqual({ total: 4, enabled: 3, online: 2, busy: 1, idle: 1, disabled: 1 }); }); it("keeps the latest completed activity per named runner", () => { const latest = latestCompletedRuntimeJobByRunner([ job("completed", { id: 1, runnerName: "runner-a", repository: "Jens/old", completedAt: "2026-09-01T12:05:00Z" }), job("completed", { id: 2, runnerName: "runner-a", repository: "Jens/new", completedAt: "2026-09-01T12:10:00Z" }), job("completed", { id: 3, runnerName: "runner-b", repository: "Jens/other", completedAt: "2026-09-01T12:07:00Z" }), job("running", { id: 4, runnerName: "runner-a" }), ]); expect(latest["runner-a"]?.id).toBe(2); expect(latest["runner-b"]?.repository).toBe("Jens/other"); }); it("translates final conclusions independently from queue state", () => { expect(runtimeJobLabel(job("completed", { conclusion: "success" }))).toBe("Geslaagd"); expect(runtimeJobLabel(job("completed", { conclusion: "failure" }))).toBe("Mislukt"); }); });