Files
projectbrain-Public/backend/test/remediation-regressions.test.ts

148 lines
5.7 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 { assertSuccessfulActionRuns } from "../src/integrations/gitea/policy.js";
import type { GiteaActionRun, GiteaContent } from "../src/integrations/gitea/types.js";
import { decodeFile, MAX_MCP_READ_BYTES } from "../src/mcp/resultHelpers.js";
import { serializeAnalysisJob } from "../src/repositories/analysisJobRepository.js";
const actionRun = (input: {
id: number;
conclusion: string;
workflow?: string;
status?: string;
headSha?: string;
}): GiteaActionRun => ({
id: input.id,
run_number: input.id,
run_attempt: 1,
display_title: input.workflow ?? "Managed validation / full",
path: `.gitea/workflows/${input.workflow ?? "managed-validation.yml"}`,
event: input.workflow ? "pull_request" : "workflow_dispatch",
head_sha: input.headSha ?? "current-sha",
head_branch: "chatgpt/feature",
status: input.status ?? "completed",
conclusion: input.conclusion,
html_url: `https://gitea.example.com/runs/${input.id}`,
jobs: input.workflow ? [] : [{
id: input.id * 10,
html_url: `https://gitea.example.com/runs/${input.id}/jobs/${input.id * 10}`,
run_id: input.id,
name: "full",
labels: [],
run_attempt: 1,
head_sha: input.headSha ?? "current-sha",
head_branch: "chatgpt/feature",
status: input.status ?? "completed",
conclusion: input.conclusion,
runner_id: 1,
steps: [],
created_at: "2026-10-06T21:33:06Z",
}],
});
const productionEnv = {
NODE_ENV: "production",
DATABASE_URL: "postgresql://projectbrain:strong-db-password@postgres:5432/projectbrain",
PROJECTBRAIN_DATA_SOURCE: "database",
PROJECTBRAIN_OWNER_USERNAME: "owner",
PROJECTBRAIN_OWNER_PASSWORD: "correct-horse-battery-staple",
PROJECTBRAIN_AUTH_SECRET: "production-secret-with-at-least-thirty-two-characters",
} as const;
describe("review remediation regressions", () => {
it("authorizes a successful rerun of the same workflow and revision without discarding evidence", () => {
const runs = [
actionRun({ id: 10, conclusion: "failure" }),
actionRun({ id: 11, conclusion: "success" }),
];
expect(assertSuccessfulActionRuns(runs, "chatgpt/feature", "current-sha")).toEqual(runs);
});
it("still fails closed when the latest run of any workflow is not successful", () => {
expect(() => assertSuccessfulActionRuns([
actionRun({ id: 9, conclusion: "success" }),
actionRun({ id: 10, conclusion: "success", workflow: "quality.yml" }),
actionRun({ id: 11, conclusion: "failure", workflow: "security.yml" }),
], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
expect(() => assertSuccessfulActionRuns([
actionRun({ id: 12, conclusion: "success" }),
actionRun({ id: 13, conclusion: "", status: "in_progress" }),
], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_FULL_VALIDATION_REQUIRED");
});
it("rejects shipped placeholder credentials in production", () => {
expect(() => loadEnv({
...productionEnv,
PROJECTBRAIN_OWNER_PASSWORD: "replace-with-a-unique-password-of-at-least-12-characters",
})).toThrow("PROJECTBRAIN_OWNER_PASSWORD must not use a placeholder");
expect(() => loadEnv({
...productionEnv,
PROJECTBRAIN_AUTH_SECRET: "replace-with-at-least-32-random-characters",
})).toThrow("PROJECTBRAIN_AUTH_SECRET must not use a placeholder");
expect(() => loadEnv({
...productionEnv,
DATABASE_URL: "postgresql://projectbrain:projectbrain@postgres:5432/projectbrain",
})).toThrow("DATABASE_URL database password must not use a placeholder");
});
it("bounds complete MCP repository file reads", () => {
const file = (size: number): GiteaContent => ({
type: "file",
name: "large.txt",
path: "large.txt",
sha: "abc",
size,
encoding: "base64",
content: Buffer.from("safe content").toString("base64"),
html_url: "https://gitea.example/large.txt",
});
expect(decodeFile(file(MAX_MCP_READ_BYTES))).toBe("safe content");
expect(() => decodeFile(file(MAX_MCP_READ_BYTES + 1))).toThrow("MCP_FILE_READ_TOO_LARGE");
});
it("deletes the managed repository webhook through an exact bounded endpoint", async () => {
const fetcher = vi.fn(async () => new Response(undefined, { status: 204 }));
const client = new GiteaClient("https://gitea.example.com", "service-token", fetcher as typeof fetch);
await client.deleteRepositoryWebhook("Acme", "Repo", 42);
expect(String(fetcher.mock.calls[0][0])).toContain("/api/v1/repos/Acme/Repo/hooks/42");
expect(fetcher.mock.calls[0][1]?.method).toBe("DELETE");
});
it("treats partially successful analysis jobs as terminal but actionable", () => {
const job = serializeAnalysisJob({
id: "job-partial",
kind: "portfolio_health",
status: "partial",
actor: "owner",
progressCurrent: 7,
progressTotal: 10,
summaryText: "7 repositories geanalyseerd, 3 mislukt.",
summaryJson: { repositoryCount: 10, succeeded: 7, failed: 3, findingCount: 2 },
errorCode: "ANALYSIS_PARTIAL_FAILURE",
cancelRequestedAt: null,
queuedAt: new Date("2026-08-29T00:00:00Z"),
startedAt: new Date("2026-08-29T00:00:01Z"),
heartbeatAt: new Date("2026-08-29T00:01:00Z"),
completedAt: new Date("2026-08-29T00:01:01Z"),
expiresAt: new Date("2026-09-28T00:00:00Z"),
createdAt: new Date("2026-08-29T00:00:00Z"),
updatedAt: new Date("2026-08-29T00:01:01Z"),
});
expect(job).toMatchObject({
status: "partial",
terminal: true,
pollAfterMs: 0,
nextAction: "read-results-and-inspect-failures",
});
});
});