Initial curated projectbrain source from 8a7b1e0651dbdab97d744cbd2e653ab3970ac6c5

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:43:26 +02:00
commit 5aa3e8d685
700 files changed
+132801

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
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";
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: "a-token-with-at-least-twenty-characters",
});
const job = (input: Partial<GiteaActionJob> = {}): GiteaActionJob => ({
id: 42,
html_url: "https://gitea.example.com/Jens/projectbrain/actions/runs/21/jobs/42",
run_id: 21,
name: "quality",
labels: ["ubuntu-latest"],
run_attempt: 1,
head_sha: "abc123",
head_branch: "main",
status: "in_progress",
runner_id: 9,
runner_name: "itworx-security-runner",
steps: [
{ name: "Checkout", number: 1, status: "completed", conclusion: "success" },
{ name: "Test", number: 2, status: "in_progress" },
],
created_at: "2026-08-28T19:00:00Z",
started_at: "2026-08-28T19:01:00Z",
completed_at: "1970-01-01T00:00:00Z",
...input,
});
describe("compact activity runtime", () => {
it("keeps older running jobs visible ahead of a full page of newer queued jobs", () => {
const jobs = [job({ id: 1 }), job({ id: 2 }), ...Array.from({ length: 30 }, (_, i) => job({ id: i + 100, status: "queued" }))];
const snapshot = compactGiteaRuntime([], jobs, "2026-09-10T00:00:00Z");
expect(snapshot.jobs.slice(0, 12).slice(0, 2).map((item) => item.id)).toEqual([2, 1]);
expect(snapshot.counts).toEqual({ running: 2, runnable: 30, dependencyBlocked: 0 });
expect(snapshot.jobs).toHaveLength(32);
});
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();
});
it("returns only compact runner and current-step data", () => {
const snapshot = compactGiteaRuntime([
{ id: 9, name: "runner", status: "online", busy: true, disabled: false, ephemeral: false, labels: [{ id: 1, name: "ubuntu-latest", type: "custom" }] },
], [job()], "2026-08-28T19:02:00Z");
expect(snapshot.runners[0]).toEqual({ id: 9, name: "runner", status: "online", busy: true, disabled: false, labels: ["ubuntu-latest"] });
expect(snapshot.jobs[0]).toMatchObject({ repository: "Jens/projectbrain", currentStep: "Test", queueState: "running", completedSteps: 1, totalSteps: 2 });
expect(snapshot.jobs[0].completedAt).toBeUndefined();
expect(JSON.stringify(snapshot)).not.toContain("head_sha");
expect(compactGiteaRuntime([], [job()], "2026-08-28T19:03:00Z").stateVersion)
.toBe(compactGiteaRuntime([], [job()], "2026-08-28T19:04:00Z").stateVersion);
});
it("derives a busy runner from an active job when user-level runner discovery omits it", () => {
const snapshot = compactGiteaRuntime([], [job()], "2026-08-28T19:02:00Z");
expect(snapshot.runners).toEqual([{ id: 9, name: "itworx-security-runner", status: "observed", busy: true, disabled: false, labels: ["ubuntu-latest"] }]);
});
it("does not present dependency-blocked jobs as runnable work", () => {
const snapshot = compactGiteaRuntime([], [job({ status: "waiting", runner_id: 0, runner_name: undefined })], "2026-08-28T19:02:00Z");
expect(snapshot.jobs[0].queueState).toBe("dependency-blocked");
expect(snapshot.counts).toEqual({ running: 0, runnable: 0, dependencyBlocked: 1 });
});
it("keeps every active job visible and bounds only completed history", () => {
const completed = Array.from({ length: 35 }, (_, index) => job({ id: 100 + index, status: "completed", conclusion: "success" }));
const active = Array.from({ length: 38 }, (_, index) => job({ id: 10 + index, status: index < 3 ? "in_progress" : "queued", runner_id: index < 3 ? 9 : 0, runner_name: index < 3 ? "runner" : undefined }));
const snapshot = compactGiteaRuntime([], [...completed, ...active], "2026-08-28T19:02:00Z");
expect(snapshot.jobs).toHaveLength(48);
expect(snapshot.jobs.filter((item) => item.queueState !== "completed")).toHaveLength(38);
expect(snapshot.jobs.filter((item) => item.queueState === "completed")).toHaveLength(10);
});
it("coalesces simultaneous refreshes and serves a stale snapshot after a transient failure", async () => {
let clock = new Date("2026-08-28T19:00:00Z");
const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 1, runners: [
{ id: 9, name: "runner", status: "online", busy: true, disabled: false, ephemeral: false, labels: [] },
] });
const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => input.status === "in_progress"
? { total_count: 1, jobs: [job()] }
: input.status === undefined ? { total_count: 1, jobs: [job()] } : { total_count: 0, jobs: [] });
const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs }, now: () => clock });
const [first, simultaneous] = await Promise.all([load(), load()]);
expect(first.stateVersion).toBe(simultaneous.stateVersion);
expect(listUserActionRunners).toHaveBeenCalledTimes(1);
expect(listUserActionJobs).toHaveBeenCalledTimes(4);
expect(first.counts).toEqual({ running: 1, runnable: 0, dependencyBlocked: 0 });
clock = new Date("2026-08-28T19:00:16Z");
listUserActionRunners.mockRejectedValue(new Error("temporary upstream failure"));
listUserActionJobs.mockRejectedValue(new Error("temporary upstream failure"));
const stale = await load();
expect(stale).toMatchObject({ availability: "stale", warning: "temporary upstream failure" });
expect(stale.jobs).toHaveLength(1);
});
it("keeps a live snapshot and derives counts when one filtered query fails", async () => {
const waitingJob = job({ id: 43, status: "waiting", runner_id: 0, runner_name: undefined });
const listUserActionRunners = vi.fn().mockResolvedValue({ total_count: 0, runners: [] });
const listUserActionJobs = vi.fn().mockImplementation(async (input: { status?: string } = {}) => {
if (input.status === "pending") throw new Error("unsupported status filter");
if (input.status === undefined) return { total_count: 1, jobs: [waitingJob] };
return { total_count: 0, jobs: [] };
});
const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } });
const snapshot = await load();
expect(snapshot.availability).toBe("live");
expect(snapshot.warning).toContain("Partial Gitea monitoring failure");
expect(snapshot.counts).toEqual({ running: 0, runnable: 0, dependencyBlocked: 1 });
expect(snapshot.jobs[0].queueState).toBe("dependency-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: [] });
const load = createGiteaActivityRuntimeLoader(env, { client: { listUserActionRunners, listUserActionJobs } });
await load();
expect(listUserActionJobs).toHaveBeenCalledWith({ status: "pending", limit: 50 });
expect(listUserActionJobs).not.toHaveBeenCalledWith({ status: "waiting", limit: 50 });
});
});
@@ -0,0 +1,93 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { loadEnv } from "../src/config/env.js";
import { createPortfolioAnalysisJob } from "../src/repositories/analysisJobRepository.js";
import { refreshPortfolioCatalog } from "../src/services/portfolioControlPlane.js";
import { registerAnalysisJobRoutes } from "../src/routes/analysisJobs.js";
import Fastify from "fastify";
const mocks = vi.hoisted(() => ({
inventory: vi.fn(), links: vi.fn(), findJob: vi.fn(), findFirstJob: vi.fn(), createJob: vi.fn(),
catalog: vi.fn(), upsert: vi.fn(), unavailable: vi.fn(),
}));
vi.mock("../src/integrations/gitea/service.js", () => ({
getGiteaClient: () => ({ listRepositories: mocks.inventory }),
listLinkedRepositories: mocks.links,
}));
vi.mock("../src/db/prisma.js", () => ({ prisma: {
analysisJob: { findUnique: mocks.findJob, findFirst: mocks.findFirstJob, create: mocks.createJob },
portfolioRepository: { findMany: mocks.catalog, upsert: mocks.upsert, updateMany: mocks.unavailable },
} }));
const env = loadEnv({ NODE_ENV: "test", PROJECTBRAIN_DATA_SOURCE: "database",
DATABASE_URL: "postgresql://fixture:fixture@localhost:5432/fixture",
PROJECTBRAIN_OWNER_USERNAME: "fixture-owner", PROJECTBRAIN_OWNER_PASSWORD: "fixture-only-password",
PROJECTBRAIN_AUTH_SECRET: "fixture-only-auth-secret-at-least-32-characters" });
const repositories = (count: number) => Array.from({ length: count }, (_, index) => ({
id: index + 1, name: `repo-${index}`, full_name: `Owner/repo-${index}`,
owner: { login: "Owner" }, default_branch: "main", permissions: { pull: true },
}));
beforeEach(() => {
vi.clearAllMocks();
mocks.links.mockResolvedValue([]);
mocks.catalog.mockResolvedValue([]);
mocks.findJob.mockResolvedValue(null);
mocks.findFirstJob.mockResolvedValue(null);
mocks.createJob.mockImplementation(async ({ data }) => ({
id: "fixture-job", status: "queued", progressTotal: data.progressTotal, progressCurrent: 0,
}));
});
describe("bounded complete portfolio analysis", () => {
it.each([58, 200, 201])("enforces the same %i-selection boundary at the project HTTP route", async (count) => {
mocks.links.mockResolvedValue(repositories(count).map((repository) => ({
id: String(repository.id), projectId: "fixture-project", projectSlug: "fixture-project",
projectName: "Fixture", owner: "Owner", repository: repository.name,
fullName: repository.full_name, defaultBranch: "main", accessMode: "read_only",
})));
const app = Fastify();
app.decorateRequest("authUser", null);
app.addHook("onRequest", async (request) => {
request.authUser = { id: "fixture-owner", username: "fixture-owner", role: "owner", credentialVersion: 1 };
});
await registerAnalysisJobRoutes(app, { env });
try {
const response = await app.inject({ method: "POST", url: "/api/projects/fixture-project/analysis-jobs",
payload: { repositoryIds: Array.from({ length: count }, (_, index) => String(index + 1)) } });
if (count <= 200) {
expect(response.statusCode).toBe(202);
expect(response.json().data.job.progressTotal).toBe(count);
expect(mocks.createJob.mock.calls[0][0].data.items.create).toHaveLength(count);
} else {
expect(response.statusCode).toBeGreaterThanOrEqual(400);
expect(mocks.createJob).not.toHaveBeenCalled();
}
} finally {
await app.close();
}
});
it.each([51, 58, 200])("persists all %i targets without truncation", async (count) => {
mocks.inventory.mockResolvedValue(repositories(count));
const result = await createPortfolioAnalysisJob(env, "fixture-actor", {});
expect(result.job.progressTotal).toBe(count);
const data = mocks.createJob.mock.calls[0][0].data;
expect(data.items.create).toHaveLength(count);
expect(data.requestJson.repositoryIds).toHaveLength(count);
expect(new Set(data.items.create.map((item: { sequence: number }) => item.sequence)).size).toBe(count);
});
it("rejects 201 targets before job persistence", async () => {
mocks.inventory.mockResolvedValue(repositories(201));
await expect(createPortfolioAnalysisJob(env, "fixture-actor", {})).rejects.toThrow("ANALYSIS_REPOSITORY_LIMIT_EXCEEDED");
expect(mocks.createJob).not.toHaveBeenCalled();
});
it("does not persist a job or mark cached repositories unavailable after incomplete inventory", async () => {
mocks.inventory.mockRejectedValue(new Error("GITEA_INVENTORY_LIMIT_EXCEEDED"));
await expect(createPortfolioAnalysisJob(env, "fixture-actor", {})).rejects.toThrow("GITEA_INVENTORY_LIMIT_EXCEEDED");
await expect(refreshPortfolioCatalog(env)).rejects.toThrow("GITEA_INVENTORY_LIMIT_EXCEEDED");
expect(mocks.createJob).not.toHaveBeenCalled();
expect(mocks.upsert).not.toHaveBeenCalled();
expect(mocks.unavailable).not.toHaveBeenCalled();
});
});
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { chooseAnalysisStrategy, findingFingerprint, groupFindingRootCauses } from "../src/services/analysisStrategy.js";
describe("analysis strategy", () => {
it("prioritizes changed-area analysis inside the budget", () => {
expect(chooseAnalysisStrategy({ changedPaths: ["src/a.ts", "src/b.ts"], now: 100, lastFullScanAt: new Date(0).toISOString() })).toMatchObject({ strategy: "diff", estimatedScope: "changed-area" });
});
it("uses targeted analysis while a full scan is fresh", () => {
expect(chooseAnalysisStrategy({ now: 100_000, lastFullScanAt: new Date(99_000).toISOString(), activeFindingCount: 2, staleAfterMs: 10_000 })).toMatchObject({ strategy: "targeted" });
expect(chooseAnalysisStrategy({ now: 100_000, lastFullScanAt: new Date(0).toISOString(), staleAfterMs: 10_000 }).strategy).toBe("full");
});
it("normalizes fingerprints and groups repeated root causes", () => {
expect(findingFingerprint({ ruleCode: "CI_FAILED", path: "src\\a123.ts", subject: "run 42" })).toBe("ci_failed|src/a#.ts|run #");
expect(groupFindingRootCauses([{ fingerprint: "a", ruleCode: "CI", subject: "workflow", rootCause: "runner" }, { fingerprint: "b", ruleCode: "CI", subject: "workflow", rootCause: "runner" }])).toEqual([{ rootCause: "runner", fingerprints: ["a", "b"] }]);
});
});
+187
View File
@@ -0,0 +1,187 @@
import { afterEach, describe, expect, it } from "vitest";
import { buildApp } from "../src/app.js";
import { loadEnv } from "../src/config/env.js";
import { canAccessProject } from "../src/security/authStore.js";
import { parseAutonomyClaimInput } from "../src/routes/autonomy.js";
import { isOwnerRequest, parseWorkGraphLeaseInput } from "../src/routes/operatingSystem.js";
import { hashPassword, verifyPassword } from "../src/security/password.js";
import { apiPaths, authSessionDataSchema, successEnvelopeSchema } from "@projectbrain/contracts";
import { PassThrough } from "node:stream";
const ownerPassword = "correct-horse-battery-staple";
const env = () => loadEnv({
NODE_ENV: "test",
LOG_LEVEL: "silent",
PROJECTBRAIN_DATA_SOURCE: "fixture",
PROJECTBRAIN_OWNER_USERNAME: "owner",
PROJECTBRAIN_OWNER_PASSWORD: ownerPassword,
PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters",
PROJECTBRAIN_SESSION_TTL_HOURS: "1",
PROJECTBRAIN_TRUST_PROXY: "false",
});
const apps: Awaited<ReturnType<typeof buildApp>>[] = [];
const makeApp = async () => {
const app = await buildApp({ env: env() });
apps.push(app);
return app;
};
afterEach(async () => {
await Promise.all(apps.splice(0).map((app) => app.close()));
});
const login = async (app: Awaited<ReturnType<typeof buildApp>>) => {
const response = await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "owner", password: ownerPassword } });
const cookie = response.cookies.find((item) => item.name === "projectbrain_session");
return { response, cookie: cookie ? `${cookie.name}=${cookie.value}` : "", csrfToken: response.json().data?.csrfToken as string };
};
describe("authentication boundary", () => {
it("rejects known insecure production bootstrap credentials", () => {
expect(() => loadEnv({ NODE_ENV: "production", DATABASE_URL: "postgresql://example.invalid/projectbrain", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: "adminadminadmin", PROJECTBRAIN_AUTH_SECRET: "production-secret-with-at-least-thirty-two-characters" })).toThrow(/insecure default/);
});
it("requires a complete, database-backed Entra configuration", () => {
const base = { NODE_ENV: "test", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters" };
expect(() => loadEnv({ ...base, PROJECTBRAIN_ENTRA_TENANT_ID: "11111111-1111-4111-8111-111111111111" })).toThrow(/must be configured together/);
expect(() => loadEnv({ ...base, PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com", PROJECTBRAIN_ENTRA_TENANT_ID: "11111111-1111-4111-8111-111111111111", PROJECTBRAIN_ENTRA_CLIENT_ID: "22222222-2222-4222-8222-222222222222", PROJECTBRAIN_ENTRA_CLIENT_SECRET: "a-test-secret-value" })).toThrow(/database/);
const blank = loadEnv({ ...base, PROJECTBRAIN_ENTRA_TENANT_ID: "", PROJECTBRAIN_ENTRA_CLIENT_ID: "", PROJECTBRAIN_ENTRA_CLIENT_SECRET: "" });
expect(blank.PROJECTBRAIN_ENTRA_TENANT_ID).toBeUndefined();
});
it("marks the production session cookie Secure", async () => {
const productionEnv = loadEnv({ NODE_ENV: "production", DATABASE_URL: "postgresql://example.invalid/projectbrain", LOG_LEVEL: "silent", PROJECTBRAIN_DATA_SOURCE: "fixture", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "production-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_SESSION_TTL_HOURS: "1", PROJECTBRAIN_TRUST_PROXY: "false" });
const app = await buildApp({ env: productionEnv });
apps.push(app);
const response = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: ownerPassword } });
expect(response.headers["set-cookie"]).toContain("Secure");
}, 10_000);
it("hashes passwords with a random salt and rejects malformed hashes", async () => {
const first = await hashPassword(ownerPassword);
const second = await hashPassword(ownerPassword);
expect(first).not.toBe(second);
expect(await verifyPassword(ownerPassword, first)).toBe(true);
expect(await verifyPassword("incorrect-password-value", first)).toBe(false);
expect(await verifyPassword(ownerPassword, "not-a-supported-hash")).toBe(false);
});
it("denies fixture project access to a non-owner identity", async () => {
expect(await canAccessProject(env(), { id: "member-1", username: "member", role: "member", credentialVersion: 1 }, "widefrog", false)).toBe(false);
});
it("keeps portfolio-wide operating-system access owner-only", () => {
expect(isOwnerRequest({ authUser: { id: "owner-1", username: "owner", role: "owner", credentialVersion: 1 } } as never)).toBe(true);
expect(isOwnerRequest({ authUser: { id: "member-1", username: "member", role: "member", credentialVersion: 1 } } as never)).toBe(false);
});
it("keeps only deliberate health, readiness and login bootstrap routes public", async () => {
const app = await makeApp();
expect((await app.inject({ method: "GET", url: "/api/health" })).statusCode).toBe(200);
expect((await app.inject({ method: "GET", url: "/api/system/readiness" })).statusCode).toBe(200);
const metrics = await app.inject({ method: "GET", url: "/api/metrics" });
expect(metrics.statusCode).toBe(200);
expect(metrics.body).toContain("projectbrain_http_requests_total");
expect(metrics.body).toContain("projectbrain_database_ready 1");
const entraConfig = await app.inject({ method: "GET", url: apiPaths.auth.entraConfig });
expect(entraConfig.statusCode).toBe(200);
expect(entraConfig.json().data).toEqual({ enabled: false, provider: "microsoft-entra" });
expect((await app.inject({ method: "GET", url: apiPaths.auth.entraStart })).statusCode).toBe(503);
expect((await app.inject({ method: "POST", url: apiPaths.auth.entraLinkStart })).statusCode).toBe(401);
const protectedResponse = await app.inject({ method: "GET", url: "/api/projects" });
expect(protectedResponse.statusCode).toBe(401);
expect(protectedResponse.json().error.code).toBe("AUTHENTICATION_REQUIRED");
});
it("rejects unsafe incoming correlation IDs and emits a safe replacement", async () => {
const app = await makeApp();
const response = await app.inject({ method: "GET", url: "/api/health", headers: { "x-request-id": "unsafe\nrequest" } });
expect(response.headers["x-request-id"]).toMatch(/^req-[0-9a-f-]+$/);
});
it("redacts credentials and project content from standard logs", async () => {
const chunks: string[] = [];
const logStream = new PassThrough();
logStream.on("data", (chunk) => chunks.push(chunk.toString()));
const loggedEnv = { ...env(), LOG_LEVEL: "info" as const };
const app = await buildApp({ env: loggedEnv, logStream });
apps.push(app);
await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "owner", password: "secret-project-content" } });
await new Promise((resolve) => setImmediate(resolve));
const output = chunks.join("");
expect(output).not.toContain("secret-project-content");
expect(output).not.toContain(ownerPassword);
});
it("creates a secure server-side session and exposes no password", async () => {
const app = await makeApp();
const { response, cookie, csrfToken } = await login(app);
expect(response.statusCode).toBe(200);
expect(cookie).toContain("projectbrain_session=");
expect(response.headers["set-cookie"]).toContain("HttpOnly");
expect(response.headers["set-cookie"]).toContain("SameSite=Lax");
expect(csrfToken.length).toBeGreaterThan(20);
expect(JSON.stringify(response.json())).not.toContain(ownerPassword);
expect(successEnvelopeSchema(authSessionDataSchema).safeParse(response.json()).success).toBe(true);
const session = await app.inject({ method: "GET", url: apiPaths.auth.session, headers: { cookie } });
expect(session.statusCode).toBe(200);
expect(session.headers["set-cookie"]).toContain("Max-Age=3600");
expect(session.json().data.user.username).toBe("owner");
expect(successEnvelopeSchema(authSessionDataSchema).safeParse(session.json()).success).toBe(true);
});
it("rejects invalid credentials and enforces login rate limiting", async () => {
const app = await makeApp();
const attempts = [];
for (let index = 0; index < 6; index += 1) attempts.push(await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: "incorrect-password-value" } }));
expect(attempts[0].statusCode).toBe(401);
expect(attempts[5].statusCode).toBe(429);
});
it("rejects malformed login and an incorrect current password", async () => {
const app = await makeApp();
expect((await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "x", password: "short" } })).statusCode).toBe(400);
const { cookie, csrfToken } = await login(app);
const response = await app.inject({ method: "POST", url: "/api/auth/rotate-password", headers: { cookie, "x-csrf-token": csrfToken }, payload: { currentPassword: "incorrect-password-value", newPassword: "a-different-secure-owner-password" } });
expect(response.statusCode).toBe(403);
});
it("requires CSRF for mutations and accepts the session token", async () => {
const app = await makeApp();
const { cookie, csrfToken } = await login(app);
const missing = await app.inject({ method: "POST", url: "/api/projects", headers: { cookie }, payload: { name: "Secure Project", description: "A securely created project." } });
expect(missing.statusCode).toBe(403);
expect(missing.json().error.code).toBe("CSRF_TOKEN_INVALID");
const accepted = await app.inject({ method: "POST", url: "/api/projects", headers: { cookie, "x-csrf-token": csrfToken }, payload: { name: "Secure Project", description: "A securely created project." } });
expect(accepted.statusCode).toBe(201);
});
it("does not accept a caller-supplied actor for durable work-graph commands", async () => {
expect(parseWorkGraphLeaseInput({ actor: "system:owner", leaseMinutes: 10 })).toEqual({ leaseMinutes: 10 });
expect(parseAutonomyClaimInput({ actor: "system:owner", runId: "scheduled-run" })).toEqual({ runId: "scheduled-run" });
});
it("revokes a session on logout", async () => {
const app = await makeApp();
const { cookie, csrfToken } = await login(app);
expect((await app.inject({ method: "POST", url: "/api/auth/logout", headers: { cookie, "x-csrf-token": csrfToken } })).statusCode).toBe(200);
expect((await app.inject({ method: "GET", url: "/api/projects", headers: { cookie } })).statusCode).toBe(401);
});
it("rotates credentials and revokes every existing session", async () => {
const app = await makeApp();
const { cookie, csrfToken } = await login(app);
const rotated = await app.inject({ method: "POST", url: "/api/auth/rotate-password", headers: { cookie, "x-csrf-token": csrfToken }, payload: { currentPassword: ownerPassword, newPassword: "a-different-secure-owner-password" } });
expect(rotated.statusCode).toBe(200);
expect((await app.inject({ method: "GET", url: "/api/projects", headers: { cookie } })).statusCode).toBe(401);
expect((await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: ownerPassword } })).statusCode).toBe(401);
expect((await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "owner", password: "a-different-secure-owner-password" } })).statusCode).toBe(200);
});
it("emits baseline security headers", async () => {
const app = await makeApp();
const response = await app.inject({ method: "GET", url: "/api/health" });
expect(response.headers["content-security-policy"]).toContain("default-src 'self'");
expect(response.headers["x-content-type-options"]).toBe("nosniff");
expect(response.headers["x-frame-options"]).toBe("SAMEORIGIN");
});
});
+331
View File
@@ -0,0 +1,331 @@
import { describe, expect, it } from "vitest";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { loadEnv } from "../src/config/env.js";
import { compareAutoModeProjectRows, createProjectBrainMcpServer } from "../src/mcp/autonomyServer.js";
import { autonomyRetryDelayMinutes, canResumeAutonomyLease, classifyAutonomyBlocker, enqueueAutonomyTaskInState, evaluateAutonomyRunBudget, isAutonomyLeaseOwner, normalizeAutonomyState, parseRoadmapMarkdown, releaseActiveAutonomyLease, retryAutonomyBlockerIfDue } from "../src/repositories/autonomyRepository.js";
import { autonomyHeartbeatSlotId, compactAutonomyState, resolveAutonomyHeartbeat, summarizeAutonomyProject } from "../src/services/autonomyRuntime.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_MCP_ENABLED: "true",
PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com",
PROJECTBRAIN_GITEA_URL: "https://gitea.example.com",
PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters",
});
describe("Auto Mode roadmap parser", () => {
it("turns markdown phases and list items into ordered durable work", () => {
const roadmap = parseRoadmapMarkdown(`# Product roadmap
## Fundament
Maak de deliverylaag hervatbaar.
- [x] Bestaande status importeren
- [ ] Leasecontract toevoegen
## Product
Voltooi de gebruikersflow.
1. Auto toggle bouwen
2. Voortgang tonen`);
expect(roadmap.title).toBe("Product roadmap");
expect(roadmap.phases).toHaveLength(2);
expect(roadmap.phases[0]).toMatchObject({ title: "Fundament", objective: "Maak de deliverylaag hervatbaar.", status: "active" });
expect(roadmap.phases[0].items.map((item) => [item.title, item.status])).toEqual([
["Bestaande status importeren", "completed"],
["Leasecontract toevoegen", "pending"],
]);
expect(roadmap.phases[1].status).toBe("pending");
expect(roadmap.phases[1].items.map((item) => item.title)).toEqual(["Auto toggle bouwen", "Voortgang tonen"]);
});
it("accepts a flat task list by creating one execution phase", () => {
const roadmap = parseRoadmapMarkdown("- Analyseer repo\n- Bouw oplossing\n- Valideer resultaat", "MCP automation");
expect(roadmap.title).toBe("MCP automation");
expect(roadmap.phases).toHaveLength(1);
expect(roadmap.phases[0].title).toBe("Execution");
expect(roadmap.phases[0].items).toHaveLength(3);
});
it("allows an empty phase so the brainstorm loop can discover its work", () => {
const roadmap = parseRoadmapMarkdown("# Roadmap\n\n## Quality convergence\nZoek autonoom naar resterende gaten.");
expect(roadmap.phases[0].items).toEqual([]);
expect(roadmap.phases[0].brainstormConverged).toBe(false);
expect(roadmap.phases[0].objective).toBe("Zoek autonoom naar resterende gaten.");
});
});
describe("Auto Mode state safety", () => {
it("keeps an hourly ChatGPT Chat executor fresh between scheduled runs", () => {
expect(env.PROJECTBRAIN_AUTONOMY_EXECUTOR_STALE_SECONDS).toBe(5_400);
});
it("stores a user assignment durably even while Auto Mode is off", () => {
const current = normalizeAutonomyState({}, "owner");
const queued = enqueueAutonomyTaskInState(current, { instruction: "Verbeter de mobiele navigatie\nmet regressietests." }, "owner");
expect(queued.state.status).toBe("off");
expect(queued.task).toMatchObject({
title: "Verbeter de mobiele navigatie",
instruction: "Verbeter de mobiele navigatie\nmet regressietests.",
source: "user_assignment",
status: "pending",
});
expect(queued.state.roadmap?.phases[0].items).toEqual([queued.task]);
expect(queued.state.events.at(-1)?.type).toBe("autonomy.task.enqueued");
});
it("prioritizes a new user assignment in the current claimable phase instead of a phase already passed", () => {
const roadmap = parseRoadmapMarkdown("## Eerder\n- [ ] Eerste opdracht\n\n## Huidig\n- [ ] Lopend werk");
roadmap.phases[0].id = "projectbrain-opdrachten-old";
roadmap.phases[0].status = "completed";
roadmap.phases[1].status = "active";
roadmap.currentPhaseIndex = 1;
const current = normalizeAutonomyState({ enabled: true, status: "running", roadmap }, "owner");
const queued = enqueueAutonomyTaskInState(current, { instruction: "Nieuwe opdracht" }, "owner");
expect(queued.state.status).toBe("running");
expect(queued.state.roadmap?.phases).toHaveLength(2);
expect(queued.state.roadmap?.phases[0].status).toBe("completed");
expect(queued.state.roadmap?.phases[1].items[0]).toEqual(queued.task);
expect(queued.state.roadmap?.phases[1].items[1].title).toBe("Lopend werk");
});
it("derives the public status from durable facts instead of stale stored copy", () => {
expect(normalizeAutonomyState({ enabled: true, status: "running" }).status).toBe("waiting_roadmap");
expect(normalizeAutonomyState({}).profile).toBe("normal");
expect(normalizeAutonomyState({ profile: "unsupported" }).profile).toBe("normal");
const disabled = normalizeAutonomyState({ enabled: false, status: "running", activeLease: { token: "old", kind: "implement", phaseId: "phase" } });
expect(disabled.status).toBe("off");
expect(disabled.activeLease).toBeUndefined();
});
it("releases in-progress work safely when Auto Mode is disabled", () => {
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] Werk");
roadmap.phases[0].status = "executing";
roadmap.phases[0].items[0].status = "in_progress";
const released = releaseActiveAutonomyLease({
version: 1,
profile: "normal",
enabled: true,
status: "running",
autoBrainstorm: true,
maxBrainstormIterations: 5,
maxConsecutiveFailures: 3,
leaseMinutes: 30,
maxWorkItemsPerRun: 8,
maxRunMinutes: 45,
runs: [],
roadmap,
activeLease: { token: "lease", kind: "implement", actor: "codex", roadmapId: roadmap.id, phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60_000).toISOString() },
completedLeases: [],
events: [],
updatedAt: new Date().toISOString(),
updatedBy: "owner",
}, "owner");
expect(released.activeLease).toBeUndefined();
expect(released.roadmap?.phases[0]).toMatchObject({ status: "active", items: [expect.objectContaining({ status: "pending" })] });
expect(released.events.at(-1)?.type).toBe("autonomy.lease.released");
});
it("separates claimable work from a fresh or expired executor lease", () => {
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] Werk");
const base = normalizeAutonomyState({ enabled: true, roadmap });
const fresh = summarizeAutonomyProject({
projectId: "project-id", projectSlug: "project", projectName: "Project",
autonomy: { ...base, activeLease: { token: "lease", kind: "implement", actor: "executor", roadmapId: roadmap.id, phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, createdAt: "2026-09-02T10:00:00.000Z", expiresAt: "2026-09-02T10:30:00.000Z" } },
}, Date.parse("2026-09-02T10:15:00.000Z"));
const expired = summarizeAutonomyProject({
projectId: "project-id", projectSlug: "project", projectName: "Project",
autonomy: { ...base, activeLease: { token: "lease", kind: "implement", actor: "executor", roadmapId: roadmap.id, phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, createdAt: "2026-09-02T10:00:00.000Z", expiresAt: "2026-09-02T10:30:00.000Z" } },
}, Date.parse("2026-09-02T10:31:00.000Z"));
expect(fresh).toMatchObject({ activeWorkKind: "implement", pendingWorkCount: 1, lifecycle: "running" });
expect(expired.activeWorkKind).toBeUndefined();
expect(expired.lifecycle).toBe("ready");
});
it("defaults and bounds the continuous executor run budget", () => {
expect(normalizeAutonomyState({})).toMatchObject({ maxWorkItemsPerRun: 8, maxRunMinutes: 45 });
expect(normalizeAutonomyState({ maxWorkItemsPerRun: 100, maxRunMinutes: 2 })).toMatchObject({ maxWorkItemsPerRun: 20, maxRunMinutes: 5 });
});
it("only lets the owning scheduled run resume a live lease", () => {
const lease = {
token: "lease",
kind: "implement" as const,
actor: "chatgpt-web",
runId: "run-1",
roadmapId: "roadmap",
phaseId: "phase",
createdAt: "2026-09-02T10:00:00.000Z",
expiresAt: "2026-09-02T10:30:00.000Z",
};
expect(canResumeAutonomyLease(lease, "chatgpt-web", "run-1")).toBe(true);
expect(canResumeAutonomyLease(lease, "chatgpt-web", "run-2")).toBe(false);
expect(canResumeAutonomyLease(lease, "another-executor", "run-1")).toBe(false);
expect(canResumeAutonomyLease(lease, "chatgpt-web")).toBe(false);
expect(isAutonomyLeaseOwner(lease, "chatgpt-web")).toBe(true);
expect(isAutonomyLeaseOwner(lease, "another-executor")).toBe(false);
});
it("enforces item and elapsed-time budgets from durable run state", () => {
const base = normalizeAutonomyState({
maxWorkItemsPerRun: 2,
maxRunMinutes: 10,
runs: [{ id: "item-run", actor: "chatgpt-web", startedAt: "2026-09-03T10:00:00.000Z", reportedWorkItems: 2 }],
});
expect(evaluateAutonomyRunBudget(base, "chatgpt-web", "item-run", Date.parse("2026-09-03T10:01:00.000Z"))).toMatchObject({ allowed: false, reason: "item_limit" });
const timed = normalizeAutonomyState({
maxWorkItemsPerRun: 8,
maxRunMinutes: 10,
runs: [{ id: "time-run", actor: "chatgpt-web", startedAt: "2026-09-03T10:00:00.000Z", reportedWorkItems: 1 }],
});
expect(evaluateAutonomyRunBudget(timed, "chatgpt-web", "time-run", Date.parse("2026-09-03T10:10:00.000Z"))).toMatchObject({ allowed: false, reason: "time_limit" });
expect(evaluateAutonomyRunBudget(timed, "other-actor", "time-run", Date.parse("2026-09-03T10:01:00.000Z"))).toMatchObject({ allowed: false, reason: "owner_mismatch" });
});
it("classifies technical failures as retryable and reserves human blockers for real owner input", () => {
expect(classifyAutonomyBlocker("Untrusted non-HTTPS Gitea server URL")).toBe("retryable");
expect(classifyAutonomyBlocker("Runner unavailable during validation")).toBe("retryable");
expect(classifyAutonomyBlocker("Missing credential required for production deployment")).toBe("human");
expect(classifyAutonomyBlocker("Owner decision required between two product directions")).toBe("human");
expect(classifyAutonomyBlocker("Temporary provider failure", "human")).toBe("retryable");
});
it("backs off repeated technical failures without requiring a human decision", () => {
expect(autonomyRetryDelayMinutes(1)).toBe(15);
expect(autonomyRetryDelayMinutes(2)).toBe(30);
expect(autonomyRetryDelayMinutes(3)).toBe(60);
expect(autonomyRetryDelayMinutes(20)).toBeLessThanOrEqual(1_440);
});
it("makes a due retryable blocker claimable without clearing failure counters", () => {
const roadmap = parseRoadmapMarkdown("## Fase\n- [ ] Werk");
roadmap.status = "blocked";
roadmap.phases[0].status = "blocked";
roadmap.phases[0].items[0].status = "blocked";
roadmap.phases[0].items[0].attempts = 2;
const blocked = normalizeAutonomyState({
enabled: true,
roadmap,
blocker: { at: "2026-09-03T10:00:00.000Z", retryAt: "2026-09-03T11:00:00.000Z", kind: "retryable", phaseId: roadmap.phases[0].id, itemId: roadmap.phases[0].items[0].id, reason: "Temporary provider failure" },
});
const retried = retryAutonomyBlockerIfDue(blocked, Date.parse("2026-09-03T11:00:00.000Z"));
expect(retried).toMatchObject({ status: "running", roadmap: { status: "active", phases: [{ status: "active", items: [{ status: "pending", attempts: 2 }] }] } });
expect(retried.blocker).toBeUndefined();
expect(retried.events.at(-1)?.type).toBe("autonomy.blocker.retry_ready");
});
it("fences heartbeat ownership between overlapping scheduled runs", () => {
const current = { runId: "new-run", status: "working", startedAt: new Date("2026-09-03T10:00:00.000Z") };
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "old-run", status: "idle" })).toMatchObject({ accepted: false });
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", status: "degraded" })).toMatchObject({ accepted: false, reason: "run_owner_mismatch" });
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "new-run", status: "idle" })).toMatchObject({ accepted: true, startedAt: current.startedAt });
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:10:00.000Z"))).toMatchObject({ accepted: false, reason: "active_run" });
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T11:00:00.000Z"))).toMatchObject({ accepted: true, startedAt: new Date("2026-09-03T11:00:00.000Z") });
expect(resolveAutonomyHeartbeat({ ...current, status: "idle" }, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:10:00.000Z"))).toMatchObject({ accepted: true, startedAt: new Date("2026-09-03T10:10:00.000Z") });
expect(resolveAutonomyHeartbeat({ ...current, activeProjectId: "project-a" }, { instanceId: "ordinary-chat", runId: "new-run", status: "working", activeProjectId: "project-b" })).toMatchObject({ accepted: false, reason: "active_project" });
});
it("allows takeover after a short project's configured run window plus grace", () => {
const current = { runId: "old-run", status: "working", startedAt: new Date("2026-09-03T10:00:00.000Z") };
const takeoverAfterMs = 25 * 60_000;
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:24:00.000Z"), takeoverAfterMs)).toMatchObject({ accepted: false, reason: "active_run" });
expect(resolveAutonomyHeartbeat(current, { instanceId: "ordinary-chat", runId: "next-run", status: "working" }, new Date("2026-09-03T10:26:00.000Z"), takeoverAfterMs)).toMatchObject({ accepted: true });
});
it("isolates concurrent ordinary ChatGPT executors by project", () => {
expect(autonomyHeartbeatSlotId("mediarr")).toBe("project:mediarr");
expect(autonomyHeartbeatSlotId("rombuilder")).toBe("project:rombuilder");
expect(autonomyHeartbeatSlotId("mediarr")).not.toBe(autonomyHeartbeatSlotId("rombuilder"));
expect(autonomyHeartbeatSlotId()).toBe("primary");
});
it("projects large durable roadmaps into bounded MCP context", () => {
const roadmap = parseRoadmapMarkdown(`## Current\n${Array.from({ length: 80 }, (_, index) => `- [ ] Item ${index + 1} ${"detail ".repeat(30)}`).join("\n")}\n## Later\n${Array.from({ length: 80 }, (_, index) => `- [ ] Later ${index + 1}`).join("\n")}`);
const state = normalizeAutonomyState({
enabled: true,
roadmap,
events: Array.from({ length: 30 }, (_, index) => ({ id: `event-${index}`, at: "2026-09-04T10:00:00.000Z", type: "test", actor: "chatgpt", summary: "evidence ".repeat(200) })),
});
for (const item of state.roadmap!.phases[0].items.slice(0, 40)) item.status = "completed";
const compact = compactAutonomyState(state, { itemLimit: 10, eventLimit: 3 });
const serialized = JSON.stringify(compact);
expect(compact.roadmap?.currentPhase?.items).toHaveLength(10);
expect(compact.roadmap?.currentPhase).toMatchObject({ totalItems: 80, itemWindowStart: 37, itemsReturned: 10, hasMoreItems: true });
expect(compact.roadmap?.currentPhase?.items[3]).toMatchObject({ title: expect.stringContaining("Item 41"), status: "pending" });
expect(compact.events).toHaveLength(3);
expect(serialized).not.toContain("sourceMarkdown");
expect(serialized.length).toBeLessThan(12_000);
expect(serialized.length).toBeLessThan(JSON.stringify(state).length / 5);
});
});
describe("Auto Mode MCP surface", () => {
it("orders claimable projects fairly before considering backlog size", () => {
const projects = [
{ name: "huge-recent", status: "running", pending_work_items: 201, last_run_at: "2026-09-05T09:00:00.000Z" },
{ name: "small-old", status: "running", pending_work_items: 4, last_run_at: "2026-09-04T09:00:00.000Z" },
{ name: "never-run", status: "running", pending_work_items: 2 },
{ name: "busy", status: "running", pending_work_items: 50, active_work_kind: "implement", last_run_at: "2026-09-01T09:00:00.000Z" },
{ name: "blocked", status: "blocked", pending_work_items: 500, last_run_at: "2026-08-01T09:00:00.000Z" },
].sort(compareAutoModeProjectRows);
expect(projects.map((project) => project.name)).toEqual(["never-run", "small-old", "huge-recent", "busy", "blocked"]);
});
it("advertises the complete durable roadmap controller to MCP clients", async () => {
const server = createProjectBrainMcpServer(env, "auto-contract-test");
const client = new Client({ name: "auto-contract-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const advertisedTools = (await client.listTools()).tools;
const tools = advertisedTools.map((tool) => tool.name);
expect(tools).toEqual(expect.arrayContaining([
"list_auto_mode_projects",
"heartbeat_auto_executor",
"get_auto_mode_state",
"configure_auto_mode",
"import_auto_roadmap",
"resume_auto_mode",
"claim_auto_work",
"report_auto_work",
]));
expect(advertisedTools.find((tool) => tool.name === "claim_auto_work")?.inputSchema).toMatchObject({
required: expect.arrayContaining(["project_id", "run_id"]),
});
expect(advertisedTools.find((tool) => tool.name === "configure_auto_mode")?.inputSchema).toMatchObject({
properties: expect.objectContaining({ max_work_items_per_run: expect.any(Object), max_run_minutes: expect.any(Object), max_recovery_minutes: expect.any(Object), pause_after_current: expect.any(Object), scheduling_policy: expect.any(Object) }),
});
expect(advertisedTools.find((tool) => tool.name === "get_auto_mode_state")?.inputSchema).toMatchObject({
properties: expect.objectContaining({ view: expect.any(Object), item_limit: expect.any(Object), event_limit: expect.any(Object) }),
});
expect(advertisedTools.find((tool) => tool.name === "report_auto_work")?.inputSchema).toMatchObject({
properties: expect.objectContaining({ blocker_type: expect.any(Object), retry_after_minutes: expect.any(Object) }),
});
} finally {
await client.close();
await server.close();
}
});
});
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { autonomyRetryDelayMinutes, classifyAutonomyBlocker } from "../src/services/autonomyRecovery.js";
describe("Auto Mode recovery classification", () => {
it("keeps ordinary infrastructure failures retryable even when a client requests human escalation", () => {
expect(classifyAutonomyBlocker("Gitea runner timed out while starting validation.", "human")).toBe("retryable");
expect(classifyAutonomyBlocker("Untrusted non-HTTPS Gitea server URL blocked security bootstrap.", "human")).toBe("retryable");
expect(classifyAutonomyBlocker("Provider returned HTTP 502 Bad Gateway.", "human")).toBe("retryable");
});
it("requires human attention only when durable evidence says a real owner action is needed", () => {
expect(classifyAutonomyBlocker("Missing credential required for the production deployment.", "retryable")).toBe("human");
expect(classifyAutonomyBlocker("Owner decision is required before choosing between destructive reset and preserving production data.")).toBe("human");
expect(classifyAutonomyBlocker("Approval is required for this irreversible operation.")).toBe("human");
});
it("defaults unknown operational failures to retryable instead of creating approval chores", () => {
expect(classifyAutonomyBlocker("Unexpected validation orchestration failure.", "human")).toBe("retryable");
});
it("uses bounded exponential retry delays", () => {
expect(autonomyRetryDelayMinutes(1)).toBe(15);
expect(autonomyRetryDelayMinutes(2)).toBe(30);
expect(autonomyRetryDelayMinutes(3)).toBe(60);
expect(autonomyRetryDelayMinutes(7)).toBe(960);
expect(autonomyRetryDelayMinutes(100)).toBe(960);
});
it.each([
"CI failed. No approval is required; repair the same branch.",
"No destructive operation was performed. Runner is unavailable.",
"This is not a credentials or approval requirement. Validation failed.",
])("does not mistake negated human decisions for blockers: %s", (reason) => {
expect(classifyAutonomyBlocker(reason, "human")).toBe("retryable");
});
it("retains a real decision even beside negated or technical context", () => {
expect(classifyAutonomyBlocker("No CI output is available, but missing credentials prevent deployment.")).toBe("human");
expect(classifyAutonomyBlocker("Runner failed. Owner authorization is required to change branch protection.")).toBe("human");
expect(classifyAutonomyBlocker("No changes were made, owner authorization is required.")).toBe("human");
expect(classifyAutonomyBlocker("This is not a CI issue and credentials are required.")).toBe("human");
});
});
+41
View File
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { reconcileAutonomyPlan } from "../src/services/autonomyReplanner.js";
import { normalizeAutonomyState } from "../src/repositories/autonomyRepository.js";
const state = () => normalizeAutonomyState({ enabled: true, roadmap: { id: "roadmap-1", title: "Release", status: "active", currentPhaseIndex: 0, importedAt: "2026-09-12T10:00:00.000Z", phases: [{ id: "phase-1", title: "Build", objective: "Ship", status: "executing", items: [{ id: "item-1", title: "Feature", source: "delivery", deliveryId: "delivery-1", status: "in_progress", attempts: 1 }] }] } });
describe("autonomy reality reconciliation", () => {
it("preserves work completed by an external merge", () => {
const result = reconcileAutonomyPlan(state(), [{ itemId: "item-1", deliveryId: "delivery-1", status: "pull_request_open", mergeCommitSha: "abc123", validationEvidence: "head123" }]);
const item = result.state.roadmap?.phases[0]?.items[0];
expect(item?.status).toBe("completed");
expect(item?.completionEvidence).toEqual(expect.arrayContaining(["delivery:delivery-1", "merge:abc123", "head123"]));
});
it("adds bounded recovery work for superseded delivery state", () => {
const result = reconcileAutonomyPlan(state(), [{ itemId: "item-1", deliveryId: "delivery-1", status: "superseded", supersededBy: "delivery-2" }]);
expect(result.repairItems).toHaveLength(1);
expect(result.state.roadmap?.phases[0]?.items).toHaveLength(2);
expect(result.state.roadmap?.phases[0]?.items[1]?.source).toBe("recovery");
});
it("replans a previously completed item when its delivery is superseded", () => {
const completed = state();
completed.roadmap!.phases[0].items[0].status = "completed";
const result = reconcileAutonomyPlan(completed, [{ itemId: "item-1", deliveryId: "delivery-1", status: "merged", supersededBy: "delivery-2" }]);
expect(result.repairItems).toHaveLength(1);
expect(result.state.roadmap?.phases[0]?.items[0]).toMatchObject({ status: "completed", supersededBy: "delivery-2" });
});
it("clears an external wait when the observed delivery becomes available again", () => {
const waiting = state();
const item = waiting.roadmap?.phases[0]?.items[0];
if (!item) throw new Error("test item missing");
item.waitingExternal = true;
item.lastError = "Provider unavailable";
const result = reconcileAutonomyPlan(waiting, [{ itemId: item.id, deliveryId: "delivery-1", status: "executing" }]);
expect(result.changed).toBe(true);
expect(result.state.roadmap?.phases[0]?.items[0]).toMatchObject({ waitingExternal: false, status: "in_progress" });
expect(result.state.roadmap?.phases[0]?.items[0]?.lastError).toBeUndefined();
});
});
+44
View File
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { chooseNextAutonomyWork, planParallelAutonomyWork } from "../src/services/autonomyScheduler.js";
import { parseRoadmapMarkdown } from "../src/repositories/autonomyRepository.js";
describe("smart autonomy scheduler", () => {
it("chooses the independent item that unlocks the most downstream work", () => {
const roadmap = parseRoadmapMarkdown("## Delivery\n- [ ] A\n- [ ] B\n- [ ] C\n- [ ] D");
const [a, b, c, d] = roadmap.phases[0].items;
c.dependsOn = [a.id];
d.dependsOn = [a.id];
b.priority = 80;
const selected = chooseNextAutonomyWork(roadmap.phases, 0);
expect(selected?.item.id).toBe(a.id);
expect(selected?.downstreamCount).toBe(2);
});
it("does not group likely path-conflicting work in parallel", () => {
const roadmap = parseRoadmapMarkdown("## Delivery\n- [ ] A\n- [ ] B\n- [ ] C");
const [a, b, c] = roadmap.phases[0].items;
a.touchedPaths = ["src/shared/project.ts"];
b.touchedPaths = ["src/shared/project.ts"];
c.touchedPaths = ["src/other.ts"];
const candidates = chooseNextAutonomyWork(roadmap.phases, 0);
expect(candidates?.item).toBeTruthy();
const all = roadmap.phases[0].items.map((item) => ({ item, phase: roadmap.phases[0], phaseIndex: 0, score: 1, downstreamCount: 0, conflictKeys: (item.touchedPaths ?? []).map((path) => `path:${path}`) }));
const groups = planParallelAutonomyWork(all);
expect(groups.some((group) => group.includes(a.id) && group.includes(b.id))).toBe(false);
});
it("can deliberately favor downstream impact when a command changes scheduling policy", () => {
const roadmap = parseRoadmapMarkdown("## Delivery\n- [ ] A\n- [ ] B\n- [ ] C\n- [ ] D");
const [a, b, c, d] = roadmap.phases[0].items;
c.dependsOn = [a.id];
d.dependsOn = [a.id];
b.priority = 100;
expect(chooseNextAutonomyWork(roadmap.phases, 0, "downstream-impact")?.item.id).toBe(a.id);
});
it("does not claim work while its external dependency is unavailable", () => {
const roadmap = parseRoadmapMarkdown("## Delivery\n- [ ] A\n- [ ] B");
roadmap.phases[0].items[0].waitingExternal = true;
expect(chooseNextAutonomyWork(roadmap.phases, 0)?.item.id).toBe(roadmap.phases[0].items[1].id);
});
});
+42
View File
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { normalizeAutonomyState, parseRoadmapMarkdown, retryAutonomyBlockerIfDue } from "../src/repositories/autonomyRepository.js";
import { addDiscoveredTasks, isAutonomyItemReady, orderAutonomyRepairs, workPrompt } from "../src/services/autonomyWork.js";
describe("autonomous repair sequencing", () => {
it("runs an existing prerequisite before retrying a priority user assignment", () => {
const phase = parseRoadmapMarkdown("## CI\n- Gate Ruff\n- Fix Ruff errors").phases[0];
const [gate, repair] = phase.items;
gate.source = "user_assignment";
expect(addDiscoveredTasks(phase, ["Fix Ruff errors"])).toEqual([]);
expect(orderAutonomyRepairs(phase, gate, ["Fix Ruff errors"])).toBe(true);
expect(isAutonomyItemReady(phase, gate)).toBe(false);
expect(isAutonomyItemReady(phase, repair)).toBe(true);
repair.status = "completed";
expect(isAutonomyItemReady(phase, gate)).toBe(true);
});
it("rejects circular repair dependencies and does not duplicate discovered work", () => {
const phase = parseRoadmapMarkdown("## CI\n- Gate\n- Repair").phases[0];
orderAutonomyRepairs(phase, phase.items[0], ["Repair"]);
expect(() => orderAutonomyRepairs(phase, phase.items[1], ["Gate"])).toThrow("AUTO_MODE_DEPENDENCY_CYCLE");
expect(addDiscoveredTasks(phase, ["Repair", " repair ", "New test"])).toEqual(["New test"]);
});
it("recovers historical technical human blockers with a bounded retry time", () => {
const roadmap = parseRoadmapMarkdown("## CI\n- Repair");
roadmap.status = "blocked";
roadmap.phases[0].status = "blocked";
const state = normalizeAutonomyState({ enabled: true, roadmap, maxRecoveryMinutes: 120, blocker: { kind: "human", at: "2026-09-01T23:00:00Z", recoveryStartedAt: "2026-09-01T23:00:00Z", reason: "CI validation failed. No approval is required." } });
expect(state.blocker?.kind).toBe("retryable");
expect(retryAutonomyBlockerIfDue(state, Date.parse("2026-09-02T00:00:00Z")).status).toBe("running");
});
it("resumes the recorded delivery through merge rather than suggesting a new branch", () => {
const phase = parseRoadmapMarkdown("## Build\n- Feature").phases[0];
phase.items[0].deliveryId = "existing-delivery";
const prompt = workPrompt("implement", phase, phase.items[0]);
expect(prompt).toContain("Resume delivery 'existing-delivery'");
expect(prompt).toContain("merge_feature_delivery");
expect(prompt).toContain("Always report delivery_id");
});
});
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { normalizeAutonomyState } from "../src/repositories/autonomyRepository.js";
import { canonicalLifecycleFromAutonomy } from "../src/services/canonicalLifecycle.js";
describe("backend canonical lifecycle projection", () => {
it("uses the same seven-state semantics as the product contract", () => {
expect(canonicalLifecycleFromAutonomy(normalizeAutonomyState({ enabled: false, status: "off" }))).toBe("ready");
expect(canonicalLifecycleFromAutonomy(normalizeAutonomyState({
enabled: true,
status: "blocked",
blocker: { kind: "human", reason: "Owner decision required", at: "2026-09-01T00:00:00Z" },
}))).toBe("needs_user");
expect(canonicalLifecycleFromAutonomy(normalizeAutonomyState({
enabled: true,
status: "blocked",
blocker: { kind: "retryable", reason: "Runner timeout", at: "2026-09-01T00:00:00Z" },
}))).toBe("recovering");
const completed = normalizeAutonomyState({ enabled: true, status: "completed" });
expect(canonicalLifecycleFromAutonomy({ ...completed, status: "completed" })).toBe("done");
});
});
@@ -0,0 +1,300 @@
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { buildApp } from "../src/app.js";
import { loadEnv } from "../src/config/env.js";
import { prisma } from "../src/db/prisma.js";
import { createSession } from "../src/security/authStore.js";
import { hashPassword } from "../src/security/password.js";
import { createHash } from "node:crypto";
import { apiPaths } from "@projectbrain/contracts";
import { exportJWK, generateKeyPair, SignJWT } from "jose";
const databaseUrl = process.env.TEST_DATABASE_URL;
if (databaseUrl) process.env.DATABASE_URL = databaseUrl;
const databaseDescribe = databaseUrl ? describe : describe.skip;
const ownerPassword = "database-owner-password-value";
const coreLoopIds = {
capture: [] as string[], review: [] as string[], truth: [] as string[], update: [] as string[],
handoff: [] as string[], task: [] as string[], runImport: [] as string[],
};
const testEnv = () => loadEnv({
NODE_ENV: "test",
LOG_LEVEL: "silent",
DATABASE_URL: databaseUrl,
PROJECTBRAIN_DATA_SOURCE: "database",
PROJECTBRAIN_OWNER_USERNAME: "database-owner",
PROJECTBRAIN_OWNER_PASSWORD: ownerPassword,
PROJECTBRAIN_AUTH_SECRET: "database-test-secret-with-at-least-thirty-two-characters",
PROJECTBRAIN_SESSION_TTL_HOURS: "1",
PROJECTBRAIN_TRUST_PROXY: "false",
});
const entraEnv = () => loadEnv({
NODE_ENV: "test",
LOG_LEVEL: "silent",
DATABASE_URL: databaseUrl,
PROJECTBRAIN_DATA_SOURCE: "database",
PROJECTBRAIN_OWNER_USERNAME: "database-owner",
PROJECTBRAIN_OWNER_PASSWORD: ownerPassword,
PROJECTBRAIN_AUTH_SECRET: "database-test-secret-with-at-least-thirty-two-characters",
PROJECTBRAIN_SESSION_TTL_HOURS: "1",
PROJECTBRAIN_TRUST_PROXY: "false",
PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com",
PROJECTBRAIN_ENTRA_TENANT_ID: "11111111-1111-4111-8111-111111111111",
PROJECTBRAIN_ENTRA_CLIENT_ID: "22222222-2222-4222-8222-222222222222",
PROJECTBRAIN_ENTRA_CLIENT_SECRET: "database-entra-test-secret",
});
databaseDescribe("database authentication persistence", () => {
beforeAll(async () => {
await prisma.oidcLoginAttempt.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.externalIdentity.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.authSession.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.projectMembership.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.user.deleteMany({ where: { username: { in: ["database-owner", "database-member"] } } });
});
afterAll(async () => {
await prisma.oidcLoginAttempt.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.externalIdentity.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
const markedTasks = await prisma.codexTask.findMany({ where: { goal: "Execute the database core-loop proof" }, select: { id: true } });
await prisma.codexRunImport.deleteMany({ where: { OR: [{ sourceRun: "core-loop-database-proof" }, { codexTaskId: { in: markedTasks.map((item) => item.id) } }] } });
await prisma.reviewQueueItem.deleteMany({ where: { OR: [{ source: "core-loop-database-proof" }, { title: { contains: "Core-loop database proof" } }] } });
await prisma.codexTask.deleteMany({ where: { goal: "Execute the database core-loop proof" } });
await prisma.handoffBundle.deleteMany({ where: { title: "Core-loop database proof handoff" } });
const markedCaptures = await prisma.captureItem.findMany({ where: { content: "Core-loop database proof source material" }, select: { linkedReviewItemId: true } });
const markedReviewIds = markedCaptures.flatMap((item) => item.linkedReviewItemId ? [item.linkedReviewItemId] : []);
const markedTruth = await prisma.acceptedTruthItem.findMany({ where: { sourceReviewItemId: { in: markedReviewIds } }, select: { id: true } });
await prisma.projectUpdate.deleteMany({ where: { sourceTruthItemId: { in: markedTruth.map((item) => item.id) } } });
await prisma.acceptedTruthItem.deleteMany({ where: { id: { in: markedTruth.map((item) => item.id) } } });
await prisma.reviewQueueItem.deleteMany({ where: { id: { in: markedReviewIds } } });
await prisma.captureItem.deleteMany({ where: { content: "Core-loop database proof source material" } });
await prisma.codexRunImport.deleteMany({ where: { id: { in: coreLoopIds.runImport } } });
await prisma.codexTask.deleteMany({ where: { id: { in: coreLoopIds.task } } });
await prisma.handoffBundle.deleteMany({ where: { id: { in: coreLoopIds.handoff } } });
await prisma.projectUpdate.deleteMany({ where: { id: { in: coreLoopIds.update } } });
await prisma.acceptedTruthItem.deleteMany({ where: { id: { in: coreLoopIds.truth } } });
await prisma.reviewQueueItem.deleteMany({ where: { id: { in: coreLoopIds.review } } });
await prisma.captureItem.deleteMany({ where: { id: { in: coreLoopIds.capture } } });
await prisma.idempotencyRecord.deleteMany({ where: { key: { contains: "core-loop-database-proof" } } });
await prisma.operatingLogEntry.deleteMany({ where: { title: "Durable idempotency restart proof" } });
await prisma.idempotencyRecord.deleteMany({ where: { key: { contains: "database-restart-replay" } } });
await prisma.authSession.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.projectMembership.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } });
await prisma.user.deleteMany({ where: { username: { in: ["database-owner", "database-member"] } } });
await prisma.$disconnect();
});
it("keeps a server-side owner session valid across API restart", async () => {
const firstApp = await buildApp({ env: testEnv() });
const login = await firstApp.inject({ method: "POST", url: "/api/auth/login", payload: { username: "database-owner", password: ownerPassword } });
expect(login.statusCode).toBe(200);
const cookie = login.cookies.find((item) => item.name === "projectbrain_session");
expect(cookie).toBeDefined();
const cookieHeader = `${cookie!.name}=${cookie!.value}`;
await firstApp.close();
const restartedApp = await buildApp({ env: testEnv() });
const response = await restartedApp.inject({ method: "GET", url: "/api/projects", headers: { cookie: cookieHeader } });
expect(response.statusCode).toBe(200);
expect(response.json().data.length).toBeGreaterThan(0);
await restartedApp.close();
}, 15_000);
it("creates a one-time PKCE Entra link attempt without persisting raw state", async () => {
const app = await buildApp({ env: entraEnv() });
const login = await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "database-owner", password: ownerPassword } });
const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session");
const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`;
const csrfToken = login.json().data.csrfToken as string;
const response = await app.inject({ method: "POST", url: apiPaths.auth.entraLinkStart, headers: { cookie, "x-csrf-token": csrfToken } });
expect(response.statusCode).toBe(200);
const authorizationUrl = new URL(response.json().data.authorizationUrl);
expect(authorizationUrl.hostname).toBe("login.microsoftonline.com");
expect(authorizationUrl.pathname).toContain("11111111-1111-4111-8111-111111111111");
expect(authorizationUrl.searchParams.get("code_challenge_method")).toBe("S256");
expect(authorizationUrl.searchParams.get("nonce")?.length).toBeGreaterThan(30);
const state = authorizationUrl.searchParams.get("state")!;
const attempt = await prisma.oidcLoginAttempt.findUnique({ where: { stateHash: createHash("sha256").update(state).digest("hex") } });
expect(attempt?.purpose).toBe("link");
expect(attempt?.returnTo).toBe("/settings");
expect(attempt?.stateHash).not.toBe(state);
await app.close();
});
it("validates the Entra ID token and turns a linked identity into a ProjectBrain session", async () => {
const app = await buildApp({ env: entraEnv() });
const login = await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "database-owner", password: ownerPassword } });
const localCookie = login.cookies.find((item) => item.name === "projectbrain_session")!;
const linkStart = await app.inject({ method: "POST", url: apiPaths.auth.entraLinkStart, headers: { cookie: `${localCookie.name}=${localCookie.value}`, "x-csrf-token": login.json().data.csrfToken } });
const authorizationUrl = new URL(linkStart.json().data.authorizationUrl);
const state = authorizationUrl.searchParams.get("state")!;
const nonce = authorizationUrl.searchParams.get("nonce")!;
const tenantId = "11111111-1111-4111-8111-111111111111";
const clientId = "22222222-2222-4222-8222-222222222222";
const objectId = "33333333-3333-4333-8333-333333333333";
const { publicKey, privateKey } = await generateKeyPair("RS256");
const publicJwk = { ...await exportJWK(publicKey), kid: "entra-test-key", alg: "RS256", use: "sig" };
const idToken = await new SignJWT({ tid: tenantId, oid: objectId, nonce, name: "Entra Owner", preferred_username: "owner@example.com" })
.setProtectedHeader({ alg: "RS256", kid: "entra-test-key" })
.setIssuer(`https://login.microsoftonline.com/${tenantId}/v2.0`)
.setAudience(clientId)
.setIssuedAt()
.setExpirationTime("5m")
.sign(privateKey);
vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request) => {
const url = String(input instanceof Request ? input.url : input);
if (url.endsWith("/oauth2/v2.0/token")) return new Response(JSON.stringify({ id_token: idToken }), { status: 200, headers: { "content-type": "application/json" } });
if (url.endsWith("/discovery/v2.0/keys")) return new Response(JSON.stringify({ keys: [publicJwk] }), { status: 200, headers: { "content-type": "application/json" } });
return new Response("not found", { status: 404 });
}));
try {
const callback = await app.inject({ method: "GET", url: `${apiPaths.auth.entraCallback}?code=one-time-code&state=${encodeURIComponent(state)}` });
expect(callback.statusCode).toBe(302);
expect(callback.headers.location).toBe("https://projectbrain.example.com/settings");
const entraCookie = callback.cookies.find((item) => item.name === "projectbrain_session");
expect(entraCookie).toBeDefined();
const identity = await prisma.externalIdentity.findUnique({ where: { provider_tenantId_subject: { provider: "microsoft-entra", tenantId, subject: objectId } } });
expect(identity?.preferredUsername).toBe("owner@example.com");
expect((await app.inject({ method: "GET", url: apiPaths.auth.session, headers: { cookie: `${entraCookie!.name}=${entraCookie!.value}` } })).statusCode).toBe(200);
expect((await app.inject({ method: "GET", url: `${apiPaths.auth.entraCallback}?code=replay&state=${encodeURIComponent(state)}` })).headers.location).toContain("expired_attempt");
} finally {
vi.unstubAllGlobals();
await app.close();
}
}, 15_000);
it("returns 403 until a member receives project access", async () => {
const app = await buildApp({ env: testEnv() });
const member = await prisma.user.create({ data: { username: "database-member", passwordHash: await hashPassword("database-member-password"), role: "member" } });
const session = await createSession(testEnv(), { id: member.id, username: member.username, role: "member", credentialVersion: member.credentialVersion });
const cookie = `projectbrain_session=${app.signCookie(session.token)}`;
const projectCreateDenied = await app.inject({
method: "POST",
url: "/api/projects",
headers: { cookie, "x-csrf-token": session.csrfToken },
payload: { name: "Unauthorized project", description: "A member must not provision a Gitea repository through project creation." },
});
expect(projectCreateDenied.statusCode).toBe(403);
expect(projectCreateDenied.json().error.code).toBe("PROJECT_CREATION_FORBIDDEN");
const denied = await app.inject({ method: "GET", url: "/api/projects/widefrog", headers: { cookie } });
expect(denied.statusCode).toBe(403);
expect(denied.json().error.code).toBe("PROJECT_ACCESS_DENIED");
const project = await prisma.project.findUniqueOrThrow({ where: { slug: "widefrog" } });
await prisma.projectMembership.create({ data: { projectId: project.id, userId: member.id, role: "reader" } });
const allowed = await app.inject({ method: "GET", url: "/api/projects/widefrog", headers: { cookie } });
expect(allowed.statusCode).toBe(200);
const writeDenied = await app.inject({ method: "PATCH", url: "/api/projects/widefrog", headers: { cookie, "x-csrf-token": session.csrfToken }, payload: { name: "Forbidden update" } });
expect(writeDenied.statusCode).toBe(403);
await app.close();
});
it("replays a core-loop mutation durably after API restart", async () => {
const firstApp = await buildApp({ env: testEnv() });
const login = await firstApp.inject({ method: "POST", url: "/api/auth/login", payload: { username: "database-owner", password: ownerPassword } });
const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session");
const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`;
const csrfToken = login.json().data.csrfToken as string;
const headers = { cookie, "x-csrf-token": csrfToken, "idempotency-key": "database-restart-replay" };
const payload = { type: "project update", title: "Durable idempotency restart proof", summary: "This entry must be created once across API restarts.", source: "database-test" };
const created = await firstApp.inject({ method: "POST", url: "/api/projects/widefrog/operating-log", headers, payload });
expect(created.statusCode).toBe(201);
await firstApp.close();
const restartedApp = await buildApp({ env: testEnv() });
const replayed = await restartedApp.inject({ method: "POST", url: "/api/projects/widefrog/operating-log", headers, payload });
expect(replayed.statusCode).toBe(201);
expect(replayed.json().data.id).toBe(created.json().data.id);
expect(await prisma.operatingLogEntry.count({ where: { title: payload.title } })).toBe(1);
expect(await prisma.idempotencyRecord.count({ where: { key: { contains: "database-restart-replay" } } })).toBe(1);
await restartedApp.close();
}, 15_000);
it("executes the complete governed core loop in PostgreSQL mode", async () => {
const app = await buildApp({ env: testEnv() });
const login = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "database-owner", password: ownerPassword } });
const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session");
const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`;
const csrfToken = login.json().data.csrfToken as string;
let operation = 0;
const mutate = (method: "POST" | "PATCH", url: string, payload: Record<string, unknown> = {}) => app.inject({
method, url, payload,
headers: { cookie, "x-csrf-token": csrfToken, "idempotency-key": `core-loop-database-proof-${++operation}` },
});
const capture = await mutate("POST", "/api/projects/widefrog/capture-items", { type: "note", content: "Core-loop database proof source material", source: "database-integration-test" });
expect(capture.statusCode).toBe(201);
coreLoopIds.capture.push(capture.json().data.id);
expect((await mutate("PATCH", `/api/projects/widefrog/capture-items/${capture.json().data.id}/classify`)).statusCode).toBe(200);
const queued = await mutate("POST", `/api/projects/widefrog/capture-items/${capture.json().data.id}/send-to-review`);
expect(queued.statusCode).toBe(201);
const reviewId = queued.json().data.createdEntityId as string;
coreLoopIds.review.push(reviewId);
const concurrentReviewDecisions = await Promise.all([
mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/accept`, { reason: "Accepted by database core-loop proof A" }),
mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/accept`, { reason: "Accepted by database core-loop proof B" }),
]);
expect(concurrentReviewDecisions.map((response) => response.statusCode).sort()).toEqual([200, 409]);
expect(concurrentReviewDecisions.find((response) => response.statusCode === 409)!.json().error.code).toBe("REVIEW_ALREADY_DECIDED");
const materialized = await mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/materialize-truth`);
expect(materialized.statusCode).toBe(200);
const truthId = materialized.json().data.createdEntityId as string;
coreLoopIds.truth.push(truthId);
const conversionCapture = await mutate("POST", "/api/projects/widefrog/capture-items", { type: "note", content: "Convert this reviewed decision into a durable truth artifact", source: "database-conversion-test" });
expect(conversionCapture.statusCode).toBe(201);
coreLoopIds.capture.push(conversionCapture.json().data.id);
expect((await mutate("PATCH", `/api/projects/widefrog/capture-items/${conversionCapture.json().data.id}/classify`)).statusCode).toBe(200);
const conversionQueue = await mutate("POST", `/api/projects/widefrog/capture-items/${conversionCapture.json().data.id}/send-to-review`);
expect(conversionQueue.statusCode).toBe(201);
const conversionReviewId = conversionQueue.json().data.createdEntityId as string;
coreLoopIds.review.push(conversionReviewId);
const converted = await mutate("POST", `/api/projects/widefrog/review-queue/${conversionReviewId}/convert`, { target: "decision", reason: "Database artifact proof" });
expect(converted.statusCode).toBe(200);
const convertedTruthId = converted.json().data.createdEntityId as string;
const convertedTruth = await prisma.acceptedTruthItem.findFirst({ where: { slug: convertedTruthId } });
expect(convertedTruth).toMatchObject({ kind: "decision", status: "converted" });
coreLoopIds.truth.push(convertedTruth!.id);
const duplicateConversion = await mutate("POST", `/api/projects/widefrog/review-queue/${conversionReviewId}/convert`, { target: "decision" });
expect(duplicateConversion.statusCode).toBe(409);
expect(duplicateConversion.json().error.code).toBe("REVIEW_ALREADY_DECIDED");
const prepared = await mutate("POST", `/api/projects/widefrog/accepted-truth/${truthId}/project-updates`);
expect(prepared.statusCode).toBe(200);
const updateId = prepared.json().data.createdEntityId as string;
coreLoopIds.update.push(updateId);
const concurrentUpdateApplications = await Promise.all([
mutate("PATCH", `/api/projects/widefrog/project-updates/${updateId}/apply`),
mutate("PATCH", `/api/projects/widefrog/project-updates/${updateId}/apply`),
]);
expect(concurrentUpdateApplications.map((response) => response.statusCode).sort()).toEqual([200, 409]);
expect(concurrentUpdateApplications.find((response) => response.statusCode === 409)!.json().error.code).toBe("PROJECT_UPDATE_ALREADY_APPLIED");
const handoff = await mutate("POST", "/api/projects/widefrog/handoff-bundles", { target: "codex-task", title: "Core-loop database proof handoff", includeAcceptedTruth: true, includeProjectUpdates: true, includeOperatingLog: true, autoSelectContext: true });
expect(handoff.statusCode).toBe(201);
coreLoopIds.handoff.push(handoff.json().data.id);
expect(handoff.json().data.sourceTruthItemIds).toContain(truthId);
const task = await mutate("POST", `/api/projects/widefrog/handoff-bundles/${handoff.json().data.id}/codex-task`, { goal: "Execute the database core-loop proof", validationCommands: ["pnpm validate:all"] });
expect(task.statusCode).toBe(201);
coreLoopIds.task.push(task.json().data.id);
const imported = await mutate("POST", `/api/projects/widefrog/codex-tasks/${task.json().data.id}/import-output`, { sourceRun: "core-loop-database-proof", summary: "Execution feedback returned to governance.", outputText: "Changed files: none. Validation passed. Review this evidence." });
expect(imported.statusCode).toBe(201);
coreLoopIds.runImport.push(imported.json().data.importDto.id);
coreLoopIds.review.push(imported.json().data.mutation.createdEntityId);
const publicId = (value: string) => ({ OR: [{ id: value }, { slug: value }, { readableId: value }] });
expect(await prisma.captureItem.count({ where: publicId(capture.json().data.id) })).toBe(1);
expect(await prisma.acceptedTruthItem.count({ where: { AND: [publicId(truthId), { status: "accepted" }] } })).toBe(1);
expect(await prisma.projectUpdate.count({ where: { AND: [publicId(updateId), { status: "applied" }] } })).toBe(1);
expect(await prisma.codexTask.count({ where: { AND: [publicId(task.json().data.id), { status: "imported" }] } })).toBe(1);
expect(await prisma.reviewQueueItem.count({ where: publicId(imported.json().data.mutation.createdEntityId) })).toBe(1);
await app.close();
}, 15_000);
});
+165
View File
@@ -0,0 +1,165 @@
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { prisma } from "../src/db/prisma.js";
import { configureAutonomy, getAutonomyState, importAutonomyRoadmap, reportAutonomyWork } from "../src/repositories/autonomyRepository.js";
import { claimAutonomyWork } from "../src/services/autonomyRuntimeControl.js";
import { createOrResumeAutonomyDelivery } from "../src/services/autonomyDelivery.js";
import { createFeatureDelivery, getFeatureDelivery, openFeatureDeliveryPullRequest, recordFeatureDeliveryActionValidation, recordFeatureDeliveryProgress, startFeatureDelivery } from "../src/repositories/featureDeliveryRepository.js";
import { refreshDeliveryFromGitea } from "../src/services/deliveryReconciliation.js";
import { createProject } from "../src/repositories/projectsRepository.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createProjectBrainMcpServer } from "../src/mcp/server.js";
import { loadEnv } from "../src/config/env.js";
import { GiteaApiError, GiteaClient } from "../src/integrations/gitea/client.js";
const databaseUrl = process.env.TEST_DATABASE_URL;
if (databaseUrl) process.env.DATABASE_URL = databaseUrl;
const databaseDescribe = databaseUrl ? describe : describe.skip;
const slugs: string[] = [];
databaseDescribe("autonomy database write isolation", () => {
it("preserves progress and the active lease when the same roadmap is imported again", async () => {
const slug = `autonomy-import-${Date.now()}`;
slugs.push(slug);
await createProject({ id: slug, name: "Roadmap import proof", description: "Isolated database regression" });
await configureAutonomy(slug, { enabled: true, autoBrainstorm: false }, "database-test");
const markdown = "## Build\n- Implement feature";
await importAutonomyRoadmap(slug, { markdown }, "database-test");
const claim = await claimAutonomyWork(slug, "database-test");
const repeated = await importAutonomyRoadmap(slug, { markdown: `\n${markdown}\n` }, "database-test");
expect(repeated.roadmap).toEqual(claim.state.roadmap);
expect(repeated.activeLease).toEqual(claim.state.activeLease);
await expect(importAutonomyRoadmap(slug, { markdown: "## Replacement\n- New task" }, "database-test")).rejects.toThrow("AUTO_MODE_WORK_ACTIVE");
expect((await getAutonomyState(slug)).activeLease?.token).toBe(claim.work!.token);
});
afterEach(() => vi.restoreAllMocks());
afterAll(async () => {
if (slugs.length) {
await prisma.deliveryPlan.deleteMany({ where: { project: { slug: { in: slugs } } } });
await prisma.project.deleteMany({ where: { slug: { in: slugs } } });
}
await prisma.$disconnect();
});
it("upgrades legacy autonomy state and preserves unrelated workspace notes", async () => {
const slug = `autonomy-legacy-${Date.now()}`;
slugs.push(slug);
await createProject({ id: slug, name: "Autonomy legacy proof", description: "Isolated database regression" });
const project = await prisma.project.findUniqueOrThrow({ where: { slug } });
await prisma.workspaceState.update({ where: { projectId: project.id }, data: { notesJson: { autonomy: {}, unrelated: { value: "preserve me" } } } });
const state = await configureAutonomy(slug, { enabled: true }, "database-test");
expect(state.enabled).toBe(true);
const workspace = await prisma.workspaceState.findUniqueOrThrow({ where: { projectId: project.id } });
expect(workspace.notesJson).toMatchObject({ unrelated: { value: "preserve me" }, autonomy: { enabled: true } });
});
it("advances the CAS version even when the clock does not advance", async () => {
const slug = `autonomy-clock-${Date.now()}`;
slugs.push(slug);
await createProject({ id: slug, name: "Autonomy clock proof", description: "Isolated database regression" });
const first = await configureAutonomy(slug, { enabled: true }, "database-test");
vi.spyOn(Date, "now").mockReturnValue(Date.parse(first.updatedAt));
const second = await configureAutonomy(slug, { enabled: false }, "database-test");
expect(Date.parse(second.updatedAt)).toBeGreaterThan(Date.parse(first.updatedAt));
expect(second.enabled).toBe(false);
});
it("resumes a failed implementation and executes its existing repair prerequisite first", async () => {
const slug = `autonomy-repair-${Date.now()}`;
slugs.push(slug);
await createProject({ id: slug, name: "Autonomy repair proof", description: "Isolated database regression" });
await configureAutonomy(slug, { enabled: true, autoBrainstorm: false }, "database-test");
await importAutonomyRoadmap(slug, { markdown: "## CI\n- Add gate\n- Fix source lint" }, "database-test");
const first = await claimAutonomyWork(slug, "database-test", "repair-run");
const report = await reportAutonomyWork(slug, { token: first.work!.token, outcome: "blocked", summary: "CI failed; no approval is required.", discoveredTasks: ["Fix source lint"] }, "database-test");
expect(report.state.status).toBe("running");
expect(report.addedTasks).toEqual([]);
const repair = await claimAutonomyWork(slug, "database-test", "repair-run");
expect(repair.work!.itemTitle).toBe("Fix source lint");
await expect(reportAutonomyWork(slug, { token: repair.work!.token, outcome: "success", summary: "Unproven success" }, "database-test")).rejects.toThrow("AUTO_MODE_DELIVERY_REQUIRED");
expect((await getAutonomyState(slug)).activeLease?.token).toBe(repair.work!.token);
});
it("retains one delivery across retries, repairs an open PR and invalidates stale evidence", async () => {
const slug = `autonomy-delivery-${Date.now()}`;
slugs.push(slug);
await createProject({ id: slug, name: "Delivery repair proof", description: "Isolated database regression" });
const project = await prisma.project.findUniqueOrThrow({ where: { slug } });
await prisma.giteaRepositoryLink.create({ data: { projectId: project.id, owner: "AutonomyProof", repository: slug, defaultBranch: "main", linkedBy: "database-test" } });
await configureAutonomy(slug, { enabled: true, autoBrainstorm: false }, "database-test");
await importAutonomyRoadmap(slug, { markdown: "## Build\n- Implement feature" }, "database-test");
const claim = await claimAutonomyWork(slug, "database-test", "delivery-run");
const plan = { title: "Implement feature", intent: "Complete feature", problem: "Broken flow", desiredOutcome: "Working flow", goals: ["repair"], acceptanceCriteria: ["tests pass"], implementationPlan: ["implement", "validate", "merge"], validationCommands: ["pnpm test"], riskLevel: "low" as const };
const delivery = await createOrResumeAutonomyDelivery(slug, plan, "database-test");
expect((await createOrResumeAutonomyDelivery(slug, { ...plan, title: "Unnecessary replacement" }, "database-test")).id).toBe(delivery.id);
await startFeatureDelivery(slug, delivery.id, "chatgpt/existing", "database-test");
await reportAutonomyWork(slug, { token: claim.work!.token, outcome: "failed", summary: "CI failed on existing branch" }, "database-test");
const resumed = await claimAutonomyWork(slug, "database-test", "delivery-run");
expect(resumed.work!.deliveryId).toBe(delivery.id);
const evidence = { provider: "gitea_actions" as const, runId: 1, runNumber: 1, url: "https://gitea.example.com/runs/1", workflowPath: ".gitea/workflows/ci.yml", headSha: "a".repeat(40), conclusion: "success" as const, verifiedAt: new Date().toISOString() };
await recordFeatureDeliveryActionValidation(slug, delivery.id, evidence, "database-test");
await openFeatureDeliveryPullRequest(slug, delivery.id, { number: 1, url: "https://gitea.example.com/pulls/1", title: "Feature" }, "database-test");
await expect(reportAutonomyWork(slug, { token: resumed.work!.token, outcome: "success", summary: "PR opened" }, "database-test")).rejects.toThrow("AUTO_MODE_DELIVERY_NOT_MERGED");
expect((await getAutonomyState(slug)).activeLease?.token).toBe(resumed.work!.token);
const head = "b".repeat(40);
let remoteMerged = false;
vi.spyOn(GiteaClient.prototype, "getPullRequest").mockImplementation(async () => ({ number: 1, title: "Feature", html_url: "https://gitea.example.com/pulls/1", state: remoteMerged ? "closed" : "open", merged: remoteMerged, merge_commit_sha: remoteMerged ? "c".repeat(40) : undefined, head: { ref: "chatgpt/existing", sha: head }, base: { ref: "main", sha: "d".repeat(40) }, mergeable: true }));
vi.spyOn(GiteaClient.prototype, "getBranch").mockResolvedValue({ name: "chatgpt/existing", commit: { id: head, message: "Repair" }, protected: false });
vi.spyOn(GiteaClient.prototype, "getContents").mockResolvedValue({ type: "file", path: "src/feature.ts", name: "feature.ts", sha: "file-sha", size: 4, encoding: "base64", content: "dGVzdA==" });
const write = vi.spyOn(GiteaClient.prototype, "updateFile").mockResolvedValue({});
vi.spyOn(GiteaClient.prototype, "listActionRuns").mockResolvedValue({ total_count: 1, workflow_runs: [{ id: 2, run_number: 2, display_title: "CI", path: ".gitea/workflows/ci.yml", event: "pull_request", head_sha: head, head_branch: "chatgpt/existing", status: "completed", conclusion: "success", html_url: "https://gitea.example.com/runs/2" }] });
vi.spyOn(GiteaClient.prototype, "getBranchProtection").mockResolvedValue({ enable_status_check: true, status_check_contexts: ["CI / test"] });
vi.spyOn(GiteaClient.prototype, "getCombinedCommitStatus").mockResolvedValue({ state: "success", statuses: [{ context: "CI / test", status: "success" }] });
const merge = vi.spyOn(GiteaClient.prototype, "mergePullRequest").mockImplementation(async () => { remoteMerged = true; });
const server = createProjectBrainMcpServer(loadEnv({ NODE_ENV: "test", DATABASE_URL: databaseUrl, PROJECTBRAIN_DATA_SOURCE: "database", PROJECTBRAIN_OWNER_USERNAME: "owner", PROJECTBRAIN_OWNER_PASSWORD: "database-proof-password", PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_GITEA_URL: "https://gitea.example.com", PROJECTBRAIN_GITEA_TOKEN: "test-gitea-token-with-at-least-twenty-characters" }), "database-test");
const client = new Client({ name: "delivery-recovery-proof", version: "1" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const written = await client.callTool({ name: "write_feature_file", arguments: { project_id: slug, delivery_id: delivery.id, branch: "chatgpt/existing", path: "src/feature.ts", content: "fixed", message: "Repair failing validation", expected_sha: "file-sha" } });
expect(written.isError, JSON.stringify(written.content)).not.toBe(true);
expect(write).toHaveBeenCalledTimes(1);
const repaired = await getFeatureDelivery(slug, delivery.id);
expect(repaired.spec.status).toBe("pull_request_open");
expect(repaired.spec.pullRequest?.number).toBe(1);
expect(repaired.spec.validationEvidence).toBeUndefined();
expect(repaired.spec.validationResults).toEqual([]);
const synced = await client.callTool({ name: "sync_feature_validation", arguments: { project_id: slug, delivery_id: delivery.id } });
expect(synced.isError, JSON.stringify(synced.content)).not.toBe(true);
const merged = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } });
expect(merged.isError, JSON.stringify(merged.content)).not.toBe(true);
expect(merge).toHaveBeenCalledTimes(1);
const dbPlan = await prisma.deliveryPlan.findFirstOrThrow({ where: { projectId: project.id, slug: delivery.id } });
// Simulate a previous response/webhook interruption after Gitea merged.
await prisma.deliveryPlan.update({ where: { id: dbPlan.id }, data: { status: "pull_request_open" } });
const replay = await client.callTool({ name: "merge_feature_delivery", arguments: { project_id: slug, delivery_id: delivery.id, expected_head_sha: head, delete_branch: false } });
expect(replay.isError, JSON.stringify(replay.content)).not.toBe(true);
expect(merge).toHaveBeenCalledTimes(1);
await client.close();
await server.close();
const completed = await reportAutonomyWork(slug, { token: resumed.work!.token, outcome: "success", summary: "Validated branch merged" }, "database-test");
expect(completed.state.roadmap!.phases[0].items[0].status).toBe("completed");
});
it("retires a removed branch only after proving its recorded work is included in main", async () => {
const slug = `autonomy-integrated-${Date.now()}`;
slugs.push(slug);
await createProject({ id: slug, name: "Integration proof", description: "Isolated database regression" });
const project = await prisma.project.findUniqueOrThrow({ where: { slug } });
await prisma.giteaRepositoryLink.create({ data: { projectId: project.id, owner: "AutonomyProof", repository: slug, defaultBranch: "main", linkedBy: "database-test" } });
const delivery = await createFeatureDelivery(slug, { title: "Integrated work", intent: "Repair", problem: "Bug", desiredOutcome: "Fixed", goals: ["fix"], acceptanceCriteria: ["verified"], implementationPlan: ["fix"], validationCommands: ["test"], riskLevel: "low" }, "database-test");
await startFeatureDelivery(slug, delivery.id, "chatgpt/integrated", "database-test");
await recordFeatureDeliveryProgress(slug, delivery.id, { headSha: "a".repeat(40) }, "database-test");
vi.spyOn(GiteaClient.prototype, "getBranch").mockImplementation(async (_owner, _repository, branch) => {
if (branch !== "main") throw new GiteaApiError(404, "GITEA_HTTP_404", "Branch removed");
return { name: "main", commit: { id: "b".repeat(40) } };
});
const compare = vi.spyOn(GiteaClient.prototype, "compareCommits").mockResolvedValueOnce({ total_commits: 1 }).mockResolvedValueOnce({ total_commits: 0 });
const client = new GiteaClient("https://gitea.example.com", "test-token");
expect((await refreshDeliveryFromGitea(client, slug, delivery.id)).spec.status).toBe("executing");
const integrated = await refreshDeliveryFromGitea(client, slug, delivery.id);
expect(integrated.spec.status).toBe("superseded");
expect(integrated.spec.validationEvidence).toBeUndefined();
expect(compare).toHaveBeenLastCalledWith("AutonomyProof", slug, "b".repeat(40), "a".repeat(40));
});
});
@@ -0,0 +1,152 @@
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { loadEnv } from "../src/config/env.js";
import { prisma } from "../src/db/prisma.js";
import { createProject } from "../src/repositories/projectsRepository.js";
import { reconcilePortfolioRepositories } from "../src/services/portfolioControlPlane.js";
import { PROJECT_REPOSITORY_PROVISION_ACTION, provisionProjectRepository } from "../src/services/projectRepositoryProvisioning.js";
const databaseUrl = process.env.TEST_DATABASE_URL;
if (databaseUrl) process.env.DATABASE_URL = databaseUrl;
const databaseDescribe = databaseUrl ? describe : describe.skip;
const owner = "ProjectBrainSyncProof";
const createdProjectSlugs: string[] = [];
const env = () => loadEnv({
NODE_ENV: "test",
LOG_LEVEL: "silent",
DATABASE_URL: databaseUrl,
PROJECTBRAIN_DATA_SOURCE: "database",
PROJECTBRAIN_OWNER_USERNAME: "database-owner",
PROJECTBRAIN_OWNER_PASSWORD: "database-owner-password-value",
PROJECTBRAIN_AUTH_SECRET: "database-test-secret-with-at-least-thirty-two-characters",
PROJECTBRAIN_GITEA_URL: "https://gitea.example.com",
PROJECTBRAIN_GITEA_TOKEN: "database-gitea-token-with-twenty-characters",
PROJECTBRAIN_AUTO_ONBOARD_REPOSITORIES: "true",
PROJECTBRAIN_REPOSITORY_RECONCILE_SECONDS: "60",
});
const repositoryPayload = (name: string) => ({
id: name.length * 100,
name,
full_name: `${owner}/${name}`,
description: `Repository sync proof for ${name}`,
private: true,
archived: false,
default_branch: "main",
html_url: `https://gitea.example.com/${owner}/${name}`,
updated_at: "2026-09-02T00:00:00Z",
owner: { login: owner },
permissions: { admin: true, push: true, pull: true },
});
const removeProofData = async () => {
await prisma.portfolioRepository.deleteMany({ where: { owner } });
if (createdProjectSlugs.length) await prisma.project.deleteMany({ where: { slug: { in: createdProjectSlugs } } });
};
databaseDescribe("bidirectional ProjectBrain and Gitea repository sync", () => {
beforeAll(removeProofData);
afterEach(() => {
vi.unstubAllGlobals();
});
afterAll(async () => {
await removeProofData();
await prisma.$disconnect();
});
it("provisions and links one initialized Gitea repository for a new ProjectBrain project", async () => {
const slug = `pb-to-gitea-${Date.now().toString(36)}`;
createdProjectSlugs.push(slug);
await createProject({
id: slug,
name: "ProjectBrain to Gitea proof",
description: "Verifies durable repository provisioning from a ProjectBrain project.",
primaryGoal: "Create exactly one linked repository",
});
const remote = repositoryPayload(slug);
let repositoryExists = false;
const fetcher = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input instanceof Request ? input.url : input);
if (url.endsWith("/api/v1/user")) return new Response(JSON.stringify({ id: 1, login: owner }), { status: 200, headers: { "content-type": "application/json" } });
if (url.endsWith("/api/v1/user/repos") && init?.method === "POST") {
repositoryExists = true;
return new Response(JSON.stringify(remote), { status: 201, headers: { "content-type": "application/json" } });
}
if (url.endsWith(`/api/v1/repos/${owner}/${slug}`) && repositoryExists) return new Response(JSON.stringify(remote), { status: 200, headers: { "content-type": "application/json" } });
return new Response(JSON.stringify({ message: "not found" }), { status: 404, headers: { "content-type": "application/json" } });
});
vi.stubGlobal("fetch", fetcher);
const first = await provisionProjectRepository(env(), slug, "database:provisioning-proof");
const second = await provisionProjectRepository(env(), slug, "database:provisioning-proof");
expect(first).toMatchObject({ status: "ready", created: true, repository: { fullName: `${owner}/${slug}`, isPrimary: true } });
expect(second).toMatchObject({ status: "ready", created: false, repository: { fullName: `${owner}/${slug}` } });
expect(fetcher.mock.calls.filter(([input, init]) => String(input).endsWith("/api/v1/user/repos") && init?.method === "POST")).toHaveLength(1);
const project = await prisma.project.findUniqueOrThrow({ where: { slug } });
expect(await prisma.giteaRepositoryLink.count({ where: { projectId: project.id } })).toBe(1);
expect(await prisma.externalOperation.count({ where: { projectId: project.id, action: PROJECT_REPOSITORY_PROVISION_ACTION, status: "succeeded" } })).toBe(1);
expect(await prisma.portfolioRepository.findUnique({ where: { owner_repository: { owner, repository: slug } } })).toMatchObject({ projectId: project.id, status: "onboarded" });
}, 15_000);
it("discovers a new Gitea repository and creates its linked ProjectBrain project", async () => {
const name = `gitea-to-pb-${Date.now().toString(36)}`;
createdProjectSlugs.push(name);
const remote = repositoryPayload(name);
vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request) => {
const url = String(input instanceof Request ? input.url : input);
if (url.includes("/api/v1/user/repos")) {
const page = new URL(url).searchParams.get("page");
return new Response(JSON.stringify(page === "1" ? [remote] : []), { status: 200, headers: { "content-type": "application/json" } });
}
if (url.endsWith(`/api/v1/repos/${owner}/${name}`)) return new Response(JSON.stringify(remote), { status: 200, headers: { "content-type": "application/json" } });
return new Response(JSON.stringify({ message: "not found" }), { status: 404, headers: { "content-type": "application/json" } });
}));
const reconciliation = await reconcilePortfolioRepositories(env(), "database:discovery-proof", {
dryRun: false,
repositoryFullNames: [`${owner}/${name}`],
});
expect(reconciliation).toMatchObject({ candidateCount: 1, onboardedCount: 1, failedCount: 0 });
const project = await prisma.project.findUniqueOrThrow({ where: { slug: name } });
expect(await prisma.giteaRepositoryLink.findUnique({ where: { owner_repository: { owner, repository: name } } })).toMatchObject({ projectId: project.id, syncStatus: "ready" });
expect(await prisma.portfolioRepository.findUnique({ where: { owner_repository: { owner, repository: name } } })).toMatchObject({ projectId: project.id, status: "onboarded" });
}, 15_000);
it("resumes an ambiguous create without issuing a duplicate repository mutation", async () => {
const slug = `pb-retry-${Date.now().toString(36)}`;
createdProjectSlugs.push(slug);
await createProject({
id: slug,
name: "ProjectBrain repository retry proof",
description: "Verifies that an uncertain Gitea create is resumed without a duplicate repository.",
primaryGoal: "Resume one durable provisioning operation",
});
const remote = repositoryPayload(slug);
let repositoryExists = false;
const fetcher = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input instanceof Request ? input.url : input);
if (url.endsWith("/api/v1/user")) return new Response(JSON.stringify({ id: 1, login: owner }), { status: 200, headers: { "content-type": "application/json" } });
if (url.endsWith("/api/v1/user/repos") && init?.method === "POST") {
repositoryExists = true;
return new Response(JSON.stringify({ message: "upstream result unknown" }), { status: 503, headers: { "content-type": "application/json" } });
}
if (url.endsWith(`/api/v1/repos/${owner}/${slug}`) && repositoryExists) return new Response(JSON.stringify(remote), { status: 200, headers: { "content-type": "application/json" } });
return new Response(JSON.stringify({ message: "not found" }), { status: 404, headers: { "content-type": "application/json" } });
});
vi.stubGlobal("fetch", fetcher);
await expect(provisionProjectRepository(env(), slug, "database:retry-proof")).rejects.toMatchObject({ status: 503, code: "GITEA_HTTP_503" });
const resumed = await provisionProjectRepository(env(), slug, "database:retry-proof");
expect(resumed).toMatchObject({ status: "ready", created: false, repository: { fullName: `${owner}/${slug}` } });
expect(fetcher.mock.calls.filter(([input, init]) => String(input).endsWith("/api/v1/user/repos") && init?.method === "POST")).toHaveLength(1);
const project = await prisma.project.findUniqueOrThrow({ where: { slug } });
expect(await prisma.externalOperation.count({ where: { projectId: project.id, action: PROJECT_REPOSITORY_PROVISION_ACTION, status: "failed" } })).toBe(1);
expect(await prisma.externalOperation.count({ where: { projectId: project.id, action: PROJECT_REPOSITORY_PROVISION_ACTION, status: "succeeded" } })).toBe(1);
expect(await prisma.giteaRepositoryLink.count({ where: { projectId: project.id } })).toBe(1);
}, 15_000);
});
@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { findReusableDeliveryPullRequest } from "../src/integrations/gitea/policy.js";
import type { GiteaPullRequest } from "../src/integrations/gitea/types.js";
const head = "a".repeat(40);
const merged: GiteaPullRequest = {
id: 31, number: 31, title: "M18 evidence", state: "closed", merged: true,
html_url: "https://example.test/pulls/31", created_at: "2026-09-07T00:00:00Z", updated_at: "2026-09-08T00:00:00Z",
merge_commit_sha: "b".repeat(40), head: { ref: "chatgpt/evidence", sha: head }, base: { ref: "main", sha: "b".repeat(40) },
};
const select = (prs: GiteaPullRequest[]) => findReusableDeliveryPullRequest(prs, "chatgpt/evidence", "main", head);
describe("existing delivery PR reconciliation", () => {
it("adopts the exact already-merged PR instead of opening an empty replacement", () => {
expect(select([merged])).toBe(merged);
});
it("reuses an exact open review", () => {
const open = { ...merged, merged: false, state: "open", merge_commit_sha: undefined };
expect(select([open])).toBe(open);
});
it.each([
{ ...merged, head: { ...merged.head, sha: "c".repeat(40) } },
{ ...merged, head: { ...merged.head, ref: "chatgpt/other" } },
{ ...merged, base: { ...merged.base, ref: "other" } },
{ ...merged, merged: false },
])("does not adopt stale, foreign or abandoned reviews", (pr) => {
expect(select([pr])).toBeUndefined();
});
it("rejects ambiguous matching reviews", () => {
expect(() => select([merged, { ...merged, id: 32, number: 32 }])).toThrow("AMBIGUOUS");
});
it("requires actual merge commit evidence", () => {
expect(() => select([{ ...merged, merge_commit_sha: undefined }])).toThrow("MERGE_EVIDENCE_REQUIRED");
});
});
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { assertFeatureDeliveryTransition, canTransitionFeatureDeliveryStatus } from "../src/repositories/featureDeliveryRepository.js";
describe("feature delivery lifecycle invariants", () => {
it("allows only forward operational progress and explicit repair paths", () => {
expect(canTransitionFeatureDeliveryStatus("executing", "validating")).toBe(true);
expect(canTransitionFeatureDeliveryStatus("pull_request_open", "merged")).toBe(true);
expect(canTransitionFeatureDeliveryStatus("merged", "verified")).toBe(true);
expect(canTransitionFeatureDeliveryStatus("verified", "released")).toBe(true);
expect(canTransitionFeatureDeliveryStatus("released", "executing")).toBe(false);
expect(canTransitionFeatureDeliveryStatus("verified", "awaiting_approval")).toBe(false);
expect(canTransitionFeatureDeliveryStatus("superseded", "approved")).toBe(false);
});
it("rejects a transition that would reactivate superseded or released work", () => {
expect(() => assertFeatureDeliveryTransition("superseded", "executing")).toThrow("FEATURE_DELIVERY_INVALID_TRANSITION");
expect(() => assertFeatureDeliveryTransition("released", "approved")).toThrow("FEATURE_DELIVERY_INVALID_TRANSITION");
});
});
+101
View File
@@ -0,0 +1,101 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { exportJWK, generateKeyPair, SignJWT } from "jose";
const mocks = vi.hoisted(() => ({
oidcLoginAttempt: { deleteMany: vi.fn(), create: vi.fn(), findUnique: vi.fn(), delete: vi.fn() },
externalIdentity: { findUnique: vi.fn(), update: vi.fn(), upsert: vi.fn(), deleteMany: vi.fn() },
user: { update: vi.fn(), findUniqueOrThrow: vi.fn() },
transaction: vi.fn(async (input: unknown) => Array.isArray(input) ? Promise.all(input) : undefined),
}));
vi.mock("../src/db/prisma.js", () => ({ prisma: { oidcLoginAttempt: mocks.oidcLoginAttempt, externalIdentity: mocks.externalIdentity, user: mocks.user, $transaction: mocks.transaction } }));
const { oidcLoginAttempt, externalIdentity, user } = mocks;
import { loadEnv } from "../src/config/env.js";
import { consumeEntraAttempt, createEntraAuthorization, exchangeEntraCode, getEntraLinkStatus, isEntraConfigured, linkEntraIdentity, resolveEntraUser, unlinkEntraIdentity } from "../src/security/entraOidc.js";
const tenantId = "11111111-1111-4111-8111-111111111111";
const clientId = "22222222-2222-4222-8222-222222222222";
const baseInput = {
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_PUBLIC_URL: "https://projectbrain.example.com",
PROJECTBRAIN_ENTRA_TENANT_ID: tenantId, PROJECTBRAIN_ENTRA_CLIENT_ID: clientId, PROJECTBRAIN_ENTRA_CLIENT_SECRET: "unit-test-client-secret",
};
const env = () => loadEnv(baseInput);
const profile = { tenantId, subject: "33333333-3333-4333-8333-333333333333", displayName: "Entra Owner", preferredUsername: "owner@example.com" };
const storedUser = { id: "owner-id", username: "owner", role: "owner" as const, credentialVersion: 1, disabledAt: null };
beforeEach(() => {
vi.clearAllMocks();
oidcLoginAttempt.deleteMany.mockResolvedValue({ count: 0 });
oidcLoginAttempt.create.mockResolvedValue({ id: "attempt-id" });
oidcLoginAttempt.delete.mockResolvedValue({ id: "attempt-id" });
externalIdentity.update.mockResolvedValue({ id: "identity-id" });
externalIdentity.upsert.mockResolvedValue({ id: "identity-id" });
externalIdentity.deleteMany.mockResolvedValue({ count: 1 });
user.update.mockResolvedValue(storedUser);
user.findUniqueOrThrow.mockResolvedValue(storedUser);
});
describe("Microsoft Entra OIDC service", () => {
it("creates a tenant-bound authorization URL with PKCE and stores only hashed state", async () => {
expect(isEntraConfigured(env())).toBe(true);
expect(isEntraConfigured(loadEnv({ ...baseInput, PROJECTBRAIN_ENTRA_TENANT_ID: undefined, PROJECTBRAIN_ENTRA_CLIENT_ID: undefined, PROJECTBRAIN_ENTRA_CLIENT_SECRET: undefined }))).toBe(false);
const authorization = new URL(await createEntraAuthorization(env(), { purpose: "login", returnTo: "//evil.example/path" }));
expect(authorization.hostname).toBe("login.microsoftonline.com");
expect(authorization.pathname).toContain(tenantId);
expect(authorization.searchParams.get("code_challenge_method")).toBe("S256");
const rawState = authorization.searchParams.get("state")!;
const stored = oidcLoginAttempt.create.mock.calls[0][0].data;
expect(stored.stateHash).not.toBe(rawState);
expect(stored.stateHash).toMatch(/^[a-f0-9]{64}$/);
expect(stored.returnTo).toBe("/");
await expect(createEntraAuthorization(env(), { purpose: "link" })).rejects.toThrow("ENTRA_LINK_USER_REQUIRED");
});
it("consumes valid state exactly once and rejects missing or expired attempts", async () => {
oidcLoginAttempt.findUnique.mockResolvedValueOnce(null);
expect(await consumeEntraAttempt(env(), "missing-state")).toBeUndefined();
oidcLoginAttempt.findUnique.mockResolvedValueOnce({ id: "expired", provider: "microsoft-entra", purpose: "login", userId: null, codeVerifier: "verifier", nonce: "nonce", returnTo: "/settings", expiresAt: new Date(Date.now() - 1_000) });
expect(await consumeEntraAttempt(env(), "expired-state")).toBeUndefined();
oidcLoginAttempt.findUnique.mockResolvedValueOnce({ id: "valid", provider: "microsoft-entra", purpose: "link", userId: "owner-id", codeVerifier: "verifier", nonce: "nonce", returnTo: "/settings", expiresAt: new Date(Date.now() + 60_000) });
await expect(consumeEntraAttempt(env(), "valid-state")).resolves.toEqual({ purpose: "link", userId: "owner-id", codeVerifier: "verifier", nonce: "nonce", returnTo: "/settings" });
expect(oidcLoginAttempt.delete).toHaveBeenCalledTimes(2);
});
it("validates Microsoft signature, issuer, audience, tenant, nonce and oid", async () => {
const { publicKey, privateKey } = await generateKeyPair("RS256");
const publicJwk = { ...await exportJWK(publicKey), kid: "unit-key", alg: "RS256", use: "sig" };
const sign = (nonce: string, oid = profile.subject) => new SignJWT({ tid: tenantId, oid, nonce, name: profile.displayName, preferred_username: profile.preferredUsername })
.setProtectedHeader({ alg: "RS256", kid: "unit-key" }).setIssuer(`https://login.microsoftonline.com/${tenantId}/v2.0`).setAudience(clientId).setIssuedAt().setExpirationTime("5m").sign(privateKey);
const idToken = await sign("expected-nonce");
vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request) => {
const url = String(input instanceof Request ? input.url : input);
return url.endsWith("/token")
? new Response(JSON.stringify({ id_token: idToken }), { status: 200, headers: { "content-type": "application/json" } })
: new Response(JSON.stringify({ keys: [publicJwk] }), { status: 200, headers: { "content-type": "application/json" } });
}));
try {
await expect(exchangeEntraCode(env(), "code", { purpose: "login", codeVerifier: "verifier", nonce: "expected-nonce", returnTo: "/" })).resolves.toEqual(profile);
await expect(exchangeEntraCode(env(), "code", { purpose: "login", codeVerifier: "verifier", nonce: "wrong-nonce", returnTo: "/" })).rejects.toThrow("ENTRA_NONCE_INVALID");
} finally {
vi.unstubAllGlobals();
}
}, 15_000);
it("resolves, links, reports and unlinks only durable mapped identities", async () => {
externalIdentity.findUnique.mockResolvedValueOnce(null);
expect(await resolveEntraUser(profile)).toBeUndefined();
externalIdentity.findUnique.mockResolvedValueOnce({ id: "identity-id", userId: storedUser.id, user: storedUser });
await expect(resolveEntraUser(profile)).resolves.toEqual({ id: storedUser.id, username: "owner", role: "owner", credentialVersion: 1 });
externalIdentity.findUnique.mockResolvedValueOnce({ id: "foreign", userId: "other-user" });
await expect(linkEntraIdentity(storedUser.id, profile)).rejects.toThrow("ENTRA_IDENTITY_ALREADY_LINKED");
externalIdentity.findUnique.mockResolvedValueOnce(null);
await expect(linkEntraIdentity(storedUser.id, profile)).resolves.toMatchObject({ id: storedUser.id });
externalIdentity.findUnique.mockResolvedValueOnce({ displayName: profile.displayName, preferredUsername: profile.preferredUsername });
await expect(getEntraLinkStatus(storedUser.id)).resolves.toEqual({ linked: true, displayName: profile.displayName, preferredUsername: profile.preferredUsername });
await unlinkEntraIdentity(storedUser.id);
expect(externalIdentity.deleteMany).toHaveBeenCalledWith({ where: { provider: "microsoft-entra", userId: storedUser.id } });
});
});
@@ -0,0 +1,182 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ApprovalState, DeliveryStatus, DeliveryValidationStatus } from "@prisma/client";
const mocks = vi.hoisted(() => {
const tx = {
deliveryPlan: { create: vi.fn(), update: vi.fn() },
initiative: { update: vi.fn() },
outboxEvent: { create: vi.fn() },
deliveryRun: { create: vi.fn(), update: vi.fn() },
deliveryValidation: { create: vi.fn(), deleteMany: vi.fn() },
findingInvestigation: { findUnique: vi.fn(), update: vi.fn() },
repositoryFinding: { update: vi.fn() },
};
const prisma = {
project: { findFirst: vi.fn() },
initiative: { findFirstOrThrow: vi.fn() },
deliveryPlan: { findFirst: vi.fn(), findMany: vi.fn() },
giteaRepositoryLink: { findFirst: vi.fn() },
deliveryRun: { findFirst: vi.fn() },
$transaction: vi.fn(async (callback: (client: typeof tx) => unknown) => callback(tx)),
};
return {
prisma,
tx,
createInitiativeForProject: vi.fn(),
};
});
vi.mock("../src/db/prisma.js", () => ({ prisma: mocks.prisma }));
vi.mock("../src/repositories/initiativesRepository.js", () => ({ createInitiativeForProject: mocks.createInitiativeForProject }));
import { createFeatureDelivery, startFeatureDelivery, recordFeatureDeliveryActionValidation, reportedValidationResult, type FeatureDeliveryInput } from "../src/repositories/featureDeliveryRepository.js";
const baseInput = (riskLevel: FeatureDeliveryInput["riskLevel"]): FeatureDeliveryInput => ({
title: "Intent-first onboarding",
intent: "Make onboarding understandable for nontechnical users.",
priority: "normal",
problem: "The current onboarding exposes too many technical concepts.",
desiredOutcome: "A short intent-first onboarding flow.",
users: ["new users"],
goals: ["reduce cognitive load"],
nonGoals: ["remove technical capabilities"],
acceptanceCriteria: ["normal users can create a project without understanding MCP or Git internals"],
implementationPlan: ["simplify the primary flow", "preserve technical drill-downs"],
risks: ["navigation changes can affect deep links"],
validationCommands: ["pnpm test"],
riskLevel,
});
const plan = (approvalState: ApprovalState, status: DeliveryStatus) => ({
id: "delivery-db-1",
slug: "delivery-1",
projectId: "project-db-1",
initiativeId: "initiative-db-1",
title: "Intent-first onboarding",
intent: "Make onboarding understandable for nontechnical users.",
priority: "normal",
problem: "The current onboarding exposes too many technical concepts.",
desiredOutcome: "A short intent-first onboarding flow.",
usersJson: ["new users"],
goalsJson: ["reduce cognitive load"],
nonGoalsJson: [],
acceptanceCriteriaJson: ["normal users understand the flow"],
implementationPlanJson: ["simplify the primary flow"],
risksJson: ["navigation changes"],
validationCommandsJson: ["pnpm test"],
status,
approvalState,
approvedAt: approvalState === ApprovalState.approved ? new Date("2026-08-31T20:00:00.000Z") : null,
approvedBy: approvalState === ApprovalState.approved ? "chatgpt" : null,
reviewFeedback: null,
version: 1,
createdBy: "chatgpt",
updatedBy: "chatgpt",
createdAt: new Date("2026-08-31T20:00:00.000Z"),
updatedAt: new Date("2026-08-31T20:00:00.000Z"),
initiative: null,
runs: [],
});
beforeEach(() => {
vi.clearAllMocks();
mocks.prisma.project.findFirst.mockResolvedValue({ id: "project-db-1", slug: "projectbrain" });
mocks.createInitiativeForProject.mockResolvedValue({ id: "initiative-1" });
mocks.prisma.initiative.findFirstOrThrow.mockResolvedValue({ id: "initiative-db-1", slug: "initiative-1" });
mocks.tx.initiative.update.mockResolvedValue({});
mocks.tx.outboxEvent.create.mockResolvedValue({});
mocks.tx.findingInvestigation.findUnique.mockResolvedValue(null);
});
describe("feature delivery risk authorization", () => {
it.each([
["low", "AUTO_ALLOWED"],
["medium", "AUTO_ALLOWED_WITH_AUDIT"],
] as const)("auto-authorizes %s reversible work", async (riskLevel, policyDecision) => {
const created = plan(ApprovalState.approved, DeliveryStatus.approved);
mocks.tx.deliveryPlan.create.mockResolvedValue(created);
const result = await createFeatureDelivery("projectbrain", baseInput(riskLevel), "chatgpt");
expect(result.approvalState).toBe(ApprovalState.approved);
expect(result.spec.status).toBe(DeliveryStatus.approved);
expect(mocks.tx.deliveryPlan.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
approvalState: ApprovalState.approved,
status: DeliveryStatus.approved,
}),
}));
expect(mocks.tx.outboxEvent.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
eventType: "delivery.plan.auto_authorized",
payloadJson: expect.objectContaining({ riskLevel, policyDecision }),
}),
}));
});
it.each(["high", "destructive"] as const)("requires a human decision for %s work", async (riskLevel) => {
const created = plan(ApprovalState.pending, DeliveryStatus.awaiting_approval);
mocks.tx.deliveryPlan.create.mockResolvedValue(created);
const result = await createFeatureDelivery("projectbrain", baseInput(riskLevel), "chatgpt");
expect(result.approvalState).toBe(ApprovalState.pending);
expect(result.spec.status).toBe(DeliveryStatus.awaiting_approval);
expect(mocks.tx.deliveryPlan.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
approvalState: ApprovalState.pending,
status: DeliveryStatus.awaiting_approval,
approvedAt: null,
approvedBy: null,
}),
}));
expect(mocks.tx.outboxEvent.create).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
eventType: "delivery.plan.user_decision_required",
payloadJson: expect.objectContaining({ riskLevel, policyDecision: "USER_DECISION_REQUIRED" }),
}),
}));
});
it("fails closed when a pending high-risk delivery tries to start repository execution", async () => {
mocks.prisma.deliveryPlan.findFirst.mockResolvedValue(plan(ApprovalState.pending, DeliveryStatus.awaiting_approval));
await expect(startFeatureDelivery("projectbrain", "delivery-1", "chatgpt/high-risk-change"))
.rejects.toThrow("FEATURE_DELIVERY_APPROVAL_REQUIRED");
expect(mocks.prisma.giteaRepositoryLink.findFirst).not.toHaveBeenCalled();
expect(mocks.tx.deliveryRun.create).not.toHaveBeenCalled();
});
});
describe("Actions evidence scope", () => {
it("records only the observed workflow, preserving separately reported checks", async () => {
const current = { ...plan(ApprovalState.approved, DeliveryStatus.validating),
validationCommandsJson: ["dotnet test", "CI / build-test (pull_request)", "Live disposable provider smoke"],
runs: [{ id: "run-1", validations: [], changes: [] }] };
mocks.prisma.deliveryPlan.findFirst.mockResolvedValue(current);
mocks.tx.deliveryPlan.update.mockResolvedValue(current);
await recordFeatureDeliveryActionValidation("projectbrain", "delivery-1", {
provider: "gitea_actions", runId: 9205, runNumber: 500, url: "https://example.test/actions/runs/9205",
workflowPath: "managed-validation.yml@refs/heads/chatgpt/work", headSha: "a".repeat(40),
conclusion: "success", verifiedAt: "2026-09-08T00:00:00Z",
});
expect(mocks.tx.deliveryValidation.create).toHaveBeenCalledTimes(1);
expect(mocks.tx.deliveryValidation.create).toHaveBeenCalledWith({ data: expect.objectContaining({
command: "Gitea Actions: managed-validation.yml@refs/heads/chatgpt/work", status: "passed", headSha: "a".repeat(40),
}) });
expect(mocks.tx.deliveryValidation.deleteMany).toHaveBeenCalledWith({ where: { deliveryRunId: "run-1", provider: "gitea_actions" } });
});
it.each(["CI / build-test (pull_request)", "Live provider smoke", "dotnet test"])("does not present legacy inferred %s as execution", (command) => {
expect(reportedValidationResult({ command, status: DeliveryValidationStatus.passed, provider: "gitea_actions", summary: "managed-validation.yml" }))
.toEqual({ command, status: "not_run", summary: expect.stringContaining("Legacy inferred result") });
});
it("preserves explicit reports and directly observed workflow results", () => {
expect(reportedValidationResult({ command: "Live provider smoke", status: DeliveryValidationStatus.skipped, provider: null, summary: "Credentials unavailable" }).status).toBe("not_run");
expect(reportedValidationResult({ command: "Local contract suite", status: DeliveryValidationStatus.failed, provider: null, summary: "Failed" }).status).toBe("failed");
expect(reportedValidationResult({ command: "CI / build-test (pull_request)", status: DeliveryValidationStatus.failed, provider: "gitea_actions", summary: "ci.yml" }).status).toBe("failed");
expect(reportedValidationResult({ command: "Gitea Actions: ci.yml", status: DeliveryValidationStatus.passed, provider: "gitea_actions", summary: "ci.yml" }).status).toBe("passed");
});
});
+20
View File
@@ -0,0 +1,20 @@
import { describe, expect, it, vi } from "vitest";
import { assertDeliveryWriteAllowed, canRepairFeatureDelivery } from "../src/services/featureDeliveryFlow.js";
import type { FeatureDeliveryDto } from "../src/repositories/featureDeliveryRepository.js";
import type { GiteaClient } from "../src/integrations/gitea/client.js";
describe("repairing a delivery after PR validation fails", () => {
const repository = { owner: "owner", repository: "repo", defaultBranch: "main" };
const delivery = { approvalState: "approved", spec: { status: "pull_request_open", branch: "chatgpt/existing", pullRequest: { number: 12 } } } as FeatureDeliveryDto;
it("allows repair on the existing open PR, while terminal deliveries stay immutable", async () => {
const getPullRequest = vi.fn().mockResolvedValue({ state: "open", merged: false, head: { ref: "chatgpt/existing" }, base: { ref: "main" } });
await expect(assertDeliveryWriteAllowed(delivery, "chatgpt/existing", { getPullRequest } as unknown as GiteaClient, repository)).resolves.toBeUndefined();
expect(getPullRequest).toHaveBeenCalledWith("owner", "repo", 12);
expect(canRepairFeatureDelivery("merged")).toBe(false);
expect(canRepairFeatureDelivery("failed")).toBe(false);
});
it("refuses writes if Gitea merged the PR before ProjectBrain saw its webhook", async () => {
const getPullRequest = vi.fn().mockResolvedValue({ state: "closed", merged: true });
await expect(assertDeliveryWriteAllowed(delivery, "chatgpt/existing", { getPullRequest } as unknown as GiteaClient, repository)).rejects.toThrow("FEATURE_DELIVERY_PULL_REQUEST_NOT_WRITABLE");
});
});
+256
View File
@@ -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" }),
}));
});
});
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import { hasGiteaAutonomyCompletionEvidence } from "../src/repositories/autonomyRepository.js";
import { autonomyGiteaBranch, isGiteaAutoExecutionBranch } from "../src/services/giteaAutonomyExecution.js";
describe("Gitea Auto execution contract", () => {
it("derives one deterministic atomic-claim branch from a durable work id", () => {
expect(autonomyGiteaBranch("PHX-003")).toBe("projectbrain/auto/phx-003");
expect(autonomyGiteaBranch(" UI / Polish #4 ")).toBe("projectbrain/auto/ui-polish-4");
expect(isGiteaAutoExecutionBranch("projectbrain/auto/phx-003")).toBe(true);
expect(isGiteaAutoExecutionBranch("chatgpt/other-work")).toBe(false);
});
it("requires merge, exact head and validation evidence before Gitea completion is authoritative", () => {
expect(hasGiteaAutonomyCompletionEvidence([
"gitea:auto:merge:abc",
"gitea:auto:head:def",
])).toBe(false);
expect(hasGiteaAutonomyCompletionEvidence([
"gitea:auto:merge:abc",
"gitea:auto:head:def",
"gitea:auto:validation:42",
])).toBe(true);
});
});
+519
View File
@@ -0,0 +1,519 @@
import { describe, expect, it, vi } from "vitest";
import { loadEnv } from "../src/config/env.js";
import { GiteaClient, GiteaApiError } from "../src/integrations/gitea/client.js";
import { assertChatGptBranch, assertExpectedFileSha, assertFileSize, assertProjectBrainBranch, assertReadablePath, assertSuccessfulActionRuns, normalizeRepositoryPath } from "../src/integrations/gitea/policy.js";
import type { GiteaActionRun } from "../src/integrations/gitea/types.js";
import { exchangeAuthorizationCode, exchangeRefreshToken, issueAuthorizationCode, registerOAuthClient, verifyMcpAccessToken } from "../src/security/mcpOAuth.js";
import { createHash, createHmac } from "node:crypto";
import { hasGiteaAdminAccess, verifyGiteaWebhookSignature } from "../src/routes/giteaIntegration.js";
import { assertFeatureDeliveryActionEvidence, assertFeatureDeliveryPlanComplete, canSyncFeatureDeliveryValidation, statusAfterFeatureDeliveryValidationSync } from "../src/repositories/featureDeliveryRepository.js";
import { augmentProjectBrainInitialize, handleProjectBrainSkillMethod, projectBrainSkills } from "../src/mcp/skills.js";
import { isMcpHubServiceToken, mcpHubActor } from "../src/security/mcpServiceAuth.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createProjectBrainMcpServer } from "../src/mcp/server.js";
import { PROJECTBRAIN_WORKSPACE_MIME_TYPE, PROJECTBRAIN_WORKSPACE_URI } from "../src/mcp/widget.js";
import { PROJECTBRAIN_ANALYSIS_WIDGET_MIME_TYPE, PROJECTBRAIN_ANALYSIS_WIDGET_URI } from "../src/mcp/analysisWidget.js";
import { findingIssueMarker, findingRiskScore, renderFindingEvidence, repositoryFindingFingerprint } from "../src/repositories/findingRepository.js";
import { canReuseProjectForRepository, isPortfolioManagedPolicy, resolvePortfolioRepositoryStatus } from "../src/services/portfolioControlPlane.js";
import { PortfolioRepositoryStatus } from "@prisma/client";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { PROJECTBRAIN_GITEA_WEBHOOK_EVENTS } from "../src/integrations/gitea/service.js";
import { existingRepositoryFromProjectSetup, giteaRepositoryNameCandidate, giteaRepositoryNameForProject, projectSlugFromInput } from "../src/services/projectRepositoryProvisioning.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_MCP_ENABLED: "true", PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com",
PROJECTBRAIN_GITEA_URL: "https://gitea.example.com", PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters",
});
describe("Gitea repository policy", () => {
it("accepts 58/200 analysis selections, rejects 201, and keeps reconciliation bounded to 50", async () => {
const registration = vi.spyOn(McpServer.prototype, "registerTool");
const server = createProjectBrainMcpServer(env, "analysis-selection-contract-test");
try {
const analysis = registration.mock.calls.find(([name]) => name === "start_repository_analysis")![1];
const schema = z.object(analysis.inputSchema as z.ZodRawShape);
for (const field of ["project_ids", "repository_ids"]) {
for (const count of [58, 200, 201]) {
const values = Array.from({ length: count }, (_, index) => `repository-${index}`);
expect(schema.safeParse({ [field]: values }).success).toBe(count <= 200);
}
}
const reconcile = registration.mock.calls.find(([name]) => name === "reconcile_portfolio_repositories")![1];
const reconcileSchema = z.object(reconcile.inputSchema as z.ZodRawShape);
expect(reconcileSchema.safeParse({ repository_full_names: Array.from({ length: 50 }, (_, index) => `Owner/repo-${index}`) }).success).toBe(true);
expect(reconcileSchema.safeParse({ repository_full_names: Array.from({ length: 51 }, (_, index) => `Owner/repo-${index}`) }).success).toBe(false);
} finally {
registration.mockRestore();
await server.close();
}
});
it("only realigns policies that still belong to the portfolio control plane", () => {
expect(isPortfolioManagedPolicy()).toBe(true);
expect(isPortfolioManagedPolicy("system")).toBe(true);
expect(isPortfolioManagedPolicy("system:portfolio-control-plane")).toBe(true);
expect(isPortfolioManagedPolicy("web:owner")).toBe(false);
});
it("normalizes safe paths and blocks traversal, Git internals, and secrets", () => {
expect(normalizeRepositoryPath("/src\\app.ts")).toBe("src/app.ts");
expect(() => normalizeRepositoryPath("../outside.txt")).toThrow("REPOSITORY_PATH_FORBIDDEN");
expect(() => normalizeRepositoryPath(".git/config")).toThrow("REPOSITORY_PATH_FORBIDDEN");
expect(() => assertReadablePath(".env")).toThrow("SENSITIVE_PATH_FORBIDDEN");
expect(assertReadablePath(".env.example")).toBe(".env.example");
});
it("allows any bounded non-default work branch and files", () => {
expect(assertChatGptBranch("chatgpt/add-search", "main")).toBe("chatgpt/add-search");
expect(assertChatGptBranch("projectbrain/continue-in-web", "main")).toBe("projectbrain/continue-in-web");
expect(assertChatGptBranch("m15-backup-restore-dr", "main")).toBe("m15-backup-restore-dr");
expect(assertChatGptBranch("feature/normal", "main")).toBe("feature/normal");
expect(() => assertChatGptBranch("main", "main")).toThrow("DEFAULT_BRANCH_WRITE_FORBIDDEN");
expect(() => assertFileSize("x".repeat(512 * 1024 + 1))).toThrow("MCP_FILE_TOO_LARGE");
});
it("keeps ProjectBrain workbench writes on non-default branches", () => {
expect(assertProjectBrainBranch("projectbrain/add-project-dashboard", "main")).toBe("projectbrain/add-project-dashboard");
expect(assertProjectBrainBranch("chatgpt/continue-in-workbench", "main")).toBe("chatgpt/continue-in-workbench");
expect(assertProjectBrainBranch("feature/normal", "main")).toBe("feature/normal");
expect(() => assertProjectBrainBranch("main", "main")).toThrow("DEFAULT_BRANCH_WRITE_FORBIDDEN");
expect(() => assertProjectBrainBranch("projectbrain/../main", "main")).toThrow("PROJECTBRAIN_BRANCH_FORBIDDEN");
});
it("rejects stale updates and create-versus-update races", () => {
expect(() => assertExpectedFileSha("sha-opened", "sha-current")).toThrow("GITEA_FILE_CONFLICT");
expect(() => assertExpectedFileSha(undefined, "sha-created-elsewhere")).toThrow("GITEA_FILE_CONFLICT");
expect(() => assertExpectedFileSha("sha-deleted", undefined)).toThrow("GITEA_FILE_CONFLICT");
expect(assertExpectedFileSha("sha-current", "sha-current")).toBeUndefined();
expect(assertExpectedFileSha(undefined, undefined)).toBeUndefined();
});
});
describe("Gitea webhook verification", () => {
it("subscribes linked repositories to push-driven analysis and closed-loop events", () => {
expect(PROJECTBRAIN_GITEA_WEBHOOK_EVENTS).toEqual(["push", "pull_request", "issues", "issue_comment"]);
});
it("accepts only the HMAC-SHA256 signature of the untouched body", () => {
const body = Buffer.from('{"action":"closed","number":7}'); const secret = "a-webhook-secret-with-enough-entropy";
const signature = createHmac("sha256", secret).update(body).digest("hex");
expect(verifyGiteaWebhookSignature(body, signature, secret)).toBe(true);
expect(verifyGiteaWebhookSignature(Buffer.from(`${body.toString()} `), signature, secret)).toBe(false);
expect(verifyGiteaWebhookSignature(body, "invalid", secret)).toBe(false);
});
});
describe("Gitea API client", () => {
it("accepts Gitea's empty HTTP 200 merge response without inventing a failed merge", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) => String(input).endsWith("/version")
? new Response(JSON.stringify({ version: "1.27.3" }), { status: 200 })
: new Response(null, { status: 200 }));
const client = new GiteaClient("https://gitea.example.com", "test-token", fetcher as typeof fetch);
await expect(client.mergePullRequest("Owner", "Repo", 7, { method: "merge", headCommitId: "a".repeat(40) })).resolves.toBeUndefined();
expect(fetcher).toHaveBeenCalledTimes(2);
});
it("creates a private initialized repository through the bounded user endpoint", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({
id: 17,
name: "new-project",
full_name: "Acme/new-project",
private: true,
archived: false,
default_branch: "main",
html_url: "https://gitea.example.com/Acme/new-project",
updated_at: "2026-09-02T00:00:00Z",
owner: { login: "Acme" },
permissions: { admin: true, push: true, pull: true },
}), { status: 201, headers: { "content-type": "application/json" } }));
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
const repository = await client.createRepository({
name: "new-project",
description: "Created by ProjectBrain",
private: true,
});
expect(repository.full_name).toBe("Acme/new-project");
expect(String(fetcher.mock.calls[0][0])).toBe("https://gitea.example.com/api/v1/user/repos");
expect(fetcher.mock.calls[0][1]?.method).toBe("POST");
expect(fetcher.mock.calls[0][1]?.body).toBe(JSON.stringify({
name: "new-project",
description: "Created by ProjectBrain",
private: true,
auto_init: true,
default_branch: "main",
}));
});
it("uses token authentication and encodes repository paths", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({ type: "file", path: "src/a b.ts", sha: "abc", size: 1, encoding: "base64", content: "eA==" }), { status: 200, headers: { "content-type": "application/json" } }));
const client = new GiteaClient("https://gitea.example.com/", "secret-token", fetcher as typeof fetch);
await client.getContents("Acme", "Repo", "src/a b.ts", "main");
const [url, init] = fetcher.mock.calls[0];
expect(String(url)).toContain("/api/v1/repos/Acme/Repo/contents/src/a%20b.ts?ref=main");
expect((init?.headers as Record<string, string>).Authorization).toBe("token secret-token");
});
it("maps remote failures without exposing the configured token", async () => {
const client = new GiteaClient("https://gitea.example.com", "very-secret-token", (async () => new Response(JSON.stringify({ message: "not found" }), { status: 404 })) as typeof fetch);
await expect(client.getRepository("owner", "missing")).rejects.toMatchObject<GiteaApiError>({ status: 404, code: "GITEA_HTTP_404", message: "not found" });
});
it("paginates the complete visible repository portfolio", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) => {
const page = new URL(String(input)).searchParams.get("page");
const count = page === "1" ? 100 : page === "2" ? 3 : 0;
return new Response(JSON.stringify(Array.from({ length: count }, (_, index) => ({
id: Number(page) * 1000 + index, name: `repo-${page}-${index}`, full_name: `Acme/repo-${page}-${index}`,
private: true, archived: false, default_branch: "main", html_url: "https://gitea.example/repo",
updated_at: "2026-08-26T00:00:00Z", owner: { login: "Acme" }, permissions: { push: true },
}))), { status: 200, headers: { "content-type": "application/json" } });
});
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
expect(await client.listRepositories()).toHaveLength(103);
expect(fetcher).toHaveBeenCalledTimes(3);
});
it("continues past a server-clamped 50-item page and never follows foreign Link URLs", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = new URL(String(input));
expect(url.origin).toBe("https://gitea.example.com");
expect(url.pathname).toBe("/api/v1/user/repos");
expect(url.searchParams.get("limit")).toBe("100");
const page = Number(url.searchParams.get("page"));
const count = page === 1 ? 50 : page === 2 ? 8 : 0;
return new Response(JSON.stringify(Array.from({ length: count }, (_, index) => ({ id: (page - 1) * 50 + index + 1 }))), {
headers: { "Link": '<https://untrusted.invalid/steal>; rel="next"', "X-Total-Count": "58" },
});
});
expect(await new GiteaClient("https://gitea.example.com", "fixture-token", fetcher as typeof fetch).listRepositories()).toHaveLength(58);
expect(fetcher).toHaveBeenCalledTimes(3);
});
it("rejects repeated pages rather than returning a partial inventory", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify([{ id: 1 }])));
await expect(new GiteaClient("https://gitea.example.com", "fixture-token", fetcher as typeof fetch).listRepositories())
.rejects.toMatchObject({ code: "GITEA_INVENTORY_NONADVANCING" });
expect(fetcher).toHaveBeenCalledTimes(2);
});
it("fails closed when the bounded inventory window is exhausted", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) =>
new Response(JSON.stringify([{ id: Number(new URL(String(input)).searchParams.get("page")) }])));
await expect(new GiteaClient("https://gitea.example.com", "fixture-token", fetcher as typeof fetch).listRepositories())
.rejects.toMatchObject({ code: "GITEA_INVENTORY_LIMIT_EXCEEDED" });
expect(fetcher).toHaveBeenCalledTimes(10);
});
it("propagates a later-page failure without returning the earlier inventory", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) =>
new URL(String(input)).searchParams.get("page") === "1"
? new Response(JSON.stringify([{ id: 1 }]))
: new Response(JSON.stringify({ message: "unavailable" }), { status: 503 }));
await expect(new GiteaClient("https://gitea.example.com", "fixture-token", fetcher as typeof fetch).listRepositories())
.rejects.toMatchObject({ code: "GITEA_HTTP_503" });
});
it("creates, patches, comments on, and labels issues through bounded API calls", async () => {
const fetcher = vi.fn(async (input: string | URL | Request, init?: RequestInit) => {
const url = String(input);
if (url.endsWith("/comments")) return new Response(JSON.stringify({ id: 4, body: "verified", created_at: "2026-08-26T00:00:00Z" }), { status: 201 });
if (url.endsWith("/labels")) return new Response(JSON.stringify([]), { status: 200 });
return new Response(JSON.stringify({ id: 7, number: 7, title: "Finding", body: "body", state: "open", html_url: "https://gitea.example/issues/7", created_at: "2026-08-26T00:00:00Z", updated_at: "2026-08-26T00:00:00Z" }), { status: init?.method === "PATCH" ? 200 : 201 });
});
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
await client.createIssue("Acme", "Repo", { title: "Finding", body: "body", labels: [1, 2] });
await client.editIssue("Acme", "Repo", 7, { state: "closed" });
await client.createIssueComment("Acme", "Repo", 7, "verified");
await client.replaceIssueLabels("Acme", "Repo", 7, [1, 2]);
expect(fetcher.mock.calls.map(([, init]) => init?.method)).toEqual(["POST", "PATCH", "POST", "PUT"]);
expect(fetcher.mock.calls[0][1]?.body).toBe(JSON.stringify({ title: "Finding", body: "body", labels: [1, 2] }));
expect(String(fetcher.mock.calls[3][0])).toContain("/issues/7/labels");
});
it("merges an exact pull request head and deletes its encoded work branch", async () => {
const fetcher = vi.fn(async (input: string | URL | Request) => String(input).endsWith("/version")
? new Response(JSON.stringify({ version: "1.27.3" }), { status: 200, headers: { "content-type": "application/json" } })
: new Response(undefined, { status: 204 }));
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
await client.mergePullRequest("Acme", "Repo", 17, { method: "squash", headCommitId: "a".repeat(40) });
await client.deleteBranch("Acme", "Repo", "feature/a b");
expect(String(fetcher.mock.calls[0][0])).toContain("/version");
expect(String(fetcher.mock.calls[1][0])).toContain("/pulls/17/merge");
expect(fetcher.mock.calls[1][1]?.method).toBe("POST");
expect(fetcher.mock.calls[1][1]?.body).toBe(JSON.stringify({
do: "squash",
head_commit_id: "a".repeat(40),
force_merge: false,
merge_when_checks_succeed: false,
delete_branch_after_merge: false,
}));
expect(String(fetcher.mock.calls[2][0])).toContain("/branches/feature%2Fa%20b");
expect(fetcher.mock.calls[2][1]?.method).toBe("DELETE");
});
it.each([
["1.25.5", "Do"],
["1.26.4", "do"],
["1.27.3", "do"],
] as const)("uses the versioned merge strategy field for Gitea %s", async (version, strategyKey) => {
const fetcher = vi.fn(async (input: string | URL | Request) => String(input).endsWith("/version")
? new Response(JSON.stringify({ version }), { status: 200, headers: { "content-type": "application/json" } })
: new Response(undefined, { status: 204 }));
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
await client.mergePullRequest("Acme", "Repo", 17, { method: "merge", headCommitId: "b".repeat(40) });
await client.mergePullRequest("Acme", "Repo", 18, { method: "squash", headCommitId: "c".repeat(40) });
expect(fetcher.mock.calls.filter(([input]) => String(input).endsWith("/version"))).toHaveLength(1);
const firstBody = JSON.parse(String(fetcher.mock.calls[1][1]?.body)) as Record<string, unknown>;
expect(firstBody[strategyKey]).toBe("merge");
expect(firstBody[strategyKey === "Do" ? "do" : "Do"]).toBeUndefined();
});
it("waits for every required pull-request status context before merging", async () => {
let statusReads = 0;
const fetcher = vi.fn(async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("/branch_protections/main")) return new Response(JSON.stringify({ enable_status_check: true, status_check_contexts: ["Managed validation / full (pull_request)"] }), { status: 200 });
if (url.endsWith("/commits/" + "d".repeat(40) + "/status")) {
statusReads += 1;
return new Response(JSON.stringify({ state: statusReads === 1 ? "pending" : "success", statuses: [{ context: "Managed validation / full (pull_request)", status: statusReads === 1 ? "pending" : "success" }] }), { status: 200 });
}
return new Response(JSON.stringify({ message: "unexpected request" }), { status: 500 });
});
const client = new GiteaClient("https://gitea.example.com", "secret-token", fetcher as typeof fetch);
const result = await client.waitForRequiredPullRequestChecks("Acme", "Repo", "main", "d".repeat(40), { timeoutMs: 1_000, pollIntervalMs: 0 });
expect(result).toEqual({ requiredContexts: ["Managed validation / full (pull_request)"], state: "success" });
expect(statusReads).toBe(2);
});
});
describe("portfolio onboarding policy", () => {
it("distinguishes existing-repository intake from new repository provisioning", () => {
expect(existingRepositoryFromProjectSetup({ firstRun: { repositoryProfile: { repository: "Acme/existing" } } })).toBe("Acme/existing");
expect(existingRepositoryFromProjectSetup({ firstRun: { repositoryProfile: { preferredStack: "TypeScript" } } })).toBeUndefined();
expect(projectSlugFromInput(undefined, "My New Project")).toBe("my-new-project");
expect(giteaRepositoryNameForProject("My Invalid / Project")).toBe("my-invalid-project");
expect(giteaRepositoryNameCandidate("my-project", 2)).toBe("my-project-2");
});
it("enables automatic reconciliation with a one-minute default", () => {
const configured = 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",
});
expect(configured.PROJECTBRAIN_AUTO_ONBOARD_REPOSITORIES).toBe(true);
expect(configured.PROJECTBRAIN_REPOSITORY_RECONCILE_SECONDS).toBe(60);
});
it("accepts a separate least-privilege token for issue writes", () => {
const configured = loadEnv({
...process.env,
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_WRITE_TOKEN: "issue-write-token-with-twenty-characters",
});
expect(configured.PROJECTBRAIN_GITEA_WRITE_TOKEN).toBe("issue-write-token-with-twenty-characters");
});
it("keeps explicit exclusions durable while a real link always wins", () => {
expect(resolvePortfolioRepositoryStatus(false)).toBe(PortfolioRepositoryStatus.discovered);
expect(resolvePortfolioRepositoryStatus(false, PortfolioRepositoryStatus.excluded)).toBe(PortfolioRepositoryStatus.excluded);
expect(resolvePortfolioRepositoryStatus(true, PortfolioRepositoryStatus.excluded)).toBe(PortfolioRepositoryStatus.onboarded);
});
it("does not reuse a project slug that already belongs to another repository", () => {
expect(canReuseProjectForRepository([], "Jens", "ProjectBrain")).toBe(true);
expect(canReuseProjectForRepository([{ owner: "jens", repository: "projectbrain" }], "Jens", "ProjectBrain")).toBe(true);
expect(canReuseProjectForRepository([{ owner: "Other", repository: "ProjectBrain" }], "Jens", "ProjectBrain")).toBe(false);
expect(canReuseProjectForRepository([{ owner: "Jens", repository: "Different" }], "Jens", "ProjectBrain")).toBe(false);
});
});
describe("durable finding identity", () => {
it("deduplicates repository findings case-insensitively and keeps rule subjects distinct", () => {
const first = repositoryFindingFingerprint("Acme", "Repo", "DEFAULT_BRANCH_UNPROTECTED", "main");
expect(repositoryFindingFingerprint("acme", "repo", "DEFAULT_BRANCH_UNPROTECTED", "main")).toBe(first);
expect(repositoryFindingFingerprint("acme", "repo", "DEFAULT_BRANCH_UNPROTECTED", "develop")).not.toBe(first);
expect(findingIssueMarker({ id: "finding-1", fingerprint: first })).toContain("projectbrain:finding=finding-1");
});
it("keeps workflow subjects distinct and ranks actionable evidence deterministically", () => {
expect(repositoryFindingFingerprint("Acme", "Repo", "DEFAULT_BRANCH_ACTION_FAILED", "main:ci.yml"))
.not.toBe(repositoryFindingFingerprint("Acme", "Repo", "DEFAULT_BRANCH_ACTION_FAILED", "main:deploy.yml"));
expect(findingRiskScore({ severity: "critical", code: "DEFAULT_BRANCH_ACTION_FAILED", message: "failed", evidence: { scope: "default_branch", consecutiveFailures: 2 } }))
.toBeGreaterThan(findingRiskScore({ severity: "critical", code: "SCHEDULED_ACTION_FAILED", message: "failed", evidence: { scope: "scheduled", consecutiveFailures: 2 } }));
expect(findingRiskScore({ severity: "warning", code: "SCHEDULED_ACTION_FAILED", message: "failed", evidence: { scope: "scheduled", consecutiveFailures: 2 } }))
.toBeGreaterThan(findingRiskScore({ severity: "info", code: "TOPIC_BRANCH_ACTION_FAILED", message: "failed", evidence: { scope: "pull_request", consecutiveFailures: 1 } }));
});
it("renders the exact Gitea run evidence for managed issues", () => {
const markdown = renderFindingEvidence({ workflow: "ci.yml", branch: "main", event: "push", conclusion: "failure", consecutiveFailures: 3, runUrl: "https://gitea.example/actions/runs/7" });
expect(markdown).toContain("[open run](https://gitea.example/actions/runs/7)");
expect(markdown).toContain("ci.yml");
expect(markdown).toContain("main");
});
});
describe("MCP OAuth 2.1 PKCE", () => {
it("registers a public client, exchanges a PKCE code, and refreshes access", () => {
const redirectUri = "https://chatgpt.com/connector/callback";
const client = registerOAuthClient(env, { redirectUris: [redirectUri], clientName: "ChatGPT" });
const verifier = "a".repeat(48);
const challenge = createHash("sha256").update(verifier).digest("base64url");
const code = issueAuthorizationCode(env, { clientId: client.clientId, redirectUri, codeChallenge: challenge, scope: "mcp:tools", subject: "owner", credentialVersion: 1, audience: "https://projectbrain.example.com/mcp" });
const pair = exchangeAuthorizationCode(env, { code, clientId: client.clientId, redirectUri, codeVerifier: verifier });
expect(verifyMcpAccessToken(env, pair.accessToken)).toMatchObject({ subject: "owner", credentialVersion: 1, scope: "mcp:tools", audience: "https://projectbrain.example.com/mcp" });
expect(verifyMcpAccessToken(env, exchangeRefreshToken(env, pair.refreshToken, client.clientId).accessToken).subject).toBe("owner");
expect(() => exchangeAuthorizationCode(env, { code, clientId: client.clientId, redirectUri, codeVerifier: verifier })).toThrow("OAUTH_CODE_ALREADY_USED");
});
it("rejects redirect substitution and an invalid PKCE verifier", () => {
const client = registerOAuthClient(env, { redirectUris: ["https://chatgpt.com/connector/callback"] });
expect(() => registerOAuthClient(env, { redirectUris: ["http://attacker.example/callback"] })).toThrow("OAUTH_REDIRECT_INVALID");
const code = issueAuthorizationCode(env, { clientId: client.clientId, redirectUri: client.redirectUris[0], codeChallenge: createHash("sha256").update("correct".repeat(8)).digest("base64url"), scope: "mcp:tools", subject: "owner", credentialVersion: 1, audience: "https://projectbrain.example.com/mcp" });
expect(() => exchangeAuthorizationCode(env, { code, clientId: client.clientId, redirectUri: client.redirectUris[0], codeVerifier: "wrong".repeat(10) })).toThrow("OAUTH_PKCE_INVALID");
});
it("requires HTTPS for an enabled production MCP origin", () => {
expect(() => loadEnv({ NODE_ENV: "production", DATABASE_URL: "postgresql://example.invalid/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", PROJECTBRAIN_MCP_ENABLED: "true", PROJECTBRAIN_PUBLIC_URL: "http://projectbrain.example.com", PROJECTBRAIN_GITEA_URL: "https://gitea.example.com", PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters" })).toThrow("must use HTTPS");
});
});
describe("Gitea administration authorization", () => {
it("allows only the ProjectBrain owner role", () => {
expect(hasGiteaAdminAccess("owner")).toBe(true);
expect(hasGiteaAdminAccess("member")).toBe(false);
expect(hasGiteaAdminAccess()).toBe(false);
});
});
describe("ProjectBrain MCP Hub and Skills integration", () => {
it("compares the Hub token safely and constrains the audit actor", () => {
const secret = "projectbrain-hub-token-with-more-than-32-characters";
expect(isMcpHubServiceToken(secret, secret)).toBe(true);
expect(isMcpHubServiceToken(secret, `${secret}x`)).toBe(false);
expect(isMcpHubServiceToken(secret, "wrong-token-with-more-than-32-characters")).toBe(false);
expect(mcpHubActor("chatgpt-web")).toBe("mcp-hub:chatgpt-web");
expect(mcpHubActor("../../spoofed actor")).toBe("mcp-hub:service");
});
it("advertises digest-verified native ProjectBrain skills", () => {
const listed = handleProjectBrainSkillMethod("skills/list", {}) as { skills: typeof projectBrainSkills };
expect(listed.skills).toHaveLength(4);
const selected = listed.skills[0];
expect(handleProjectBrainSkillMethod("skills/get", { uri: selected.uri })).toEqual({ skill: selected });
const resource = handleProjectBrainSkillMethod("resources/read", { uri: selected.uri }) as { contents: Array<{ text: string }> };
expect(resource.contents[0].text).toContain("# ProjectBrain operator");
const initialized = augmentProjectBrainInitialize({ result: { capabilities: {} } }) as { result: { capabilities: { extensions: Record<string, unknown> } } };
expect(initialized.result.capabilities.extensions).toHaveProperty("io.modelcontextprotocol/skills");
});
it("serves every advertised ChatGPT UI template as a versioned MCP Apps resource", async () => {
const server = createProjectBrainMcpServer(env, "template-contract-test");
const client = new Client({ name: "template-contract-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const [listedTools, listedResources] = await Promise.all([client.listTools(), client.listResources()]);
expect(listedTools.tools.map((tool) => tool.name)).toEqual(expect.arrayContaining([
"get_portfolio_control_plane", "list_portfolio_repositories", "reconcile_portfolio_repositories", "set_portfolio_repository_exclusion",
"list_repository_findings", "get_repository_finding", "create_finding_issue", "start_finding_investigation",
"get_finding_investigation", "save_finding_investigation", "create_delivery_from_finding", "verify_finding_resolution",
"apply_feature_changeset", "merge_feature_delivery",
]));
const changeset = listedTools.tools.find((tool) => tool.name === "apply_feature_changeset");
const merge = listedTools.tools.find((tool) => tool.name === "merge_feature_delivery");
expect(changeset?.annotations).toMatchObject({ readOnlyHint: false, destructiveHint: true, idempotentHint: false });
expect(merge?.annotations).toMatchObject({ readOnlyHint: false, destructiveHint: true, idempotentHint: true });
const advertisedUris = listedTools.tools.flatMap((tool) => {
const meta = tool._meta as { ui?: { resourceUri?: unknown }; "openai/outputTemplate"?: unknown } | undefined;
return [meta?.ui?.resourceUri, meta?.["openai/outputTemplate"]].filter((uri): uri is string => typeof uri === "string");
});
expect(advertisedUris).toEqual([PROJECTBRAIN_WORKSPACE_URI, PROJECTBRAIN_WORKSPACE_URI, PROJECTBRAIN_ANALYSIS_WIDGET_URI, PROJECTBRAIN_ANALYSIS_WIDGET_URI]);
expect(listedResources.resources).toEqual(expect.arrayContaining([
expect.objectContaining({ uri: PROJECTBRAIN_WORKSPACE_URI, mimeType: PROJECTBRAIN_WORKSPACE_MIME_TYPE }),
expect.objectContaining({ uri: PROJECTBRAIN_ANALYSIS_WIDGET_URI, mimeType: PROJECTBRAIN_ANALYSIS_WIDGET_MIME_TYPE }),
]));
for (const uri of new Set(advertisedUris)) {
const resource = await client.readResource({ uri });
expect(resource.contents).toHaveLength(1);
expect(resource.contents[0]).toMatchObject({ uri, mimeType: PROJECTBRAIN_WORKSPACE_MIME_TYPE });
expect("text" in resource.contents[0] ? resource.contents[0].text : "").toContain("<!doctype html>");
if (uri === PROJECTBRAIN_ANALYSIS_WIDGET_URI) {
const text = "text" in resource.contents[0] ? resource.contents[0].text : "";
expect(text).toContain("projectbrain.analysis.status");
expect(text).toContain("projectbrain.analysis.results");
}
expect(resource.contents[0]._meta).toMatchObject({
ui: { prefersBorder: true, domain: env.PROJECTBRAIN_PUBLIC_URL, csp: { connectDomains: [], resourceDomains: [] } },
});
}
} finally {
await client.close();
await server.close();
}
});
});
describe("ChatGPT feature delivery approval", () => {
const completePlan = {
problem: "Users cannot carry an idea through to a reviewed repository change.", desiredOutcome: "A persisted and review-gated feature delivery.",
goals: ["Keep scope explicit"], acceptanceCriteria: ["Owner approval is required"], implementationPlan: ["Create an isolated branch"], validationCommands: ["pnpm test"],
};
it("accepts complete plans and rejects incomplete approval contracts", () => {
expect(assertFeatureDeliveryPlanComplete(completePlan)).toBeUndefined();
expect(() => assertFeatureDeliveryPlanComplete({ ...completePlan, acceptanceCriteria: [] })).toThrow("FEATURE_DELIVERY_PLAN_INCOMPLETE");
expect(() => assertFeatureDeliveryPlanComplete({ ...completePlan, validationCommands: [] })).toThrow("FEATURE_DELIVERY_PLAN_INCOMPLETE");
});
it("accepts only successful Gitea Actions evidence for the current branch head", () => {
const evidence = { provider: "gitea_actions" as const, runId: 42, runNumber: 7, url: "https://gitea.example.com/runs/42", workflowPath: "ci.yml@refs/heads/chatgpt/feature", headSha: "current-sha", conclusion: "success" as const, verifiedAt: new Date().toISOString() };
expect(assertFeatureDeliveryActionEvidence(evidence, "current-sha")).toBeUndefined();
expect(() => assertFeatureDeliveryActionEvidence(evidence, "newer-sha")).toThrow("FEATURE_DELIVERY_CI_EVIDENCE_REQUIRED");
expect(() => assertFeatureDeliveryActionEvidence(undefined, "current-sha")).toThrow("FEATURE_DELIVERY_CI_EVIDENCE_REQUIRED");
});
it("refreshes exact-head validation evidence without closing an open pull request", () => {
expect(canSyncFeatureDeliveryValidation("executing")).toBe(true);
expect(canSyncFeatureDeliveryValidation("validating")).toBe(true);
expect(canSyncFeatureDeliveryValidation("pull_request_open")).toBe(true);
expect(canSyncFeatureDeliveryValidation("merged")).toBe(false);
expect(statusAfterFeatureDeliveryValidationSync("executing")).toBe("validating");
expect(statusAfterFeatureDeliveryValidationSync("pull_request_open")).toBe("pull_request_open");
});
it("requires every Gitea Actions workflow for the exact branch head to succeed", () => {
const run = (id: number, conclusion: string, headSha = "current-sha"): GiteaActionRun => ({
id, run_number: id, display_title: "CI", path: ".gitea/workflows/ci.yml", event: "push", head_sha: headSha,
head_branch: "chatgpt/feature", status: "completed", conclusion, html_url: `https://gitea.example.com/runs/${id}`,
});
expect(assertSuccessfulActionRuns([run(1, "success"), run(2, "success")], "chatgpt/feature", "current-sha")).toHaveLength(2);
expect(() => assertSuccessfulActionRuns([run(1, "success"), run(2, "failure")], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
expect(() => assertSuccessfulActionRuns([run(1, "success", "stale-sha")], "chatgpt/feature", "current-sha")).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
});
});
+83
View File
@@ -0,0 +1,83 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { describe, expect, it } from "vitest";
import { loadEnv } from "../src/config/env.js";
import {
parsePortfolioFetchId,
portfolioFetchId,
} from "../src/integrations/gitea/portfolio.js";
import { registerPortfolioMcpTools } from "../src/mcp/portfolioTools.js";
import { createProjectBrainMcpServer } from "../src/mcp/server.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_MCP_ENABLED: "true",
PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com",
PROJECTBRAIN_GITEA_URL: "https://gitea.example.com",
PROJECTBRAIN_GITEA_TOKEN: "a-token-with-at-least-twenty-characters",
});
describe("read-only Gitea portfolio MCP projection", () => {
it("round-trips canonical repository/ref/path fetch identifiers", () => {
const id = portfolioFetchId(
{ owner: "Jens", repository: "Mediarr" },
"codex/example-branch",
"src/a file.ts",
);
expect(id).toBe(
"gitea-repository://Jens/Mediarr/codex%2Fexample-branch/src%2Fa%20file.ts",
);
expect(parsePortfolioFetchId(id)).toEqual({
owner: "Jens",
repository: "Mediarr",
ref: "codex/example-branch",
path: "src/a file.ts",
});
expect(() => parsePortfolioFetchId("gitea://project/main/file.ts")).toThrow(
"MCP_FETCH_ID_INVALID",
);
});
it("advertises portfolio tools as distinct read-only capabilities", async () => {
const server = createProjectBrainMcpServer(env, "portfolio-contract-test");
registerPortfolioMcpTools(server, env, "portfolio-contract-test");
const client = new Client({ name: "portfolio-contract-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const listed = await client.listTools();
const tools = new Map(listed.tools.map((tool) => [tool.name, tool]));
for (const name of [
"list_portfolio_repositories",
"search_portfolio_repositories",
"fetch_portfolio_file",
]) {
const tool = tools.get(name);
expect(tool, `${name} should be registered`).toBeDefined();
expect(tool?.annotations).toMatchObject({
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
});
}
expect(tools.get("list_portfolio_repositories")?.annotations?.openWorldHint).toBe(true);
expect(tools.get("search_portfolio_repositories")?.annotations?.openWorldHint).toBe(false);
expect(tools.get("fetch_portfolio_file")?.annotations?.openWorldHint).toBe(false);
// Existing delivery mutations remain separate, linked-project tools.
expect(tools.has("write_feature_file")).toBe(true);
expect(tools.has("search")).toBe(true);
expect(tools.has("fetch")).toBe(true);
} finally {
await client.close();
await server.close();
}
});
});
+37
View File
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import { serializeAnalysisJob } from "../src/repositories/analysisJobRepository.js";
import { buildProjectContextResume } from "../src/services/contextResolver.js";
describe("MCP efficiency hints", () => {
it("finishes open PRs before starting newer delivery branches", () => {
const resume = buildProjectContextResume(
[{ id: "repo", owner: "Jens", repository: "app", accessMode: "read_write", defaultBranch: "main" }],
[{ id: "new", title: "New plan", status: "approved", approvalState: "approved" }, { id: "existing", title: "Finish me", status: "pull_request_open", approvalState: "approved", runs: [{ pullRequestNumber: 7 }] }], [],
);
expect(resume.nextAction).toMatchObject({ type: "review-and-merge", deliveryId: "existing", pullRequestNumber: 7 });
});
it("returns one durable resume action for an active delivery", () => {
const resume = buildProjectContextResume(
[{ id: "repo-1", owner: "Jens", repository: "app", accessMode: "read_write", defaultBranch: "main" }],
[{ id: "delivery-1", title: "Improve MCP", status: "executing", approvalState: "approved", runs: [{ branch: "chatgpt/mcp", headSha: "abc" }] }],
[],
"2026-08-27T00:00:00.000Z",
);
expect(resume).toMatchObject({
repositoryCount: 1,
activeDelivery: { id: "delivery-1", branch: "chatgpt/mcp" },
nextAction: { type: "continue-implementation", deliveryId: "delivery-1" },
});
expect(resume.stateVersion).toMatch(/^[0-9a-f]{24}$/);
});
it("tells clients when and whether to poll analysis state", () => {
const running = serializeAnalysisJob({ id: "job-1", status: "running", progressCurrent: 1, progressTotal: 4, updatedAt: new Date("2026-08-27T00:00:00.000Z") });
const succeeded = serializeAnalysisJob({ id: "job-1", status: "succeeded", progressCurrent: 4, progressTotal: 4, updatedAt: new Date("2026-08-27T00:01:00.000Z") });
expect(running).toMatchObject({ terminal: false, pollAfterMs: 4_000, nextAction: "wait-for-change" });
expect(succeeded).toMatchObject({ terminal: true, pollAfterMs: 0, nextAction: "read-results" });
expect(running.stateVersion).not.toBe(succeeded.stateVersion);
});
});
+56
View File
@@ -0,0 +1,56 @@
import { describe, expect, it } from "vitest";
import { augmentProjectBrainInitialize, handleProjectBrainSkillMethod } from "../src/mcp/skills.js";
type SkillList = { skills: Array<{ uri: string }> };
type SkillResource = { contents: Array<{ text: string }> };
describe("ProjectBrain MCP skills", () => {
it("publishes canonical public workflows for repository bootstrap and delivery", () => {
const listed = handleProjectBrainSkillMethod("skills/list", {}) as SkillList;
const uris = listed.skills.map((skill) => skill.uri);
expect(uris).toContain("skill://projectbrain/repository-bootstrap/SKILL.md");
expect(uris).toContain("skill://projectbrain/feature-delivery/SKILL.md");
const bootstrap = handleProjectBrainSkillMethod("resources/read", { uri: "skill://projectbrain/repository-bootstrap/SKILL.md" }) as SkillResource;
expect(bootstrap.contents[0]?.text).toContain("gitea.repository.create");
expect(bootstrap.contents[0]?.text).toContain("projectbrain.portfolio.repository.reconcile");
const delivery = handleProjectBrainSkillMethod("resources/read", { uri: "skill://projectbrain/feature-delivery/SKILL.md" }) as SkillResource;
expect(delivery.contents[0]?.text).toContain("projectbrain.delivery.changeset.apply");
expect(delivery.contents[0]?.text).toContain("projectbrain.delivery.merge");
expect(delivery.contents[0]?.text).toContain("gitea.workspace.run");
expect(delivery.contents[0]?.text).not.toContain("apply_feature_changeset");
expect(delivery.contents[0]?.text).not.toContain("merge_feature_delivery");
});
it("publishes the Gitea-first Auto execution contract", () => {
const operator = handleProjectBrainSkillMethod("resources/read", { uri: "skill://projectbrain/projectbrain-operator/SKILL.md" }) as SkillResource;
const text = operator.contents[0]?.text ?? "";
expect(text).toContain("Gitea is the only primary repository execution surface");
expect(text).toContain("projectbrain/auto/<normalized-work-id>");
expect(text).toContain("atomic execution claim");
expect(text).toContain("exact-head validation evidence");
expect(text).toContain("missing or interrupted report_auto_work callback");
expect(text).toContain("No OpenAI API key is required");
});
it("advertises Gitea as the primary Auto execution surface during MCP initialization", () => {
const initialized = augmentProjectBrainInitialize({ result: { capabilities: {} } }) as {
result: { instructions: string };
};
expect(initialized.result.instructions).toContain(
"Gitea as its only primary repository execution surface",
);
expect(initialized.result.instructions).toContain("projectbrain/auto/<normalized-work-id>");
expect(initialized.result.instructions).toContain("Branch existence is the atomic claim");
expect(initialized.result.instructions).toContain(
"report_auto_work are backward-compatible coordination/bookkeeping surfaces only",
);
expect(initialized.result.instructions).toContain(
"Never write directly to the default branch",
);
});
});
+78
View File
@@ -0,0 +1,78 @@
import { describe, expect, it, vi } from "vitest";
import { GiteaClient } from "../src/integrations/gitea/client.js";
import { assertSuccessfulActionRuns, exactHeadActionRuns } from "../src/integrations/gitea/policy.js";
import type { GiteaActionRun } from "../src/integrations/gitea/types.js";
const branch = "codex/native-ci";
const sha = "a".repeat(40);
const context = "CI / build-test (pull_request)";
const run = (id: number, overrides: Partial<GiteaActionRun> = {}): GiteaActionRun => ({
id, run_number: id, display_title: "CI", path: ".gitea/workflows/ci.yml",
event: "pull_request", head_sha: sha, head_branch: "", status: "completed",
conclusion: "success", html_url: `https://gitea.example/runs/${id}`, ...overrides,
});
describe("native PR validation discovery", () => {
it("discovers an empty branch by exact SHA and checks available PR identity", () => {
const matching = run(1, { pull_requests: [{ number: 30, head: { ref: branch, sha } }] });
expect(exactHeadActionRuns([
matching, run(2), run(3, { head_sha: "stale" }),
run(4, { head_branch: "other" }),
run(5, { pull_requests: [{ number: 31, head: { ref: "other", sha } }] }),
run(6, { event: "workflow_dispatch" }),
], branch, sha)).toEqual([matching, run(2)]);
expect(assertSuccessfulActionRuns([matching], branch, sha)).toEqual([matching]);
});
it.each(["pending", "queued", "waiting", "in_progress"])("does not authorize %s", (status) => {
expect(() => assertSuccessfulActionRuns([run(1, { status, conclusion: undefined })], branch, sha)).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
});
it.each(["cancelled", "canceled", "failure"])("does not authorize a %s job that never started", (conclusion) => {
expect(() => assertSuccessfulActionRuns([run(1, { conclusion })], branch, sha)).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
});
it("uses the newest attempt of each workflow and preserves all evidence", () => {
const failure = run(2, { conclusion: "failure" });
expect(() => assertSuccessfulActionRuns([failure, run(1, { status: "pending" })], branch, sha)).toThrow();
const success = run(3);
expect(assertSuccessfulActionRuns([failure, success], branch, sha)).toEqual([failure, success]);
expect(() => assertSuccessfulActionRuns([success, run(4, { status: "queued" })], branch, sha)).toThrow();
});
it("never lets dispatch success replace PR failure even for the same workflow path", () => {
expect(() => assertSuccessfulActionRuns([
run(1, { conclusion: "failure" }),
run(2, { event: "workflow_dispatch", head_branch: branch }),
], branch, sha)).toThrow("FEATURE_DELIVERY_CI_NOT_SUCCESSFUL");
});
it("queries by exact SHA without the upstream branch filter", async () => {
const fetcher = vi.fn(async () => new Response(JSON.stringify({ total_count: 1, workflow_runs: [run(1)] })));
await new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch).listActionRuns("Jens", "Repo", { branch, headSha: sha });
const url = new URL(String(fetcher.mock.calls[0]![0]));
expect(url.searchParams.get("head_sha")).toBe(sha);
expect(url.searchParams.has("branch")).toBe(false);
});
it.each([
[[{ context, status: "failure" }, { context, status: "pending" }], "GITEA_REQUIRED_CHECKS_FAILED"],
[[{ context: "CI / build-test (workflow_dispatch)", status: "success" }], "GITEA_REQUIRED_CHECKS_PENDING"],
[[{ context: `${context} extra`, status: "success" }], "GITEA_REQUIRED_CHECKS_PENDING"],
[[{ context, status: "pending" }], "GITEA_REQUIRED_CHECKS_PENDING"],
] as const)("requires the exact latest protected status %#", async (statuses, error) => {
const fetcher = vi.fn(async (url: URL | RequestInfo) => new Response(JSON.stringify(String(url).includes("branch_protections")
? { enable_status_check: true, status_check_contexts: [context] } : { statuses })));
const client = new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch);
await expect(client.waitForRequiredPullRequestChecks("Jens", "Repo", "main", sha, { timeoutMs: 0 })).rejects.toMatchObject({ code: error });
expect(String(fetcher.mock.calls[1]![0])).toContain(`/commits/${sha}/status`);
});
it("accepts a newer native success after an old native failure", async () => {
const fetcher = vi.fn(async (url: URL | RequestInfo) => new Response(JSON.stringify(String(url).includes("branch_protections")
? { enable_status_check: true, status_check_contexts: [context] }
: { statuses: [{ context, status: "success" }, { context, status: "failure" }] })));
await expect(new GiteaClient("https://gitea.example", "test-only", fetcher as typeof fetch)
.waitForRequiredPullRequestChecks("Jens", "Repo", "main", sha, { timeoutMs: 0 })).resolves.toMatchObject({ state: "success" });
});
});
@@ -0,0 +1,78 @@
import { describe, expect, it } from "vitest";
import { parseCommandIntent } from "../src/services/commandIntent.js";
import { detectStuckConditions, interpretConnectorHealth } from "../src/services/diagnostics.js";
import { buildPulse } from "../src/services/pulse.js";
import { deliveryValidationsAreGreen } from "../src/services/projectCompletion.js";
import { buildParallelGroups, rankReadyWork, type WorkGraphNodeView } from "../src/services/workGraph.js";
const graphNode = (patch: Partial<WorkGraphNodeView> = {}): WorkGraphNodeView => ({
id: "node-1", projectId: "project-1", stableKey: "node-1", source: "test", sourceId: "node-1", type: "feature", title: "Work", canonicalLifecycle: "ready", domainLifecycle: "pending", scope: "committed", priority: 50, risk: "normal", blockingStatus: "open", executionEligibility: "ready", attemptCount: 0, consecutiveFailureCount: 0, dependencies: [], dependents: [], completionEvidence: [], createdAt: "2026-09-12T10:00:00.000Z", updatedAt: "2026-09-12T10:00:00.000Z", ...patch,
});
describe("operating-system user-facing services", () => {
it("turns natural language into constrained policy instead of an execution bypass", () => {
expect(parseCommandIntent("Continue this project but don't deploy.")).toMatchObject({
objective: "continue",
policy: { deploy: "blocked", automaticRecovery: true },
allowedScope: ["required", "committed"],
});
expect(parseCommandIntent("Finish everything required for release.")).toMatchObject({ objective: "release", requestedCompletionState: "definition_of_done", policy: { deploy: "unchanged" } });
});
it("distinguishes functional connector failure from process health", () => {
expect(interpretConnectorHealth({ process: true, transport: true, authentication: true, api: true, functional_read: false })).toBe("degraded");
expect(interpretConnectorHealth({ process: true, transport: true, api: false })).toBe("unavailable");
expect(interpretConnectorHealth({ process: true, transport: true, authentication: true, authorization: true, api: true, functional_read: true, write_readiness: true })).toBe("healthy");
});
it("detects reclaimable stale work and keeps the repair explicit", () => {
const now = Date.parse("2026-09-12T12:00:00.000Z");
expect(detectStuckConditions({
now,
running: [{ id: "lease-1", leaseExpiresAt: "2026-09-12T11:00:00.000Z" }],
queued: [{ id: "queue-1", queuedAt: "2026-09-12T11:00:00.000Z", consumerPresent: false }],
})).toMatchObject([
{ kind: "expired_lease", repairable: true },
{ kind: "queue_without_consumer", repairable: true },
]);
});
it("summarizes meaningful pulse events instead of returning a raw event dump", () => {
const pulse = buildPulse([
{ at: "2026-09-12T11:55:00.000Z", kind: "work.completed", title: "Delivery merged", summary: "The delivery was merged." },
{ at: "2026-09-12T11:56:00.000Z", kind: "heartbeat", title: "heartbeat", summary: "technical noise", meaningful: false },
{ at: "2026-09-12T11:57:00.000Z", kind: "work.blocked", title: "Validation failed", summary: "Retrying automatically." },
], Date.parse("2026-09-12T12:00:00.000Z"));
expect(pulse.counts).toMatchObject({ meaningful: 2, completed: 1, blockers: 1 });
expect(pulse.nextLikelyAction).toContain("Retrying");
});
it("treats a released delivery as a meaningful result while keeping noise out", () => {
const pulse = buildPulse([
{ at: "2026-09-12T11:55:00.000Z", kind: "delivery.released", title: "Release completed", summary: "The release is live." },
{ at: "2026-09-12T11:56:00.000Z", kind: "work.claimed", title: "Claimed", summary: "technical lease", meaningful: false },
], Date.parse("2026-09-12T12:00:00.000Z"));
expect(pulse.counts).toMatchObject({ meaningful: 1, completed: 1, active: 0 });
});
it("does not invent an operating action for unsupported language", () => {
expect(parseCommandIntent("Tell me a joke")).toMatchObject({ objective: "unknown", confidence: 35 });
});
it("keeps Future work out of ready scheduling and separates conflicting groups", () => {
const ready = [
graphNode({ id: "a", stableKey: "a", metadata: { touchedPaths: ["src/shared/a.ts"] } }),
graphNode({ id: "b", stableKey: "b", metadata: { touchedPaths: ["src/shared/a.ts"] } }),
graphNode({ id: "c", stableKey: "c", scope: "future" }),
];
expect(rankReadyWork(ready).map((item) => item.id)).toEqual(["a", "b"]);
expect(buildParallelGroups(rankReadyWork(ready))).toHaveLength(2);
});
it("does not mark a delivery complete before its declared validations have run", () => {
expect(deliveryValidationsAreGreen(["pnpm test"], [])).toBe(false);
expect(deliveryValidationsAreGreen(["pnpm test"], [{ status: "passed" }])).toBe(true);
expect(deliveryValidationsAreGreen(["pnpm test"], [{ status: "failed" }])).toBe(false);
expect(deliveryValidationsAreGreen([], [])).toBe(true);
});
});
@@ -0,0 +1,99 @@
import { describe, expect, it } from "vitest";
import type { AutonomyProjectRuntime } from "../src/services/autonomyRuntime.js";
import { buildAutoHumanAttentionPriority, PORTFOLIO_CATALOG_CACHE_MS, shouldRefreshPortfolioCatalog, summarizeDeliveryActivity, summarizeLiveExecution } from "../src/services/portfolioControlPlane.js";
const autoProject = (patch: Partial<AutonomyProjectRuntime> = {}): AutonomyProjectRuntime => ({
projectId: "project-db-id",
projectSlug: "demo",
projectName: "Demo",
enabled: true,
status: "running",
lifecycle: "ready",
pendingWorkCount: 3,
completedWorkCount: 1,
maxWorkItemsPerRun: 5,
maxRunMinutes: 30,
stalled: false,
...patch,
});
describe("portfolio delivery activity semantics", () => {
it("counts stored delivery phases separately from executor liveness", () => {
expect(summarizeDeliveryActivity({
draft: 2,
approved: 3,
executing: 4,
validating: 5,
pull_request_open: 6,
merged: 7,
verified: 8,
released: 9,
})).toEqual({
active: 9,
queued: 5,
waitingExternal: 6,
mergedAwaitingVerification: 7,
completed: 17,
});
});
it("defaults missing delivery buckets to zero", () => {
expect(summarizeDeliveryActivity({})).toEqual({
active: 0,
queued: 0,
waitingExternal: 0,
mergedAwaitingVerification: 0,
completed: 0,
});
});
});
describe("confirmed execution", () => {
const now = Date.parse("2026-09-09T12:00:00Z");
const working = autoProject({ activeWorkKind: "implement", activeRunId: "run-1", executorRunId: "run-1", executorStatus: "working", executorLastSeenAt: new Date(now - 1000).toISOString(), leaseExpiresAt: new Date(now + 60000).toISOString() });
it("requires enabled state, valid lease and fresh working heartbeat together", () => {
expect(summarizeLiveExecution([working], 120, now).verifiedActiveProjects).toBe(1);
for (const patch of [{ enabled: false }, { executorStatus: "stale" as const }, { executorStatus: "idle" as const }, { activeWorkKind: undefined }, { leaseExpiresAt: new Date(now).toISOString() }, { executorLastSeenAt: new Date(now - 121000).toISOString() }, { executorLastSeenAt: "invalid" }, { executorLastSeenAt: new Date(now + 1000).toISOString() }]) {
expect(summarizeLiveExecution([{ ...working, ...patch }], 120, now).verifiedActiveProjects).toBe(0);
}
});
it("distinguishes paused automation from orphaned claims", () => {
expect(summarizeLiveExecution([{ ...working, executorRunId: "different-run" }], 120, now).verifiedActiveProjects).toBe(0);
expect(summarizeLiveExecution([{ ...working, activeRunId: undefined }], 120, now).verifiedActiveProjects).toBe(0);
expect(summarizeLiveExecution([{ ...working, enabled: false }], 120, now)).toMatchObject({ verifiedActiveProjects: 0, enabledProjects: 0, unconfirmedLeaseProjects: 0 });
expect(summarizeLiveExecution([{ ...working, executorStatus: "offline" }], 120, now).unconfirmedLeaseProjects).toBe(1);
});
});
describe("portfolio Auto human attention", () => {
it("surfaces true human Auto blockers as portfolio attention", () => {
const priority = buildAutoHumanAttentionPriority([
autoProject({ status: "blocked", blockerKind: "human", blockerReason: "Owner decision required" }),
autoProject({ projectId: "retry-db", projectSlug: "retry", projectName: "Retry", status: "blocked", blockerKind: "retryable", blockerReason: "Runner timeout" }),
]);
expect(priority).toMatchObject({
id: "auto-human-blockers",
projectId: "project-db-id",
requiresHumanAttention: true,
count: 1,
route: "/projects/demo/attention",
});
});
it("does not turn retryable or disabled Auto state into human attention", () => {
expect(buildAutoHumanAttentionPriority([
autoProject({ status: "blocked", blockerKind: "retryable" }),
autoProject({ enabled: false, status: "blocked", blockerKind: "human" }),
])).toBeUndefined();
});
});
describe("portfolio catalog freshness", () => {
it("serves a recent durable catalog before asking Gitea for another inventory", () => {
const now = Date.parse("2026-09-13T12:00:00.000Z");
expect(shouldRefreshPortfolioCatalog(new Date(now - PORTFOLIO_CATALOG_CACHE_MS), now)).toBe(false);
expect(shouldRefreshPortfolioCatalog(new Date(now - PORTFOLIO_CATALOG_CACHE_MS - 1), now)).toBe(true);
expect(shouldRefreshPortfolioCatalog(undefined, now)).toBe(true);
});
});
+78
View File
@@ -0,0 +1,78 @@
import Fastify from "fastify";
import { afterEach, describe, expect, it } from "vitest";
import { registerProjectRoutes } from "../src/routes/projects.js";
describe("project create setup contract", () => {
const apps: Array<ReturnType<typeof Fastify>> = [];
afterEach(async () => {
await Promise.all(apps.splice(0).map((app) => app.close()));
});
it("returns the rich wizard setup only after the create request succeeds", async () => {
const app = Fastify();
apps.push(app);
await registerProjectRoutes(app, { dataSource: "fixtures" });
const response = await app.inject({
method: "POST",
url: "/api/projects",
payload: {
id: "durable-wizard",
name: "Durable wizard",
description: "A project whose complete wizard state survives the create boundary.",
domain: "webapp",
primaryGoal: "Persist the complete setup",
targetAudience: "Project owners",
dna: {
vision: "Keep project setup durable",
goals: ["Persist the complete setup", "Reload it safely"],
nonGoals: ["Do not navigate before success"],
targetAudience: "Project owners",
principles: ["Review before truth"],
successMetrics: [{ metric: "setup-roundtrip", target: "= true", operator: "=" }],
maturityStage: "Discovery",
revision: 1,
},
setupState: {
id: "must-not-override",
firstRun: {
documentationDepth: "Full - exhaustive design system",
repositoryProfile: { preferredStack: "React + Fastify", repository: "Acme/durable-wizard" },
},
constraints: [{ id: "constraint-1", description: "No false success" }],
workspaceState: {
projectId: "durable-wizard",
primaryObjective: "Build the first durable slice",
focusAreas: [],
openTasksCount: 2,
openBugCount: 0,
activeSection: "workspace",
updatedAt: "2026-08-30T00:00:00.000Z",
notes: ["Created from the wizard"],
},
},
},
});
expect(response.statusCode).toBe(201);
expect(response.json().data).toMatchObject({
id: "durable-wizard",
name: "Durable wizard",
dna: {
goals: ["Persist the complete setup", "Reload it safely"],
targetAudience: "Project owners",
},
constraints: [{ id: "constraint-1", description: "No false success" }],
firstRun: {
documentationDepth: "Full - exhaustive design system",
repositoryProfile: { preferredStack: "React + Fastify", repository: "Acme/durable-wizard" },
},
workspaceState: {
projectId: "durable-wizard",
primaryObjective: "Build the first durable slice",
openTasksCount: 2,
},
});
}, 15_000);
});
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { mergeProjectSetupState, sanitizeProjectSetupState } from "../src/repositories/projectSetupState.js";
describe("project setup state persistence", () => {
it("round-trips rich wizard fields while rejecting unsupported top-level state", () => {
const setup = sanitizeProjectSetupState({
id: "spoofed-project",
name: "Spoofed name",
firstRun: { documentationDepth: "Deep", repoFolders: ["docs/00-project"] },
constraints: [{ id: "constraint-1", description: "Keep data durable" }],
workspaceState: { projectId: "wizard-project", primaryObjective: "Ship the first slice" },
repositoryIntakes: [{ id: "not-part-of-project-setup" }],
});
expect(setup).toMatchObject({
firstRun: { documentationDepth: "Deep", repoFolders: ["docs/00-project"] },
constraints: [{ id: "constraint-1", description: "Keep data durable" }],
workspaceState: { projectId: "wizard-project", primaryObjective: "Ship the first slice" },
});
expect(setup).not.toHaveProperty("id");
expect(setup).not.toHaveProperty("name");
expect(setup).not.toHaveProperty("repositoryIntakes");
});
it("always lets canonical database fields win over setup state", () => {
expect(mergeProjectSetupState(
{ activeContextPackId: "starter-pack", workspaceState: { primaryObjective: "Wizard objective" }, name: "Spoofed" },
{ id: "canonical", name: "Canonical" },
)).toEqual({
activeContextPackId: "starter-pack",
workspaceState: { primaryObjective: "Wizard objective" },
id: "canonical",
name: "Canonical",
});
});
});
@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { projectHealth } from "../src/services/projectHealthSemantics.js";
const finding = (severity = "warning", status = "new") => ({ severity, status });
const health = (repositoryFindings: ReturnType<typeof finding>[] = [], syncStatuses: string[] = ["ready"]) =>
projectHealth({ repositoryFindings, giteaRepositories: syncStatuses.map((syncStatus) => ({ syncStatus })) });
describe("project health semantics", () => {
it("does not label a returned warning such as branch sprawl as a critical risk", () => {
expect(health([finding("warning", "regressed")])).toEqual({
score: 80, healthStatus: "warn", activeFindingCount: 1, criticalCount: 0, warningCount: 1, regressedCount: 1,
});
});
it.each(["new", "triaged", "regressed", "issue_open"])("preserves critical failures in status %s", (status) => {
expect(health([finding("critical", status)]).healthStatus).toBe("risk");
});
it("preserves aggregate low-score risk without a critical finding", () => {
expect(health(Array.from({ length: 11 }, () => finding()))).toMatchObject({ score: 45, healthStatus: "risk" });
expect(health(Array.from({ length: 10 }, () => finding()))).toMatchObject({ score: 50, healthStatus: "warn" });
});
it("retains recurrence evidence and its aggregate penalty", () => {
expect(health(Array.from({ length: 3 }, () => finding("warning", "regressed")))).toMatchObject({
score: 40, healthStatus: "risk", regressedCount: 3, warningCount: 3,
});
expect(health([finding("info", "regressed")])).toMatchObject({ score: 85, healthStatus: "warn", regressedCount: 1 });
});
it("excludes resolved and suppressed findings without hiding active findings", () => {
expect(health([finding("critical", "resolved"), finding("critical", "suppressed"), finding("info")])).toMatchObject({
score: 100, healthStatus: "healthy", activeFindingCount: 1, criticalCount: 0,
});
});
it("retains repository availability penalties and bounded scores", () => {
expect(health([], ["failed"])).toMatchObject({ score: 80, healthStatus: "warn" });
expect(health(Array.from({ length: 10 }, () => finding("critical")), ["failed", "failed", "failed"])).toMatchObject({
score: 0, healthStatus: "risk",
});
expect(health()).toMatchObject({ score: 100, healthStatus: "healthy" });
});
});
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { reconcileTruthCandidates } from "../src/services/projectKnowledge.js";
describe("project knowledge truth reconciliation", () => {
it("flags ambiguous disagreement as one actionable conflict", () => {
const conflicts = reconcileTruthCandidates([
{ key: "architecture", claim: "uses service a", source: "memory", authority: "memory", confidence: 90, entryId: "memory:1" },
{ key: "architecture", claim: "uses service b", source: "documentation", authority: "roadmap", confidence: 80, entryId: "documentation:1" },
]);
expect(conflicts).toHaveLength(1);
expect(conflicts[0]).toMatchObject({ status: "attention", entryIds: ["memory:1", "documentation:1"] });
});
it("lets verified repository truth supersede stale descriptive memory", () => {
const conflicts = reconcileTruthCandidates([
{ key: "architecture", claim: "uses service a", source: "memory", authority: "memory", confidence: 95, entryId: "memory:1" },
{ key: "architecture", claim: "uses service b", source: "repository", authority: "repository", confidence: 95, verifiedAt: "2026-09-12T10:00:00.000Z", entryId: "repo:1" },
]);
expect(conflicts[0]).toMatchObject({ status: "reconciled", winningEntryId: "repo:1" });
});
});
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { decideRecovery } from "../src/services/recoveryPolicy.js";
import { normalizeAutonomyState, retryAutonomyBlockerIfDue } from "../src/repositories/autonomyRepository.js";
describe("bounded Auto Mode recovery policy", () => {
it("walks through retry, cooldown and alternate recovery before escalation", () => {
const sequence = [1, 2, 3, 4, 5, 6].map((recoveryAttempts) => decideRecovery({
failureCount: recoveryAttempts,
recoveryAttempts,
recoveryStartedAt: "2026-09-12T10:00:00.000Z",
maxRecoveryMinutes: 120,
now: Date.parse("2026-09-12T10:10:00.000Z"),
}).stage);
expect(sequence).toEqual(["retry", "retry", "cooldown", "retry", "alternate_recovery", "escalated"]);
});
it("stops on the wall-clock budget even if attempts are low", () => {
expect(decideRecovery({
failureCount: 1,
recoveryAttempts: 1,
recoveryStartedAt: "2026-09-12T08:00:00.000Z",
maxRecoveryMinutes: 30,
now: Date.parse("2026-09-12T08:31:00.000Z"),
})).toMatchObject({ stage: "escalated", exhausted: true, reason: "recovery_wall_clock_limit" });
});
it("escalates a persisted blocker when its cooldown wakes after the wall-clock budget", () => {
const state = normalizeAutonomyState({
enabled: true,
maxRecoveryMinutes: 30,
recoveryStartedAt: "2026-09-12T08:00:00.000Z",
blocker: { at: "2026-09-12T08:00:00.000Z", phaseId: "phase-1", reason: "Runner unavailable", kind: "retryable", retryAt: "2026-09-12T08:31:00.000Z", recoveryStage: "retry", recoveryAttempts: 1, recoveryStartedAt: "2026-09-12T08:00:00.000Z" },
roadmap: { id: "roadmap-1", title: "Release", status: "blocked", currentPhaseIndex: 0, importedAt: "2026-09-12T08:00:00.000Z", phases: [{ id: "phase-1", title: "Build", objective: "Build", status: "blocked", items: [] }] },
});
const result = retryAutonomyBlockerIfDue(state, Date.parse("2026-09-12T08:31:00.000Z"));
expect(result.blocker).toMatchObject({ recoveryStage: "escalated" });
expect(result.status).toBe("blocked");
expect(result.roadmap?.status).toBe("blocked");
});
});
@@ -0,0 +1,130 @@
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,
display_title: input.workflow ?? "CI",
path: `.gitea/workflows/${input.workflow ?? "ci.yml"}`,
event: "push",
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}`,
});
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: 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_CI_NOT_SUCCESSFUL");
});
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",
});
});
});
+419
View File
@@ -0,0 +1,419 @@
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();
});
});
+194
View File
@@ -0,0 +1,194 @@
import { describe, expect, it } from "vitest";
import { isSafeGitProjectSourcePath } from "../src/services/gitSourceSync.js";
import { chooseNextAutonomyWork } from "../src/services/autonomyScheduler.js";
import {
buildRepositoryAutonomyRoadmap,
isRepositoryManagedRoadmap,
mergeRepositoryAutonomyRoadmap,
} from "../src/services/repositoryAutonomy.js";
const roadmapSource = {
path: "ROADMAP.md",
content: `# Phoenix roadmap
## M1 — Safe software foundation
Build the strict software foundation while lab qualification can happen independently.
## M2 — K3s lab
Provision only after the external lab gate is cleared.`,
contentSha: "roadmap-sha",
};
const backlogSource = (extraTasks: Array<Record<string, unknown>> = []) => ({
path: "docs/implementation/backlog.json",
contentSha: `backlog-sha-${extraTasks.length}`,
content: JSON.stringify({
schema_version: "1.0",
project: "Phoenix",
execution_enabled: false,
tasks: [
{
id: "PHX-001",
title: "Read-only lab qualification",
milestone: "M1",
depends_on: [],
requirements: ["R02"],
plan: "docs/implementation/01-foundation.md",
execution: "qualification",
status: "blocked_external",
evidence: [],
verified_commit: null,
},
{
id: "PHX-002",
title: "Strict contracts and development harness",
milestone: "M1",
depends_on: [],
requirements: ["R03"],
plan: "docs/implementation/01-foundation.md",
execution: "code",
status: "planned",
evidence: [],
verified_commit: null,
},
{
id: "PHX-003",
title: "API, database, durable jobs and outbox",
milestone: "M1",
depends_on: ["PHX-002"],
requirements: ["R04"],
plan: "docs/implementation/01-foundation.md",
execution: "code",
status: "planned",
evidence: [],
verified_commit: null,
},
{
id: "PHX-006",
title: "Safe K3s bootstrap",
milestone: "M2",
depends_on: ["PHX-001", "PHX-002"],
requirements: ["R06"],
plan: "docs/implementation/01-foundation.md",
execution: "lab",
status: "planned",
evidence: [],
verified_commit: null,
},
...extraTasks,
],
}),
});
describe("repository-native Auto Mode", () => {
it("syncs planning truth without opening the source filter to arbitrary JSON", () => {
expect(isSafeGitProjectSourcePath("ROADMAP.md")).toBe(true);
expect(isSafeGitProjectSourcePath("docs/implementation/backlog.json")).toBe(true);
expect(isSafeGitProjectSourcePath("docs/implementation/roadmap.json")).toBe(true);
expect(isSafeGitProjectSourcePath("docs/implementation/random-data.json")).toBe(false);
expect(isSafeGitProjectSourcePath("config/backlog.json")).toBe(false);
expect(isSafeGitProjectSourcePath(".env")).toBe(false);
});
it("turns a structured repository backlog into dependency-aware autonomous work", () => {
const roadmap = buildRepositoryAutonomyRoadmap({
roadmap: roadmapSource,
backlog: backlogSource(),
importedAt: "2026-09-21T12:00:00.000Z",
});
expect(roadmap).toBeDefined();
expect(isRepositoryManagedRoadmap(roadmap)).toBe(true);
expect(roadmap?.phases.map((phase) => phase.title)).toEqual([
"M1 — Safe software foundation",
"M2 — K3s lab",
]);
const items = new Map(roadmap?.phases.flatMap((phase) => phase.items).map((item) => [item.id, item]));
expect(items.get("PHX-001")).toMatchObject({ status: "pending", waitingExternal: true, source: "roadmap" });
expect(items.get("PHX-002")).toMatchObject({ status: "pending", scope: "committed" });
expect(items.get("PHX-002")).not.toHaveProperty("waitingExternal");
expect(items.get("PHX-003")).toMatchObject({ dependsOn: ["PHX-002"] });
expect(items.get("PHX-006")).toMatchObject({ waitingExternal: true, dependsOn: ["PHX-001", "PHX-002"] });
expect(roadmap?.phases.every((phase) => phase.brainstormConverged)).toBe(true);
const next = chooseNextAutonomyWork(roadmap!.phases, roadmap!.currentPhaseIndex, "balanced");
expect(next?.item.id).toBe("PHX-002");
});
it("does not let a planning refresh newly complete an existing runtime item", () => {
const current = buildRepositoryAutonomyRoadmap({
roadmap: roadmapSource,
backlog: backlogSource(),
importedAt: "2026-09-21T12:00:00.000Z",
})!;
const currentItem = current.phases.flatMap((phase) => phase.items).find((item) => item.id === "PHX-002")!;
currentItem.status = "pending";
const verifiedBacklog = JSON.parse(backlogSource().content) as { tasks: Array<Record<string, unknown>> };
const task = verifiedBacklog.tasks.find((entry) => entry.id === "PHX-002")!;
task.status = "verified";
task.verified_commit = "deadbeef";
const incoming = buildRepositoryAutonomyRoadmap({
roadmap: { ...roadmapSource, contentSha: "roadmap-sha-verified" },
backlog: { ...backlogSource(), content: JSON.stringify(verifiedBacklog), contentSha: "backlog-sha-verified" },
importedAt: "2026-09-21T13:00:00.000Z",
})!;
const reconciled = mergeRepositoryAutonomyRoadmap(current, incoming);
const item = reconciled.phases.flatMap((phase) => phase.items).find((entry) => entry.id === "PHX-002")!;
expect(item.status).toBe("pending");
});
it("preserves verified runtime progress and explicit assignments when repository planning changes", () => {
const current = buildRepositoryAutonomyRoadmap({
roadmap: roadmapSource,
backlog: backlogSource(),
importedAt: "2026-09-21T12:00:00.000Z",
})!;
const phx002 = current.phases.flatMap((phase) => phase.items).find((item) => item.id === "PHX-002")!;
phx002.status = "completed";
phx002.attempts = 2;
phx002.deliveryId = "delivery-phx-002";
phx002.completionEvidence = ["merge:abc"];
current.phases[0].items.unshift({
id: "assignment-1",
title: "Explicit owner task",
instruction: "Keep this assignment across repo refreshes.",
source: "user_assignment",
status: "pending",
attempts: 0,
});
const incoming = buildRepositoryAutonomyRoadmap({
roadmap: { ...roadmapSource, contentSha: "roadmap-sha-2" },
backlog: backlogSource([{
id: "PHX-004",
title: "Auth and target guard",
milestone: "M1",
depends_on: ["PHX-003"],
requirements: ["R05"],
plan: "docs/implementation/01-foundation.md",
execution: "code",
status: "planned",
evidence: [],
verified_commit: null,
}]),
importedAt: "2026-09-21T13:00:00.000Z",
})!;
const reconciled = mergeRepositoryAutonomyRoadmap(current, incoming);
const items = new Map(reconciled.phases.flatMap((phase) => phase.items).map((item) => [item.id, item]));
expect(items.get("PHX-002")).toMatchObject({
status: "completed",
attempts: 2,
deliveryId: "delivery-phx-002",
completionEvidence: expect.arrayContaining(["merge:abc"]),
});
expect(items.get("assignment-1")).toMatchObject({ source: "user_assignment", status: "pending" });
expect(items.get("PHX-004")).toMatchObject({ dependsOn: ["PHX-003"], status: "pending" });
});
});
+48
View File
@@ -0,0 +1,48 @@
import { describe, expect, it } from "vitest";
import {
assertExplicitRepositoryTransfer,
assertRepositoryTransferHasNoDeliveryHistory,
GiteaRepositoryOwnershipConflictError,
GiteaRepositoryTransferBlockedError,
} from "../src/integrations/gitea/service.js";
import { integrationError } from "../src/routes/giteaIntegration.js";
describe("Gitea repository ownership", () => {
const existing = { projectId: "project-a", project: { name: "Project A" } };
it("blocks a silent move to another project with an HTTP 409 conflict", () => {
let conflict: unknown;
try {
assertExplicitRepositoryTransfer(existing, "project-b", "Acme/platform", false);
} catch (error) {
conflict = error;
}
expect(conflict).toBeInstanceOf(GiteaRepositoryOwnershipConflictError);
expect(integrationError(conflict)).toMatchObject({
status: 409,
code: "GITEA_REPOSITORY_ALREADY_LINKED",
});
});
it("allows same-project updates and an explicitly confirmed transfer", () => {
expect(() => assertExplicitRepositoryTransfer(existing, "project-a", "Acme/platform", false)).not.toThrow();
expect(() => assertExplicitRepositoryTransfer(existing, "project-b", "Acme/platform", true)).not.toThrow();
});
it("blocks even an explicit transfer when delivery history would cross project boundaries", () => {
let conflict: unknown;
try {
assertRepositoryTransferHasNoDeliveryHistory("Acme/platform", 2);
} catch (error) {
conflict = error;
}
expect(conflict).toBeInstanceOf(GiteaRepositoryTransferBlockedError);
expect(integrationError(conflict)).toMatchObject({
status: 409,
code: "GITEA_REPOSITORY_TRANSFER_BLOCKED",
});
expect(() => assertRepositoryTransferHasNoDeliveryHistory("Acme/platform", 0)).not.toThrow();
});
});
@@ -0,0 +1,27 @@
import { ReviewStatus } from "@prisma/client";
import { describe, expect, it } from "vitest";
import { reviewRequiresHumanAttention } from "../src/repositories/reviewAttentionPolicy.js";
const review = (overrides: Partial<{ status: ReviewStatus; source: string; sourceType: string; createdBy: string }> = {}) => ({
status: ReviewStatus.pending,
source: "projectbrain_runtime",
sourceType: "documentation_update",
createdBy: "api",
...overrides,
});
describe("review attention policy", () => {
it("keeps genuine unresolved reviews visible", () => {
expect(reviewRequiresHumanAttention(review())).toBe(true);
});
it("keeps seed and smoke evidence out of the owner's attention count", () => {
expect(reviewRequiresHumanAttention(review({ createdBy: "seed" }))).toBe(false);
expect(reviewRequiresHumanAttention(review({ source: "rc41-smoke-proof" }))).toBe(false);
expect(reviewRequiresHumanAttention(review({ sourceType: "SMOKE_IMPORT" }))).toBe(false);
});
it("requires an unresolved status", () => {
expect(reviewRequiresHumanAttention(review({ status: ReviewStatus.accepted }))).toBe(false);
});
});
@@ -0,0 +1,98 @@
import { afterEach, describe, expect, it } from "vitest";
import { buildApp } from "../src/app.js";
import { loadEnv } from "../src/config/env.js";
const ownerPassword = "correct-horse-battery-staple";
const appEnv = () => loadEnv({
NODE_ENV: "test",
LOG_LEVEL: "silent",
PROJECTBRAIN_DATA_SOURCE: "fixture",
PROJECTBRAIN_OWNER_USERNAME: "owner",
PROJECTBRAIN_OWNER_PASSWORD: ownerPassword,
PROJECTBRAIN_AUTH_SECRET: "test-secret-with-at-least-thirty-two-characters",
PROJECTBRAIN_SESSION_TTL_HOURS: "1",
PROJECTBRAIN_TRUST_PROXY: "false",
});
const apps: Awaited<ReturnType<typeof buildApp>>[] = [];
afterEach(async () => {
await Promise.all(apps.splice(0).map((app) => app.close()));
});
describe("capture and review state machines", () => {
it("classifies once, queues once, and keeps final review decisions immutable", async () => {
const app = await buildApp({ env: appEnv() });
apps.push(app);
const login = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "owner", password: ownerPassword },
});
const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session");
const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`;
const csrfToken = login.json().data.csrfToken as string;
let operation = 0;
const mutate = (method: "POST" | "PATCH", url: string, payload: Record<string, unknown> = {}) =>
app.inject({
method,
url,
payload,
headers: {
cookie,
"x-csrf-token": csrfToken,
"idempotency-key": `review-state-machine-${++operation}`,
},
});
const capture = await mutate("POST", "/api/projects/widefrog/capture-items", {
type: "note",
content: "A decision that must pass through one governed review.",
source: "state-machine-test",
});
expect(capture.statusCode).toBe(201);
const captureId = capture.json().data.id as string;
const prematureQueue = await mutate("POST", `/api/projects/widefrog/capture-items/${captureId}/send-to-review`);
expect(prematureQueue.statusCode).toBe(409);
expect(prematureQueue.json().error.code).toBe("CAPTURE_NOT_CLASSIFIED");
expect((await mutate("PATCH", `/api/projects/widefrog/capture-items/${captureId}/classify`)).statusCode).toBe(200);
const queued = await mutate("POST", `/api/projects/widefrog/capture-items/${captureId}/send-to-review`);
expect(queued.statusCode).toBe(201);
const reviewId = queued.json().data.createdEntityId as string;
const duplicateQueue = await mutate("POST", `/api/projects/widefrog/capture-items/${captureId}/send-to-review`);
expect(duplicateQueue.statusCode).toBe(409);
expect(duplicateQueue.json().error.code).toBe("CAPTURE_ALREADY_IN_REVIEW");
const reclassify = await mutate("PATCH", `/api/projects/widefrog/capture-items/${captureId}/classify`);
expect(reclassify.statusCode).toBe(409);
expect(reclassify.json().error.code).toBe("CAPTURE_ALREADY_IN_REVIEW");
const edited = await mutate("PATCH", `/api/projects/widefrog/review-queue/${reviewId}`, {
description: "Edited review content that still needs a final decision.",
});
expect(edited.statusCode).toBe(200);
expect(edited.json().data.description).toContain("Edited review content");
const invalidMerge = await mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/merge`, {
targetEntityId: "does-not-exist",
});
expect(invalidMerge.statusCode).toBe(409);
expect(invalidMerge.json().error.code).toBe("REVIEW_TARGET_NOT_FOUND");
const unsupportedConversion = await mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/convert`, {
target: "decision",
});
expect(unsupportedConversion.statusCode).toBe(501);
expect(unsupportedConversion.json().error.code).toBe("DATABASE_MODE_REQUIRED");
expect((await mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/accept`)).statusCode).toBe(200);
const overwrite = await mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/reject`);
expect(overwrite.statusCode).toBe(409);
expect(overwrite.json().error.code).toBe("REVIEW_ALREADY_DECIDED");
const editFinal = await mutate("PATCH", `/api/projects/widefrog/review-queue/${reviewId}`, { notes: "overwrite" });
expect(editFinal.statusCode).toBe(409);
expect(editFinal.json().error.code).toBe("REVIEW_ALREADY_DECIDED");
});
});
+96
View File
@@ -0,0 +1,96 @@
import Fastify from "fastify";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { loadEnv } from "../src/config/env.js";
import type { AutonomyRuntimeSnapshot } from "../src/services/autonomyRuntime.js";
import { registerSystemReadinessRoutes } from "../src/routes/systemReadiness.js";
const mocks = vi.hoisted(() => ({ runtime: vi.fn(), query: vi.fn(), worker: vi.fn() }));
vi.mock("../src/services/autonomyRuntime.js", () => ({ loadAutonomyRuntime: mocks.runtime }));
vi.mock("../src/db/prisma.js", () => ({ prisma: {
$queryRaw: mocks.query,
project: { count: vi.fn(async () => 1) },
blueprintGraph: { count: vi.fn(async () => 1) },
portfolioRepository: { count: vi.fn(async () => 1) },
giteaRepositoryLink: { count: vi.fn(async () => 1) },
analysisWorkerHeartbeat: { findUnique: mocks.worker },
analysisJob: { count: vi.fn(async () => 0) },
} }));
const snapshot = (): AutonomyRuntimeSnapshot => ({
executors: [], projects: [],
counts: { connectedExecutors: 0, enabledProjects: 0, claimableProjects: 0,
activeProjects: 0, pendingWorkItems: 0, blockedProjects: 0,
waitingRoadmapProjects: 0, stalledProjects: 0 },
});
const apps: ReturnType<typeof Fastify>[] = [];
const requestReadiness = async (runtime: AutonomyRuntimeSnapshot) => {
mocks.runtime.mockResolvedValue(runtime);
const app = Fastify();
apps.push(app);
await registerSystemReadinessRoutes(app, { env: loadEnv({
NODE_ENV: "test", PROJECTBRAIN_DATA_SOURCE: "database",
DATABASE_URL: "postgresql://fixture:fixture@localhost:5432/fixture",
PROJECTBRAIN_OWNER_USERNAME: "fixture-owner",
PROJECTBRAIN_OWNER_PASSWORD: "fixture-only-owner-password",
PROJECTBRAIN_AUTH_SECRET: "fixture-only-auth-secret-at-least-32-characters",
}) });
const response = await app.inject({ method: "GET", url: "/api/system/readiness" });
expect(response.statusCode).toBe(200);
return response.json().data;
};
beforeEach(() => {
vi.clearAllMocks();
mocks.query.mockResolvedValue([{ count: 1n }]);
mocks.worker.mockResolvedValue({ status: "ready", lastSeenAt: new Date() });
});
afterEach(async () => { await Promise.all(apps.splice(0).map((app) => app.close())); });
describe("autonomy readiness demand", () => {
it.each([false, true])("does not require an executor when all Auto work is paused (historical heartbeat: %s)", async (historical) => {
const runtime = snapshot();
if (historical) runtime.executor = { status: "stale", instanceId: "old-web-task",
startedAt: "2026-01-01T00:00:00.000Z", lastSeenAt: "2026-01-01T00:00:00.000Z" };
const data = await requestReadiness(runtime);
expect(data.status).toBe("ready");
expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "skip" });
expect(data.autonomyExecutor).toMatchObject({ status: "ready", connectedExecutors: 0 });
expect(data.autonomyExecutor.lastSeenAt).toBe(runtime.executor?.lastSeenAt);
});
it.each(["enabledProjects", "claimableProjects", "activeProjects", "pendingWorkItems"] as const)(
"preserves warning without an executor when %s is positive", async (count) => {
const runtime = snapshot();
runtime.counts[count] = 1;
const data = await requestReadiness(runtime);
expect(data.status).toBe("degraded");
expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "warn" });
expect(data.autonomyExecutor.connectedExecutors).toBe(0);
},
);
it("keeps fresh connected executor and pending work ready", async () => {
const runtime = snapshot();
runtime.counts.connectedExecutors = runtime.counts.enabledProjects = runtime.counts.pendingWorkItems = 1;
const data = await requestReadiness(runtime);
expect(data.status).toBe("ready");
expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "pass" });
});
it("retains a stale heartbeat warning when enabled work still needs the executor", async () => {
const runtime = snapshot();
runtime.counts.enabledProjects = runtime.counts.pendingWorkItems = 1;
runtime.executor = { status: "stale", instanceId: "old-web-task",
startedAt: "2026-01-01T00:00:00.000Z", lastSeenAt: "2026-01-01T00:00:00.000Z" };
const data = await requestReadiness(runtime);
expect(data.status).toBe("degraded");
expect(data.autonomyExecutor).toMatchObject({ status: "degraded", connectedExecutors: 0,
lastSeenAt: runtime.executor.lastSeenAt });
expect(data.checks.find((check: { id: string }) => check.id === "autonomy-executor")).toMatchObject({ status: "warn" });
});
it("does not suppress other readiness warnings while Auto is paused", async () => {
mocks.worker.mockResolvedValue(null);
expect((await requestReadiness(snapshot())).status).toBe("degraded");
});
});