195 lines
7.2 KiB
TypeScript
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" });
|
|
});
|
|
});
|