Publish curated projectbrain source from 88fab8df1542019289e947f092c2b387e0825c60
This commit is contained in:
1 parent
48e72d07e0
commit
eca2891b07
45 files changed
+2697
-328
No files matched your search
@@ -2,6 +2,7 @@ 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",
|
||||
@@ -44,6 +45,26 @@ describe("compact activity runtime", () => {
|
||||
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();
|
||||
@@ -123,6 +144,49 @@ describe("compact activity runtime", () => {
|
||||
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: [] });
|
||||
@@ -133,4 +197,361 @@ describe("compact activity runtime", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user