148 lines
5.7 KiB
TypeScript
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",
|
|
});
|
|
});
|
|
});
|