Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5
This commit is contained in:
commit
5aa3e8d685
700 files changed
+132801
No files matched your search
@@ -0,0 +1,256 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const repositoryFinding = { findUnique: vi.fn(), findFirst: vi.fn(), findMany: vi.fn(), update: vi.fn(), create: vi.fn() };
|
||||
const findingOccurrence = { upsert: vi.fn() };
|
||||
const giteaIssueLink = { findUnique: vi.fn(), update: vi.fn(), upsert: vi.fn() };
|
||||
const outboxEvent = { create: vi.fn() };
|
||||
const findingAutomationPolicy = { upsert: vi.fn() };
|
||||
const tx = { repositoryFinding, findingOccurrence, giteaIssueLink, outboxEvent };
|
||||
const transaction = vi.fn(async (operation: unknown) => typeof operation === "function" ? operation(tx) : Promise.all(operation as Promise<unknown>[]));
|
||||
const issueClient = { createIssueComment: vi.fn(), editIssue: vi.fn() };
|
||||
const analysisJobItem = { findMany: vi.fn() };
|
||||
const findingInvestigation = { findMany: vi.fn(), update: vi.fn() };
|
||||
return { repositoryFinding, findingOccurrence, giteaIssueLink, outboxEvent, findingAutomationPolicy, transaction, issueClient, analysisJobItem, findingInvestigation };
|
||||
});
|
||||
|
||||
vi.mock("../src/db/prisma.js", () => ({ prisma: {
|
||||
repositoryFinding: mocks.repositoryFinding,
|
||||
findingOccurrence: mocks.findingOccurrence,
|
||||
giteaIssueLink: mocks.giteaIssueLink,
|
||||
outboxEvent: mocks.outboxEvent,
|
||||
findingAutomationPolicy: mocks.findingAutomationPolicy,
|
||||
$transaction: mocks.transaction,
|
||||
analysisJobItem: mocks.analysisJobItem,
|
||||
findingInvestigation: mocks.findingInvestigation,
|
||||
} }));
|
||||
vi.mock("../src/integrations/gitea/service.js", () => ({ getGiteaIssueClient: () => mocks.issueClient }));
|
||||
|
||||
import { loadEnv } from "../src/config/env.js";
|
||||
import { closeAutomaticallyResolvedFindingIssues, finalizeFindingVerificationForJob, reconcileFindingIssueWebhook, reconcileRepositoryFindings } from "../src/repositories/findingRepository.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: "read-token-with-at-least-twenty-characters",
|
||||
PROJECTBRAIN_GITEA_WRITE_TOKEN: "write-token-with-at-least-twenty-characters",
|
||||
});
|
||||
|
||||
const legacyFinding = {
|
||||
id: "finding-legacy",
|
||||
fingerprint: "legacy-fingerprint",
|
||||
projectId: "project-1",
|
||||
repositoryLinkId: "link-1",
|
||||
owner: "Acme",
|
||||
repository: "Repo",
|
||||
ruleCode: "LATEST_ACTION_FAILED",
|
||||
subject: "repository",
|
||||
title: "Legacy",
|
||||
message: "Legacy action failure",
|
||||
severity: "critical",
|
||||
confidence: 95,
|
||||
riskScore: 90,
|
||||
status: "issue_open",
|
||||
occurrenceCount: 10,
|
||||
clearCount: 0,
|
||||
firstSeenAt: new Date("2026-08-20T00:00:00Z"),
|
||||
lastSeenAt: new Date("2026-08-27T00:00:00Z"),
|
||||
resolvedAt: null,
|
||||
suppressedAt: null,
|
||||
suppressedUntil: null,
|
||||
suppressionReason: null,
|
||||
lastAnalysisJobId: "job-old",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.findingOccurrence.upsert.mockResolvedValue({ id: "occurrence-1" });
|
||||
mocks.giteaIssueLink.upsert.mockResolvedValue({ id: "issue-link-1" });
|
||||
mocks.outboxEvent.create.mockResolvedValue({ id: "outbox-1" });
|
||||
});
|
||||
|
||||
describe("finding reconciliation lifecycle", () => {
|
||||
it.each(["DEFAULT_BRANCH_ACTION_FAILED", "DEFAULT_BRANCH_UNPROTECTED", "OPEN_PR_BACKLOG", "TOPIC_BRANCH_ACTION_FAILED"])("does not clear an unevaluated %s finding", async (ruleCode) => {
|
||||
mocks.repositoryFinding.findMany.mockResolvedValue([{ ...legacyFinding, ruleCode, clearCount: 1 }]);
|
||||
for (const evaluatedRuleCodes of [undefined, ["REPOSITORY_ARCHIVED"]]) {
|
||||
await reconcileRepositoryFindings({ jobId: "partial-job", analysisJobItemId: "partial-item",
|
||||
owner: "Acme", repository: "Repo", defaultBranch: "main", findings: [], evaluatedRuleCodes });
|
||||
}
|
||||
expect(mocks.repositoryFinding.update).not.toHaveBeenCalled();
|
||||
expect(mocks.findingOccurrence.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still resolves after two distinct explicitly evaluated clean scans", async () => {
|
||||
const current = { ...legacyFinding, ruleCode: "DEFAULT_BRANCH_ACTION_FAILED" };
|
||||
mocks.repositoryFinding.findMany.mockImplementation(async () => [current]);
|
||||
mocks.repositoryFinding.update.mockImplementation(async ({ data }) => Object.assign(current, data));
|
||||
for (const jobId of ["clean-one", "clean-two"]) {
|
||||
await reconcileRepositoryFindings({ jobId, analysisJobItemId: jobId, owner: "Acme", repository: "Repo",
|
||||
defaultBranch: "main", findings: [], evaluatedRuleCodes: [current.ruleCode] });
|
||||
expect(current.clearCount).toBe(jobId === "clean-one" ? 1 : 2);
|
||||
expect(current.status).toBe(jobId === "clean-one" ? "issue_open" : "resolved");
|
||||
}
|
||||
expect(mocks.findingOccurrence.upsert).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it.each(["failed", "pending", "running", "cancelled"])("never verifies a %s analysis item even with a clean occurrence", async (status) => {
|
||||
mocks.analysisJobItem.findMany.mockResolvedValue([{ status, findingOccurrences: [{ findingId: "finding-legacy", observed: false }] }]);
|
||||
expect(await finalizeFindingVerificationForJob(env, "job")).toEqual([]);
|
||||
expect(mocks.findingInvestigation.findMany).not.toHaveBeenCalled();
|
||||
expect(mocks.repositoryFinding.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([false, true])("requires an explicit clean occurrence for the exact finding (present: %s)", async (present) => {
|
||||
mocks.analysisJobItem.findMany.mockResolvedValue([{ status: "succeeded", owner: "Acme", repository: "Repo",
|
||||
findingOccurrences: [{ findingId: present ? "finding-legacy" : "other-finding", observed: false }] }]);
|
||||
mocks.findingInvestigation.findMany.mockResolvedValue([{ id: "investigation", findingId: "finding-legacy",
|
||||
finding: { projectId: null, issueLink: null }, deliveryPlan: null }]);
|
||||
mocks.repositoryFinding.update.mockResolvedValue({});
|
||||
const result = await finalizeFindingVerificationForJob(env, "job");
|
||||
expect(result).toEqual(present ? [{ findingId: "finding-legacy", resolved: true }] : []);
|
||||
expect(mocks.repositoryFinding.update).toHaveBeenCalledTimes(present ? 1 : 0);
|
||||
});
|
||||
it("migrates an active legacy critical in place and preserves its history and issue identity", async () => {
|
||||
mocks.repositoryFinding.findUnique.mockResolvedValueOnce(null);
|
||||
mocks.repositoryFinding.findFirst.mockResolvedValueOnce(legacyFinding);
|
||||
mocks.repositoryFinding.update
|
||||
.mockImplementationOnce(async ({ data }: { data: Record<string, unknown> }) => ({ ...legacyFinding, ...data }))
|
||||
.mockImplementationOnce(async ({ data }: { data: Record<string, unknown> }) => ({ ...legacyFinding, ...data, id: legacyFinding.id }));
|
||||
mocks.repositoryFinding.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const reconciled = await reconcileRepositoryFindings({
|
||||
jobId: "job-new",
|
||||
analysisJobItemId: "item-new",
|
||||
projectId: "project-1",
|
||||
repositoryLinkId: "link-1",
|
||||
owner: "Acme",
|
||||
repository: "Repo",
|
||||
defaultBranch: "main",
|
||||
findings: [{
|
||||
severity: "critical",
|
||||
code: "DEFAULT_BRANCH_ACTION_FAILED",
|
||||
subject: "default:ci.yml",
|
||||
message: "CI failed",
|
||||
evidence: { scope: "default_branch", runUrl: "https://gitea.example/actions/runs/7", consecutiveFailures: 2 },
|
||||
}],
|
||||
});
|
||||
|
||||
expect(reconciled[0].id).toBe("finding-legacy");
|
||||
expect(mocks.repositoryFinding.update.mock.calls[0][0]).toMatchObject({
|
||||
where: { id: "finding-legacy" },
|
||||
data: { ruleCode: "DEFAULT_BRANCH_ACTION_FAILED", subject: "default:ci.yml", status: "triaged" },
|
||||
});
|
||||
expect(mocks.repositoryFinding.update.mock.calls[1][0]).toMatchObject({
|
||||
where: { id: "finding-legacy" },
|
||||
data: { occurrenceCount: { increment: 1 }, lastAnalysisJobId: "job-new" },
|
||||
});
|
||||
expect(mocks.findingOccurrence.upsert).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { findingId_analysisJobItemId: { findingId: "finding-legacy", analysisJobItemId: "item-new" } },
|
||||
}));
|
||||
});
|
||||
|
||||
it("does not increment occurrence or clear counters twice when an item is retried in the same job", async () => {
|
||||
const current = { ...legacyFinding, fingerprint: "current", ruleCode: "DEFAULT_BRANCH_ACTION_FAILED", subject: "default:ci.yml", lastAnalysisJobId: "job-current" };
|
||||
mocks.repositoryFinding.findUnique.mockResolvedValueOnce(current);
|
||||
mocks.repositoryFinding.update.mockImplementationOnce(async ({ data }: { data: Record<string, unknown> }) => ({ ...current, ...data }));
|
||||
mocks.repositoryFinding.findMany.mockResolvedValueOnce([{ ...legacyFinding, id: "missing", lastAnalysisJobId: "job-current" }]);
|
||||
|
||||
await reconcileRepositoryFindings({
|
||||
jobId: "job-current",
|
||||
analysisJobItemId: "item-current",
|
||||
owner: "Acme",
|
||||
repository: "Repo",
|
||||
defaultBranch: "main",
|
||||
findings: [{ severity: "critical", code: "DEFAULT_BRANCH_ACTION_FAILED", subject: "default:ci.yml", message: "CI failed" }],
|
||||
});
|
||||
|
||||
expect(mocks.repositoryFinding.update).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.repositoryFinding.update.mock.calls[0][0].data.occurrenceCount).toBe(10);
|
||||
});
|
||||
|
||||
it("closes only resolved linked issues whose project policy enables automatic closure", async () => {
|
||||
const resolved = {
|
||||
...legacyFinding,
|
||||
status: "resolved",
|
||||
resolvedAt: new Date(),
|
||||
issueLink: { id: "issue-link-1", state: "open", issueNumber: 7 },
|
||||
project: { findingAutomationPolicy: { autoCloseResolved: true } },
|
||||
};
|
||||
const detailed = {
|
||||
...resolved,
|
||||
fingerprint: "resolved-fingerprint",
|
||||
repositoryLink: { accessMode: "read_write" },
|
||||
investigations: [],
|
||||
occurrences: [{ observed: true, detailJson: { evidence: { runUrl: "https://gitea.example/actions/runs/7" } }, createdAt: new Date() }],
|
||||
};
|
||||
mocks.repositoryFinding.findMany.mockResolvedValueOnce([resolved]);
|
||||
mocks.repositoryFinding.findUnique.mockResolvedValue(detailed);
|
||||
mocks.repositoryFinding.update.mockResolvedValue(detailed);
|
||||
mocks.issueClient.createIssueComment.mockResolvedValue({ id: 1 });
|
||||
mocks.issueClient.editIssue.mockResolvedValue({ id: 7, number: 7, title: "Finding", body: "body", state: "closed", html_url: "https://gitea.example/issues/7", labels: [], created_at: "2026-08-20T00:00:00Z", updated_at: "2026-08-28T00:00:00Z", closed_at: "2026-08-28T00:00:00Z" });
|
||||
|
||||
await expect(closeAutomaticallyResolvedFindingIssues(env)).resolves.toEqual(["finding-legacy"]);
|
||||
expect(mocks.issueClient.createIssueComment).toHaveBeenCalledOnce();
|
||||
expect(mocks.issueClient.editIssue).toHaveBeenCalledWith("Acme", "Repo", 7, { state: "closed" });
|
||||
|
||||
vi.clearAllMocks();
|
||||
mocks.repositoryFinding.findMany.mockResolvedValueOnce([{ ...resolved, project: { findingAutomationPolicy: { autoCloseResolved: false } } }]);
|
||||
await expect(closeAutomaticallyResolvedFindingIssues(env)).resolves.toEqual([]);
|
||||
expect(mocks.issueClient.editIssue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps an automatically resolved finding resolved when the issue-close webhook arrives later", async () => {
|
||||
mocks.giteaIssueLink.findUnique.mockResolvedValueOnce({
|
||||
id: "issue-link-1",
|
||||
findingId: "finding-legacy",
|
||||
finding: { ...legacyFinding, status: "suppressed", resolvedAt: new Date("2026-08-28T00:00:00Z") },
|
||||
});
|
||||
mocks.giteaIssueLink.update.mockResolvedValueOnce({ id: "issue-link-1", state: "closed" });
|
||||
mocks.repositoryFinding.update.mockResolvedValueOnce({ ...legacyFinding, status: "resolved", projectId: "project-1" });
|
||||
|
||||
await reconcileFindingIssueWebhook({
|
||||
owner: "Acme",
|
||||
repository: "Repo",
|
||||
issueNumber: 7,
|
||||
state: "closed",
|
||||
title: "Finding",
|
||||
url: "https://gitea.example/issues/7",
|
||||
actor: "projectbrain",
|
||||
});
|
||||
|
||||
expect(mocks.repositoryFinding.update).toHaveBeenCalledWith({
|
||||
where: { id: "finding-legacy" },
|
||||
data: { status: "resolved", suppressedAt: null, suppressedUntil: null, suppressionReason: null },
|
||||
});
|
||||
});
|
||||
|
||||
it("still suppresses a genuinely active finding when its issue is manually closed", async () => {
|
||||
mocks.giteaIssueLink.findUnique.mockResolvedValueOnce({
|
||||
id: "issue-link-1",
|
||||
findingId: "finding-legacy",
|
||||
finding: { ...legacyFinding, status: "issue_open", resolvedAt: null },
|
||||
});
|
||||
mocks.giteaIssueLink.update.mockResolvedValueOnce({ id: "issue-link-1", state: "closed" });
|
||||
mocks.repositoryFinding.update.mockResolvedValueOnce({ ...legacyFinding, status: "suppressed", projectId: "project-1" });
|
||||
|
||||
await reconcileFindingIssueWebhook({
|
||||
owner: "Acme",
|
||||
repository: "Repo",
|
||||
issueNumber: 7,
|
||||
state: "closed",
|
||||
title: "Finding",
|
||||
url: "https://gitea.example/issues/7",
|
||||
actor: "jens",
|
||||
});
|
||||
|
||||
expect(mocks.repositoryFinding.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
where: { id: "finding-legacy" },
|
||||
data: expect.objectContaining({ status: "suppressed", suppressionReason: "Gitea issue handmatig gesloten door jens" }),
|
||||
}));
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user