Files
projectbrain-Public/backend/test/repository-autonomy.test.ts

195 lines
7.2 KiB
TypeScript

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