166 lines
14 KiB
TypeScript
166 lines
14 KiB
TypeScript
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));
|
|
});
|
|
});
|