153 lines
8.8 KiB
TypeScript
153 lines
8.8 KiB
TypeScript
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);
|
|
});
|