import { mkdir, mkdtemp, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { ulwLoopBriefPath, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js"; import { addUlwLoopGoal, createUlwLoopPlan, deriveGoalCandidates, seedDefaultSuccessCriteria, startNextUlwLoop, summarizeUlwLoopPlan, } from "../src/plan-crud.js"; import { writePlan } from "../src/plan-io.js"; import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; async function makeRepo(): Promise { return mkdtemp(join(tmpdir(), "ug-crud-")); } async function readBriefFixture(): Promise { return readFile(join(process.cwd(), "test", "fixtures", "sample-brief.md"), "utf8"); } async function ledgerKinds(repoRoot: string): Promise { const raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8"); return raw .split(/\r?\n/) .filter(Boolean) .map((line) => JSON.parse(line).kind); } function criterion(status: UlwLoopSuccessCriterion["status"]): UlwLoopSuccessCriterion { const [base] = seedDefaultSuccessCriteria(0, "Implement auth endpoint"); if (base === undefined) throw new Error("expected seeded criterion"); return { ...base, status }; } function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Build auth service", objective: "Implement JWT auth endpoint", status: "pending", successCriteria: seedDefaultSuccessCriteria(0, "Implement JWT auth endpoint"), attempt: 0, createdAt: NOW, updatedAt: NOW, ...overrides, }; } function makePlan(goals: UlwLoopItem[]): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, briefPath: ".omo/ulw-loop/brief.md", goalsPath: ".omo/ulw-loop/goals.json", ledgerPath: ".omo/ulw-loop/ledger.jsonl", codexGoalMode: "aggregate", goals, }; } function scheduled(result: Awaited>) { if ("done" in result) throw new Error("expected scheduled goal"); return result; } describe("seedDefaultSuccessCriteria", () => { it("produces 3 criteria with C001/C002/C003 ids", () => { const cs = seedDefaultSuccessCriteria(0, "Implement auth endpoint"); expect(cs).toHaveLength(3); expect(cs.map((c) => c.id)).toEqual(["C001", "C002", "C003"]); }); it("covers happy + edge + regression user models", () => { const cs = seedDefaultSuccessCriteria(0, "Implement auth endpoint"); expect(cs.map((c) => c.userModel).sort()).toEqual(["edge", "happy", "regression"]); }); it("seeds all criteria as pending with null capturedEvidence", () => { const cs = seedDefaultSuccessCriteria(0, "Implement auth endpoint"); for (const c of cs) { expect(c.status).toBe("pending"); expect(c.capturedEvidence).toBeNull(); } }); }); describe("createUlwLoopPlan", () => { it("creates .omo/ulw-loop/{brief.md, goals.json, ledger.jsonl} in repoRoot", async () => { const repoRoot = await makeRepo(); const brief = await readBriefFixture(); await createUlwLoopPlan(repoRoot, { brief }); expect(await readFile(ulwLoopBriefPath(repoRoot), "utf8")).toBe(brief.endsWith("\n") ? brief : `${brief}\n`); expect(await readFile(ulwLoopGoalsPath(repoRoot), "utf8")).toContain("G001-build-the-jwt-auth-endpoint"); expect(await ledgerKinds(repoRoot)).toEqual(["plan_created"]); }); it("seeds at least 3 successCriteria per goal", async () => { const plan = await createUlwLoopPlan(await makeRepo(), { brief: await readBriefFixture() }); expect(plan.goals).toHaveLength(3); expect(plan.goals.every((goal) => goal.successCriteria.length >= 3)).toBe(true); }); it("refuses overwrite of an existing plan without --force", async () => { const repoRoot = await makeRepo(); await createUlwLoopPlan(repoRoot, { brief: "first" }); await expect(createUlwLoopPlan(repoRoot, { brief: "second" })).rejects.toThrow(UlwLoopError); await expect(createUlwLoopPlan(repoRoot, { brief: "second" })).rejects.toThrow("Refusing to overwrite"); }); it("aggregate is the default codexGoalMode", async () => { const plan = await createUlwLoopPlan(await makeRepo(), { brief: "Ship the feature" }); expect(plan.codexGoalMode).toBe("aggregate"); expect(plan.codexObjective).toContain(".omo/ulw-loop/goals.json"); }); }); describe("deriveGoalCandidates", () => { it("extracts bullets as goals", () => { expect(deriveGoalCandidates("# Brief\n\n- Build auth\n- Add tests")).toEqual([ { title: "Build auth", objective: "Build auth" }, { title: "Add tests", objective: "Add tests" }, ]); }); it("falls back to paragraph parsing when no bullets", () => { expect(deriveGoalCandidates("First objective.\n\nSecond objective.").map((goal) => goal.objective)).toEqual([ "First objective.", "Second objective.", ]); }); it("returns single default goal for empty/whitespace brief", () => { expect(deriveGoalCandidates(" \n\t ")).toEqual([ { title: "Complete the requested project objective.", objective: "Complete the requested project objective." }, ]); }); }); describe("addUlwLoopGoal", () => { it("appends a new goal to plan with seeded successCriteria", async () => { const repoRoot = await makeRepo(); await createUlwLoopPlan(repoRoot, { brief: "Build auth" }); const { plan, goal } = await addUlwLoopGoal(repoRoot, { title: "Add rate limit", objective: "Throttle login" }); expect(plan.goals).toHaveLength(2); expect(goal.id).toBe("G002-add-rate-limit"); expect(goal.successCriteria).toHaveLength(3); }); it("appends a ledger entry for goal_added", async () => { const repoRoot = await makeRepo(); await createUlwLoopPlan(repoRoot, { brief: "Build auth" }); await addUlwLoopGoal(repoRoot, { title: "Add rate limit", objective: "Throttle login" }); expect(await ledgerKinds(repoRoot)).toEqual(["plan_created", "goal_added"]); }); }); describe("startNextUlwLoop", () => { it("picks the first pending goal", async () => { const repoRoot = await makeRepo(); await createUlwLoopPlan(repoRoot, { brief: "- First\n- Second" }); const result = scheduled(await startNextUlwLoop(repoRoot, {})); expect(result.goal.id).toBe("G001-first"); expect(result.goal.status).toBe("in_progress"); expect(result.resumed).toBe(false); }); it("resumes the in_progress goal when one exists", async () => { const repoRoot = await makeRepo(); const plan = await createUlwLoopPlan(repoRoot, { brief: "- First\n- Second" }); const active = makeGoal({ ...plan.goals[1], status: "in_progress" }); await writePlan(repoRoot, { ...plan, goals: [makeGoal({ ...plan.goals[0] }), active], activeGoalId: active.id }); const result = scheduled(await startNextUlwLoop(repoRoot, {})); expect(result.goal.id).toBe(active.id); expect(result.resumed).toBe(true); }); it("with retryFailed picks first failed (non-blocked) goal", async () => { const repoRoot = await makeRepo(); const failed = makeGoal({ status: "failed", failureReason: "flake" }); await mkdir(join(repoRoot, ".omo", "ulw-loop"), { recursive: true }); await writePlan(repoRoot, makePlan([failed])); const result = scheduled(await startNextUlwLoop(repoRoot, { retryFailed: true })); expect(result.goal.id).toBe("G001"); expect(result.goal.attempt).toBe(1); expect(await ledgerKinds(repoRoot)).toEqual(["goal_retried", "goal_started"]); }); it("returns { done: true } when no eligible goals remain", async () => { const repoRoot = await makeRepo(); await mkdir(join(repoRoot, ".omo", "ulw-loop"), { recursive: true }); await writePlan(repoRoot, makePlan([makeGoal({ status: "complete" })])); const result = await startNextUlwLoop(repoRoot, {}); expect(result).toMatchObject({ done: true }); }); }); describe("summarizeUlwLoopPlan", () => { it("counts goals by status", () => { const plan = makePlan([ makeGoal({ id: "G001", status: "pending" }), makeGoal({ id: "G002", status: "in_progress" }), makeGoal({ id: "G003", status: "complete" }), makeGoal({ id: "G004", status: "failed" }), makeGoal({ id: "G005", status: "blocked", steeringStatus: "blocked" }), makeGoal({ id: "G006", status: "review_blocked" }), makeGoal({ id: "G007", status: "needs_user_decision", steeringStatus: "superseded" }), ]); expect(summarizeUlwLoopPlan(plan)).toMatchObject({ total: 7, pending: 1, in_progress: 1, complete: 1, failed: 1, blocked: 1, review_blocked: 1, needs_user_decision: 1, superseded: 1, }); }); it("aggregates criteria pass/pending/fail/blocked across all goals", () => { const plan = makePlan([ makeGoal({ successCriteria: [criterion("pass"), criterion("pending")] }), makeGoal({ id: "G002", successCriteria: [criterion("fail"), criterion("blocked"), criterion("pending")] }), ]); expect(summarizeUlwLoopPlan(plan).criteria).toEqual({ total: 5, pass: 1, pending: 2, fail: 1, blocked: 1 }); }); });