Files
projectbrain-Public/backend/test/repository-analysis.test.ts

420 lines
28 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import { loadEnv } from "../src/config/env.js";
import { GiteaClient } from "../src/integrations/gitea/client.js";
import type { LinkedRepository } from "../src/integrations/gitea/service.js";
import type { GiteaActionRun, GiteaPullRequest } from "../src/integrations/gitea/types.js";
import { analyzeLinkedRepository, classifyActionFindings, loadActiveWorkflowEvents, parseWorkflowTriggerEvents } from "../src/services/repositoryAnalysis.js";
import { projectAnalysisItemScope, resolveAnalysisTargets } from "../src/repositories/analysisJobRepository.js";
const envInput = {
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",
} satisfies NodeJS.ProcessEnv;
const env = loadEnv(envInput);
const link: LinkedRepository = {
id: "repo-1", projectId: "project-1", projectSlug: "project-one", projectName: "Project One",
owner: "Acme", repository: "Repo", fullName: "Acme/Repo", defaultBranch: "main", accessMode: "read_write",
role: "primary", isPrimary: true, syncStatus: "ready",
};
const actionRun = (input: Partial<GiteaActionRun> & Pick<GiteaActionRun, "id" | "run_number">): GiteaActionRun => ({
display_title: "CI",
path: "ci.yml@refs/heads/main",
event: "push",
head_sha: `sha-${input.run_number}`,
head_branch: "main",
status: "completed",
conclusion: "success",
html_url: `https://gitea.example/actions/runs/${input.id}`,
completed_at: `2026-08-28T00:${String(input.run_number).padStart(2, "0")}:00Z`,
...input,
});
const pullRequest = (number: number, branch: string): GiteaPullRequest => ({
id: number,
number,
title: `PR ${number}`,
state: "open",
html_url: `https://gitea.example/pulls/${number}`,
merged: false,
created_at: "2026-08-28T00:00:00Z",
updated_at: "2026-08-28T00:00:00Z",
head: { ref: branch, sha: `head-${number}` },
base: { ref: "main", sha: "main-sha" },
});
describe("actionable Gitea Actions classification", () => {
it.each([
{ includeBranches: false, includePullRequests: true, includeActions: true, absent: ["REPOSITORY_EMPTY", "DEFAULT_BRANCH_UNPROTECTED", "BRANCH_SPRAWL"] },
{ includeBranches: true, includePullRequests: false, includeActions: true, absent: ["OPEN_PR_BACKLOG", "TOPIC_BRANCH_ACTION_FAILED"] },
{ includeBranches: true, includePullRequests: true, includeActions: false, absent: ["DEFAULT_BRANCH_ACTION_FAILED", "SCHEDULED_ACTION_FAILED", "TOPIC_BRANCH_ACTION_FAILED"] },
{ includeBranches: true, includePullRequests: true, includeActions: true, absent: [] },
])("records only actual rule coverage for partial analysis %j", async ({ absent, ...request }) => {
const client = new GiteaClient("https://gitea.example", "fixture-token");
vi.spyOn(client, "getRepository").mockResolvedValue({ default_branch: "main", archived: false, private: true, permissions: { push: true } } as never);
const branches = vi.spyOn(client, "listBranches").mockResolvedValue([{ name: "main", protected: true }] as never);
const prs = vi.spyOn(client, "listPullRequests").mockResolvedValue([]);
const actions = vi.spyOn(client, "listActionRuns").mockResolvedValue({ workflow_runs: [actionRun({ id: 1, run_number: 1 })] } as never);
vi.spyOn(client, "getContents").mockResolvedValue([]);
const detail = await analyzeLinkedRepository(env, link, request, client);
for (const code of absent) expect(detail.evaluatedRuleCodes).not.toContain(code);
expect(branches).toHaveBeenCalledTimes(request.includeBranches ? 1 : 0);
expect(prs).toHaveBeenCalledTimes(request.includePullRequests ? 1 : 0);
expect(actions).toHaveBeenCalledTimes(request.includeActions ? 1 : 0);
if (!absent.length) expect(detail.evaluatedRuleCodes).toEqual(expect.arrayContaining([
"DEFAULT_BRANCH_UNPROTECTED", "OPEN_PR_BACKLOG", "DEFAULT_BRANCH_ACTION_FAILED", "TOPIC_BRANCH_ACTION_FAILED",
]));
});
it("does not revive an older failure that finished after a newer successful run", () => {
const findings = classifyActionFindings({ defaultBranch: "main", recentRuns: [], openPullRequests: [], defaultBranchRuns: [
actionRun({ id: 1, run_number: 1, conclusion: "failure", completed_at: "2026-09-09T12:00:00Z" }),
actionRun({ id: 2, run_number: 2, conclusion: "success", completed_at: "2026-09-09T11:00:00Z" }),
] });
expect(findings).toEqual([]);
});
it("retains a newer failure even if an older successful run finishes later", () => {
const findings = classifyActionFindings({ defaultBranch: "main", recentRuns: [], openPullRequests: [], defaultBranchRuns: [
actionRun({ id: 1, run_number: 1, conclusion: "success", completed_at: "2026-09-09T12:00:00Z" }),
actionRun({ id: 2, run_number: 2, conclusion: "failure", completed_at: "2026-09-09T11:00:00Z" }),
] });
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({ code: "DEFAULT_BRANCH_ACTION_FAILED", evidence: { runId: 2 } });
});
it("does not hide a failing schedule behind a successful push in the same workflow", () => {
const findings = classifyActionFindings({ defaultBranch: "main", recentRuns: [], openPullRequests: [], defaultBranchRuns: [
actionRun({ id: 1, run_number: 1, event: "schedule", conclusion: "failure" }),
actionRun({ id: 2, run_number: 2, event: "push", conclusion: "success" }),
] });
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({ code: "SCHEDULED_ACTION_FAILED", severity: "warning", evidence: { consecutiveFailures: 1 } });
});
it("does not count push failures toward the scheduled failure threshold", () => {
const findings = classifyActionFindings({ defaultBranch: "main", recentRuns: [], openPullRequests: [], defaultBranchRuns: [
actionRun({ id: 1, run_number: 1, conclusion: "failure" }),
actionRun({ id: 2, run_number: 2, conclusion: "failure" }),
actionRun({ id: 3, run_number: 3, event: "schedule", conclusion: "failure" }),
] });
expect(findings).toHaveLength(2);
expect(findings.find((finding) => finding.code === "SCHEDULED_ACTION_FAILED")).toMatchObject({ severity: "warning", evidence: { consecutiveFailures: 1 } });
expect(findings.find((finding) => finding.code === "DEFAULT_BRANCH_ACTION_FAILED")).toMatchObject({ severity: "critical", evidence: { consecutiveFailures: 2 } });
});
it.each(["pull_request", "pull_request_target", "push"])("treats an explicit PR ref as PR evidence even with head_branch main (%s)", (event) => {
const run = actionRun({ id: 2, run_number: 2, event, path: "ci.yml@refs/pull/7/merge", head_branch: "main", conclusion: "failure" });
const findings = classifyActionFindings({ defaultBranch: "main", defaultBranchRuns: [actionRun({ id: 1, run_number: 1 }), run], recentRuns: [run], openPullRequests: [pullRequest(7, "codex/work")] });
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({ code: "TOPIC_BRANCH_ACTION_FAILED", severity: "info", evidence: { branch: "codex/work" } });
});
it("ignores in-flight agent branches while retaining a real default-branch failure", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [actionRun({ id: 1, run_number: 1, conclusion: "failure" })],
recentRuns: [
actionRun({ id: 2, run_number: 2, head_branch: "codex/work", path: "ci.yml@refs/heads/codex/work", conclusion: "failure" }),
actionRun({ id: 1, run_number: 1, conclusion: "failure" }),
],
openPullRequests: [pullRequest(2, "codex/work")],
});
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({ code: "DEFAULT_BRANCH_ACTION_FAILED", severity: "critical", subject: "default:ci.yml" });
});
it("escalates a scheduled workflow only after three consecutive failures", () => {
const scheduled = (count: number) => classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: Array.from({ length: count }, (_, index) => actionRun({
id: 10 - index,
run_number: 10 - index,
event: "schedule",
path: "canary.yml@refs/heads/main",
conclusion: "failure",
})),
recentRuns: [],
openPullRequests: [],
})[0];
expect(scheduled(2)).toMatchObject({ code: "SCHEDULED_ACTION_FAILED", severity: "warning", evidence: { consecutiveFailures: 2 } });
expect(scheduled(3)).toMatchObject({ code: "SCHEDULED_ACTION_FAILED", severity: "critical", evidence: { consecutiveFailures: 3 } });
});
it("keeps the previous completed failure while a newer run is still in progress", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [
actionRun({ id: 3, run_number: 3, status: "in_progress", conclusion: undefined }),
actionRun({ id: 2, run_number: 2, conclusion: "failure" }),
],
recentRuns: [],
openPullRequests: [],
});
expect(findings[0]).toMatchObject({ code: "DEFAULT_BRANCH_ACTION_FAILED", evidence: { runId: 2 } });
});
it("does not treat cancelled or skipped conclusions as failures", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [actionRun({ id: 2, run_number: 2, conclusion: "cancelled" }), actionRun({ id: 1, run_number: 1, conclusion: "failure" })],
recentRuns: [],
openPullRequests: [],
});
expect(findings).toEqual([]);
});
it("defensively rejects non-default runs even if Gitea ignores the branch query", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [
actionRun({ id: 3, run_number: 3, head_branch: "codex/work", path: "ci.yml@refs/heads/codex/work", conclusion: "failure" }),
actionRun({ id: 2, run_number: 2, head_branch: "main", conclusion: "success" }),
],
recentRuns: [],
openPullRequests: [],
});
expect(findings).toEqual([]);
});
it("keeps workflows independent and includes actionable run evidence", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [
actionRun({ id: 5, run_number: 5, path: "build.yml@refs/heads/main", conclusion: "success" }),
actionRun({ id: 4, run_number: 4, path: "deploy.yml@refs/heads/main", conclusion: "failure", html_url: "https://gitea.example/actions/runs/4" }),
],
recentRuns: [],
openPullRequests: [],
});
expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({
code: "DEFAULT_BRANCH_ACTION_FAILED",
subject: "default:deploy.yml",
evidence: { workflow: "deploy.yml", branch: "main", runUrl: "https://gitea.example/actions/runs/4" },
});
});
it("ignores historical failures for workflow files removed from the default branch", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [
actionRun({ id: 5, run_number: 5, path: "managed-validation.yml@refs/heads/main", conclusion: "success" }),
actionRun({ id: 4, run_number: 4, path: "ci.yml@refs/heads/main", conclusion: "failure" }),
],
recentRuns: [],
openPullRequests: [],
activeWorkflows: ["managed-validation.yml"],
});
expect(findings).toEqual([]);
});
it("retains failures for workflow files that still exist on the default branch", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [actionRun({ id: 4, run_number: 4, path: "ci.yml@refs/heads/main", conclusion: "failure" })],
recentRuns: [],
openPullRequests: [],
activeWorkflows: ["ci.yml"],
});
expect(findings[0]).toMatchObject({ code: "DEFAULT_BRANCH_ACTION_FAILED", subject: "default:ci.yml" });
});
it("ignores a historical event removed from a workflow while retaining current events", () => {
const historicalPush = actionRun({ id: 11, run_number: 11, path: "unraid-deploy.yml@refs/heads/main", event: "push", conclusion: "failure" });
const manualFailure = actionRun({ id: 12, run_number: 12, path: "unraid-deploy.yml@refs/heads/main", event: "workflow_dispatch", conclusion: "failure" });
const currentEvents = new Map([["unraid-deploy.yml", new Set(["workflow_dispatch"])]]);
const obsolete = classifyActionFindings({ defaultBranch: "main", defaultBranchRuns: [historicalPush], recentRuns: [], openPullRequests: [], activeWorkflowEvents: currentEvents });
expect(obsolete.some((finding) => finding.code === "DEFAULT_BRANCH_ACTION_FAILED")).toBe(false);
const current = classifyActionFindings({ defaultBranch: "main", defaultBranchRuns: [manualFailure, historicalPush], recentRuns: [], openPullRequests: [], activeWorkflowEvents: currentEvents });
expect(current[0]).toMatchObject({ code: "DEFAULT_BRANCH_ACTION_FAILED", subject: "default:unraid-deploy.yml", evidence: { event: "workflow_dispatch", runId: 12 } });
});
it("keeps historical failures actionable when current workflow events are unknown", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [actionRun({ id: 4, run_number: 4, path: "ci.yml@refs/heads/main", conclusion: "failure" })],
recentRuns: [],
openPullRequests: [],
activeWorkflowEvents: new Map([["ci.yml", undefined]]),
});
expect(findings[0]).toMatchObject({ code: "DEFAULT_BRANCH_ACTION_FAILED", subject: "default:ci.yml" });
});
it("parses common top-level workflow trigger forms and rejects ambiguous syntax", () => {
expect([...parseWorkflowTriggerEvents("on: push\njobs: {}") ?? []]).toEqual(["push"]);
expect([...parseWorkflowTriggerEvents("on: [push, pull_request]\njobs: {}") ?? []]).toEqual(["push", "pull_request"]);
expect([...parseWorkflowTriggerEvents("on:\n push:\n branches: [main]\n workflow_dispatch:\njobs: {}") ?? []]).toEqual(["push", "workflow_dispatch"]);
expect(parseWorkflowTriggerEvents("on: { push: null }\njobs: {}"), "inline maps stay conservative").toBeUndefined();
expect(parseWorkflowTriggerEvents("jobs:\n build:\n runs-on: ubuntu-latest"), "missing on stays conservative").toBeUndefined();
});
it("loads current workflow events and falls back conservatively when one file cannot be read", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("manual.yml")) return new Response(JSON.stringify({ type: "file", name: "manual.yml", path: ".gitea/workflows/manual.yml", sha: "manual", size: 20, encoding: "base64", content: Buffer.from("on:\n workflow_dispatch:\n").toString("base64") }), { status: 200 });
return new Response(JSON.stringify({ message: "unavailable" }), { status: 503 });
});
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
const events = await loadActiveWorkflowEvents(client, "Acme", "Repo", "main", [
{ type: "file", name: "manual.yml", path: ".gitea/workflows/manual.yml", sha: "manual", size: 20 },
{ type: "file", name: "unknown.yml", path: ".gitea/workflows/unknown.yml", sha: "unknown", size: 20 },
]);
expect([...(events?.get("manual.yml") ?? [])]).toEqual(["workflow_dispatch"]);
expect(events?.has("unknown.yml")).toBe(true);
expect(events?.get("unknown.yml")).toBeUndefined();
});
it("downgrades an open pull-request failure to info and ignores closed pull requests", () => {
const run = actionRun({
id: 8,
run_number: 8,
event: "pull_request",
head_branch: undefined,
path: "release-gates.yml@refs/pull/7/head",
conclusion: "failure",
});
const active = classifyActionFindings({ defaultBranch: "main", defaultBranchRuns: [actionRun({ id: 1, run_number: 1 })], recentRuns: [run], openPullRequests: [pullRequest(7, "feature/release")] });
const closed = classifyActionFindings({ defaultBranch: "main", defaultBranchRuns: [actionRun({ id: 1, run_number: 1 })], recentRuns: [run], openPullRequests: [] });
expect(active).toHaveLength(1);
expect(active[0]).toMatchObject({ code: "TOPIC_BRANCH_ACTION_FAILED", severity: "info", evidence: { branch: "feature/release" } });
expect(closed).toEqual([]);
});
it("recognizes Gitea pull-request merge refs", () => {
const findings = classifyActionFindings({
defaultBranch: "main",
defaultBranchRuns: [actionRun({ id: 1, run_number: 1 })],
recentRuns: [actionRun({ id: 9, run_number: 9, head_branch: undefined, event: "pull_request", path: "ci.yml@refs/pull/9/merge", conclusion: "failure" })],
openPullRequests: [pullRequest(9, "feature/merge-ref")],
});
expect(findings[0]).toMatchObject({ code: "TOPIC_BRANCH_ACTION_FAILED", evidence: { branch: "feature/merge-ref" } });
});
it("reports absent default-branch CI and an agent branch configured as default separately", () => {
expect(classifyActionFindings({ defaultBranch: "main", defaultBranchRuns: [], recentRuns: [], openPullRequests: [] })[0]).toMatchObject({ code: "DEFAULT_BRANCH_CI_ABSENT", severity: "warning" });
expect(classifyActionFindings({ defaultBranch: "codex/temporary", defaultBranchRuns: [], recentRuns: [], openPullRequests: [] })).toEqual([
expect.objectContaining({ code: "DEFAULT_BRANCH_IS_AGENT_BRANCH", severity: "warning", subject: "codex/temporary" }),
]);
});
it("reclassifies the frozen twelve-repository production sample without hiding failures", () => {
const failed = (branch: string, event: string = "push", path = "ci.yml") => actionRun({ id: 10, run_number: 10, head_branch: branch, event, path: `${path}@refs/heads/${branch}`, conclusion: "failure" });
const cases = [
{ repository: "Gifted", defaultBranch: "main", defaults: [failed("main")], recent: [] },
{ repository: "ITWorx-Pulse", defaultBranch: "master", defaults: [failed("master")], recent: [failed("codex/integration")] },
{ repository: "Microsoft-Cloud-Operations-Platform", defaultBranch: "main", defaults: [failed("main", "push", "unraid-deploy.yml")], recent: [] },
{ repository: "MobilityOps", defaultBranch: "master", defaults: [failed("master", "schedule", "live-canary.yml"), actionRun({ ...failed("master", "schedule", "live-canary.yml"), id: 9, run_number: 9 }), actionRun({ ...failed("master", "schedule", "live-canary.yml"), id: 8, run_number: 8 })], recent: [] },
{ repository: "PentestForge", defaultBranch: "main", defaults: [], recent: [failed("chatgpt/m1-control-plane", "workflow_dispatch", "managed-validation.yml")] },
{ repository: "Portfolio", defaultBranch: "main", defaults: [failed("main", "push", "deploy.yml")], recent: [] },
{ repository: "RAGcore", defaultBranch: "main", defaults: [failed("main", "push", "unraid-deploy.yml")], recent: [] },
{ repository: "chimera-retroarch", defaultBranch: "codex/ps5-loader-entry-diagnosis", defaults: [failed("codex/ps5-loader-entry-diagnosis", "schedule", "crowdin-daily.yml")], recent: [] },
{ repository: "geointel", defaultBranch: "main", defaults: [], recent: [actionRun({ id: 7, run_number: 7, head_branch: undefined, event: "pull_request", path: "release-gates.yml@refs/pull/1/head", conclusion: "failure" })], pullRequests: [pullRequest(1, "feature/geointel")] },
{ repository: "itworx-knowledge-hub", defaultBranch: "main", defaults: [failed("main")], recent: [] },
{ repository: "opscenter", defaultBranch: "main", defaults: [actionRun({ id: 6, run_number: 6, head_branch: "main", conclusion: "success" })], recent: [failed("chatgpt/remediation")] },
{ repository: "spoortrack", defaultBranch: "main", defaults: [], recent: [actionRun({ id: 5, run_number: 5, head_branch: undefined, event: "pull_request", path: "ci.yml@refs/pull/1/head", conclusion: "failure" })], pullRequests: [pullRequest(1, "feature/spoortrack")] },
];
const classified = cases.map((entry) => ({
repository: entry.repository,
findings: classifyActionFindings({ defaultBranch: entry.defaultBranch, defaultBranchRuns: entry.defaults, recentRuns: entry.recent, openPullRequests: entry.pullRequests ?? [] }),
}));
expect(classified.filter((entry) => entry.findings.some((finding) => finding.severity === "critical")).map((entry) => entry.repository)).toEqual([
"Gifted", "ITWorx-Pulse", "Microsoft-Cloud-Operations-Platform", "MobilityOps", "Portfolio", "RAGcore", "itworx-knowledge-hub",
]);
expect(classified.find((entry) => entry.repository === "opscenter")?.findings).toEqual([]);
expect(classified.find((entry) => entry.repository === "PentestForge")?.findings.map((finding) => finding.code)).toEqual(["DEFAULT_BRANCH_CI_ABSENT"]);
expect(classified.find((entry) => entry.repository === "chimera-retroarch")?.findings.map((finding) => finding.code)).toEqual(["DEFAULT_BRANCH_IS_AGENT_BRANCH"]);
expect(classified.find((entry) => entry.repository === "geointel")?.findings.map((finding) => finding.code)).toEqual(["DEFAULT_BRANCH_CI_ABSENT", "TOPIC_BRANCH_ACTION_FAILED"]);
expect(classified.find((entry) => entry.repository === "spoortrack")?.findings.map((finding) => finding.code)).toEqual(["DEFAULT_BRANCH_CI_ABSENT", "TOPIC_BRANCH_ACTION_FAILED"]);
});
});
describe("bounded repository analysis", () => {
it("matches project-scoped jobs by internal id or browser-facing slug", () => {
expect(projectAnalysisItemScope("web-privacy-auditor")).toEqual({
OR: [{ projectId: "web-privacy-auditor" }, { projectSlug: "web-privacy-auditor" }],
});
});
it("discovers unlinked current and future repositories for an unscoped portfolio job", () => {
const repositories = [
{ id: 1, name: "Repo", full_name: "Acme/Repo", private: true, archived: false, default_branch: "main", html_url: "https://gitea.example/Acme/Repo", updated_at: "2026-08-26T00:00:00Z", permissions: { push: true }, owner: { login: "Acme" } },
{ id: 2, name: "Future", full_name: "Acme/Future", private: true, archived: false, default_branch: "trunk", html_url: "https://gitea.example/Acme/Future", updated_at: "2026-08-26T00:00:00Z", permissions: { pull: true }, owner: { login: "Acme" } },
];
const targets = resolveAnalysisTargets([link], repositories, { includeBranches: true, includePullRequests: true, includeActions: true });
expect(targets.map((target) => target.fullName)).toEqual(["Acme/Future", "Acme/Repo"]);
expect(targets.find((target) => target.fullName === "Acme/Repo")?.repositoryLinkId).toBe("repo-1");
expect(targets.find((target) => target.fullName === "Acme/Future")).toMatchObject({ repositoryLinkId: undefined, projectSlug: "gitea-all", accessMode: "read_only" });
});
it("keeps project-scoped jobs limited to explicit ProjectBrain links", () => {
const targets = resolveAnalysisTargets([link], [], { projectIds: ["project-one"], includeBranches: true, includePullRequests: true, includeActions: true });
expect(targets).toHaveLength(1);
expect(targets[0].repositoryLinkId).toBe("repo-1");
});
it("reduces several Gitea reads to a compact finding summary", async () => {
const branches = Array.from({ length: 26 }, (_, index) => ({ name: index === 0 ? "main" : `branch-${index}`, commit: { id: `sha-${index}` }, protected: false }));
const pullRequests = Array.from({ length: 11 }, (_, index) => ({ id: index, number: index, title: `PR ${index}`, state: "open", html_url: `https://gitea.example/pr/${index}`, merged: false, created_at: "2026-08-26T00:00:00Z", updated_at: "2026-08-26T00:00:00Z", head: { ref: `branch-${index}`, sha: `sha-${index}` }, base: { ref: "main", sha: "main-sha" } }));
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("/actions/runs")) return new Response(JSON.stringify({ total_count: 1, workflow_runs: [{ id: 1, run_number: 1, display_title: "CI", path: "ci.yml", event: "push", head_sha: "sha", head_branch: "main", status: "completed", conclusion: "failure", html_url: "https://gitea.example/run/1" }] }), { status: 200 });
if (url.includes("/contents/.gitea/workflows/ci.yml")) return new Response(JSON.stringify({ type: "file", name: "ci.yml", path: ".gitea/workflows/ci.yml", sha: "workflow-file-sha", size: 20, encoding: "base64", content: Buffer.from("on:\n push:\n").toString("base64") }), { status: 200 });
if (url.includes("/contents/.gitea/workflows")) return new Response(JSON.stringify([{ type: "file", name: "ci.yml", path: ".gitea/workflows/ci.yml", sha: "workflow-sha", size: 1 }]), { status: 200 });
if (url.includes("/branches")) return new Response(JSON.stringify(branches), { status: 200 });
if (url.includes("/pulls")) return new Response(JSON.stringify(pullRequests), { status: 200 });
return new Response(JSON.stringify({ id: 1, name: "Repo", full_name: "Acme/Repo", private: true, archived: false, default_branch: "main", html_url: "https://gitea.example/Acme/Repo", updated_at: "2026-08-26T00:00:00Z", permissions: { push: true }, owner: { login: "Acme" } }), { status: 200 });
});
const detail = await analyzeLinkedRepository(env, link, { includeBranches: true, includePullRequests: true, includeActions: true }, new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch));
expect(detail.branchSummary).toMatchObject({ total: 26, defaultProtected: false });
expect(detail.pullRequestSummary).toEqual({ open: 11 });
expect(detail.findings.map((finding) => finding.code)).toEqual(expect.arrayContaining(["DEFAULT_BRANCH_UNPROTECTED", "BRANCH_SPRAWL", "OPEN_PR_BACKLOG", "DEFAULT_BRANCH_ACTION_FAILED"]));
expect(detail.findings.find((finding) => finding.code === "DEFAULT_BRANCH_ACTION_FAILED")?.evidence).toMatchObject({ runUrl: "https://gitea.example/run/1", branch: "main" });
expect(JSON.stringify(detail).length).toBeLessThan(8_000);
expect(fetcher).toHaveBeenCalledTimes(7);
expect(fetcher.mock.calls.map(([input]) => String(input))).toContainEqual(expect.stringContaining("branch=main"));
});
it("treats Gitea's empty-repository pull-request 404 as an empty collection", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("/branches")) return new Response("[]", { status: 200 });
if (url.includes("/pulls")) return new Response(JSON.stringify({ message: "not found" }), { status: 404 });
if (url.includes("/actions/runs")) return new Response(JSON.stringify({ total_count: 0, workflow_runs: [] }), { status: 200 });
if (url.includes("/contents/.gitea/workflows")) return new Response(JSON.stringify({ message: "not found" }), { status: 404 });
return new Response(JSON.stringify({ id: 1, name: "Repo", full_name: "Acme/Repo", private: true, archived: false, default_branch: "main", html_url: "https://gitea.example/Acme/Repo", updated_at: "2026-08-26T00:00:00Z", permissions: { push: true }, owner: { login: "Acme" } }), { status: 200 });
});
const detail = await analyzeLinkedRepository(env, link, { includeBranches: true, includePullRequests: true, includeActions: true }, new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch));
expect(detail.pullRequestSummary).toEqual({ open: 0 });
expect(detail.findings).toEqual([{ severity: "info", code: "REPOSITORY_EMPTY", message: "Repository bevat nog geen commit of branch." }]);
});
it("enforces safe worker concurrency and timeout bounds in configuration", () => {
expect(() => loadEnv({ ...envInput, PROJECTBRAIN_ANALYSIS_REPOSITORY_CONCURRENCY: "3" })).toThrow();
expect(() => loadEnv({ ...envInput, PROJECTBRAIN_ANALYSIS_JOB_TIMEOUT_SECONDS: "10" })).toThrow();
});
});