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); });