import { mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { describe, expect, test } from "vitest"; import { ArtifactManager } from "../src/artifacts.js"; import { loadConfig } from "../src/config.js"; import { AgentROrchestrator } from "../src/orchestrator.js"; import { strategyPlanSchema } from "../src/schema-catalog.js"; import { RunStore } from "../src/store.js"; import type { AgentInvocation, AgentInvocationResult, RawAgentRunner } from "../src/types.js"; class ScriptedRunner implements RawAgentRunner { public readonly prompts: string[] = []; constructor( private readonly outputs: string[], ) {} async invoke(request: AgentInvocation): Promise> { const next = this.outputs.shift(); if (!next) { throw new Error("No scripted output remaining."); } this.prompts.push(request.prompt); readFileSync(request.artifacts.schemaPath, "utf8"); const parsed = JSON.parse(next) as Record; if (typeof parsed.taskId === "string" && !parsed.taskId) { const match = request.prompt.match(/exact task id `([^`]+)`/); if (match) { parsed.taskId = match[1]; } } return { sessionId: request.sessionId ?? `session-${this.prompts.length}`, output: parsed as T, rawMessage: JSON.stringify(parsed), rawEvents: ['{"type":"turn.completed"}'], stderr: "", artifacts: request.artifacts, }; } } describe("AgentROrchestrator", () => { test("runs through plan, implementation, self-check, and independent check", async () => { const repoPath = mkdtempSync(path.join(tmpdir(), "agent-r-orchestrator-")); const config = loadConfig(repoPath); const store = new RunStore(config.databasePath); const artifacts = new ArtifactManager(config.runsDir); const runner = new ScriptedRunner([ JSON.stringify({ decision: "continue", summary: "Implement the CLI skeleton.", rationale: "The project is empty and needs a first vertical slice.", goalProgress: "No implementation exists yet.", risks: [], tasks: [ { title: "Create CLI entrypoint", objective: "Build the initial command surface.", acceptanceCriteria: ["A run command exists."], verificationSteps: ["Run the help command."], allowedPaths: ["src", "package.json"], }, ], blockedReason: null, }), JSON.stringify({ taskId: "", status: "completed", summary: "Added the CLI skeleton.", changes: ["Created an entrypoint."], verification: [{ command: "agent-r --help", outcome: "passed", details: "Help output rendered." }], followUps: [], touchedFiles: ["src/index.ts", "package.json"], blockers: [], }), JSON.stringify({ decision: "done", summary: "The requested slice is complete.", rationale: "The only planned task is complete and verified.", goalProgress: "The vertical slice exists.", risks: [], tasks: [], blockedReason: null, }), JSON.stringify({ readyForIndependentCheck: true, summary: "The run is ready for independent review.", rationale: "Implementation and verification are present.", evidence: ["CLI entrypoint exists."], remainingGaps: [], }), JSON.stringify({ verdict: "approved", summary: "The goal is satisfied.", rationale: "The requested slice exists and is verified.", evidence: ["CLI entrypoint present."], remainingTasks: [], }), ]); const orchestrator = new AgentROrchestrator(config, store, artifacts, runner); const run = orchestrator.createRun("Create a CLI skeleton"); const firstStatus = await orchestrator.runUntilStable(run.id, 10); const snapshot = store.buildSnapshot(run.id); expect(firstStatus.status).toBe("done"); expect(snapshot.completedTasks).toHaveLength(1); expect(snapshot.approvals).toHaveLength(2); expect(snapshot.pendingTasks).toHaveLength(0); expect(runner.prompts.at(1)).toContain("You must echo the exact task id"); }); test("strategy prompt explicitly permits read-only repository inspection", async () => { const repoPath = mkdtempSync(path.join(tmpdir(), "agent-r-strategy-")); const config = loadConfig(repoPath); const store = new RunStore(config.databasePath); const artifacts = new ArtifactManager(config.runsDir); const runner = new ScriptedRunner([ JSON.stringify({ decision: "blocked", summary: "Cannot continue.", rationale: "Test stop.", goalProgress: "None.", risks: [], tasks: [], blockedReason: "Stop immediately.", }), ]); const orchestrator = new AgentROrchestrator(config, store, artifacts, runner); const run = orchestrator.createRun("Inspect repo"); await orchestrator.runUntilStable(run.id, 1); expect(runner.prompts[0]).toContain("You may inspect the repository directly"); expect(runner.prompts[0]).toContain("Do not edit files"); }); test("strategy plan schema requires blockedReason and accepts null", () => { const valid = JSON.parse( JSON.stringify({ decision: "continue", summary: "Keep going.", rationale: "Work remains.", goalProgress: "Partial.", risks: [], tasks: [], blockedReason: null, }), ) as Record; expect(strategyPlanSchema.required).toContain("blockedReason"); expect(strategyPlanSchema.properties.blockedReason.type).toEqual(["string", "null"]); expect(valid.blockedReason).toBeNull(); }); });