agent-r/test/orchestrator.test.ts

138 lines
5 KiB
TypeScript

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 { 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<T>(request: AgentInvocation<T>): Promise<AgentInvocationResult<T>> {
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<string, unknown>;
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"],
},
],
}),
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: [],
}),
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");
});
});