Files
projectbrain-Public/backend/test/activity-runtime.test.ts
T

137 lines
7.7 KiB
TypeScript

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";
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> = {}): 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("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("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 });
});
});