Files
ai_ops/tests/run-service.test.ts

238 lines
7.4 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdtemp, mkdir, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { UiRunService, readRunMetaBySession } from "../src/runs/run-service.js";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
async function waitForTerminalRun(
runService: UiRunService,
runId: string,
): Promise<"success" | "failure" | "cancelled"> {
const maxPolls = 100;
for (let index = 0; index < maxPolls; index += 1) {
const run = runService.getRun(runId);
if (run && run.status !== "running") {
return run.status;
}
await new Promise((resolveWait) => setTimeout(resolveWait, 20));
}
throw new Error("Run did not reach a terminal status within polling window.");
}
test("run service persists failure when pipeline summary is failure", async () => {
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-run-service-"));
const stateRoot = resolve(workspaceRoot, "state");
const projectContextPath = resolve(workspaceRoot, "project-context.json");
const envPath = resolve(workspaceRoot, ".env");
await writeFile(
envPath,
[
`AGENT_STATE_ROOT=${stateRoot}`,
`AGENT_PROJECT_CONTEXT_PATH=${projectContextPath}`,
].join("\n"),
"utf8",
);
const runService = new UiRunService({
workspaceRoot,
envFilePath: ".env",
});
const manifest = {
schemaVersion: "1",
topologies: ["sequential"],
personas: [
{
id: "writer",
displayName: "Writer",
systemPromptTemplate: "Write the draft",
toolClearance: {
allowlist: ["read_file", "write_file"],
banlist: [],
},
},
],
relationships: [],
topologyConstraints: {
maxDepth: 1,
maxRetries: 0,
},
pipeline: {
entryNodeId: "write-node",
nodes: [
{
id: "write-node",
actorId: "writer-actor",
personaId: "writer",
topology: {
kind: "sequential",
},
constraints: {
maxRetries: 0,
},
},
],
edges: [],
},
};
const started = await runService.startRun({
prompt: "force validation failure on first attempt",
manifest,
executionMode: "mock",
simulateValidationNodeIds: ["write-node"],
});
const terminalStatus = await waitForTerminalRun(runService, started.runId);
assert.equal(terminalStatus, "failure");
const persisted = await readRunMetaBySession({
stateRoot,
sessionId: started.sessionId,
});
assert.equal(persisted?.status, "failure");
});
test("run service creates, runs, and closes explicit sessions", async () => {
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-run-service-session-"));
const stateRoot = resolve(workspaceRoot, "state");
const envPath = resolve(workspaceRoot, ".env");
const projectPath = resolve(workspaceRoot, "project");
await mkdir(projectPath, { recursive: true });
await execFileAsync("git", ["init", projectPath], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "config", "user.name", "AI Ops"], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "config", "user.email", "ai-ops@example.local"], { encoding: "utf8" });
await writeFile(resolve(projectPath, "README.md"), "# project\n", "utf8");
await execFileAsync("git", ["-C", projectPath, "add", "README.md"], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "commit", "-m", "initial"], { encoding: "utf8" });
await writeFile(
envPath,
[
`AGENT_STATE_ROOT=${stateRoot}`,
"AGENT_WORKTREE_ROOT=.ai_ops/worktrees",
"AGENT_WORKTREE_BASE_REF=HEAD",
].join("\n"),
"utf8",
);
const runService = new UiRunService({
workspaceRoot,
envFilePath: ".env",
});
const createdSession = await runService.createSession({
projectPath,
});
assert.equal(createdSession.sessionStatus, "active");
const manifest = {
schemaVersion: "1",
topologies: ["sequential"],
personas: [
{
id: "writer",
displayName: "Writer",
systemPromptTemplate: "Write draft",
toolClearance: {
allowlist: ["read_file", "write_file"],
banlist: [],
},
},
],
relationships: [],
topologyConstraints: {
maxDepth: 1,
maxRetries: 0,
},
pipeline: {
entryNodeId: "write-node",
nodes: [
{
id: "write-node",
actorId: "writer-actor",
personaId: "writer",
},
],
edges: [],
},
};
const started = await runService.startRun({
prompt: "complete task",
manifest,
sessionId: createdSession.sessionId,
executionMode: "mock",
});
const terminalStatus = await waitForTerminalRun(runService, started.runId);
assert.equal(terminalStatus, "success");
const closed = await runService.closeSession({
sessionId: createdSession.sessionId,
});
assert.equal(closed.sessionStatus, "closed");
await assert.rejects(() => stat(createdSession.baseWorkspacePath), {
code: "ENOENT",
});
});
test("run service marks session closed_with_conflicts when close merge conflicts", async () => {
const workspaceRoot = await mkdtemp(resolve(tmpdir(), "ai-ops-run-service-close-conflict-"));
const stateRoot = resolve(workspaceRoot, "state");
const envPath = resolve(workspaceRoot, ".env");
const projectPath = resolve(workspaceRoot, "project");
await mkdir(projectPath, { recursive: true });
await execFileAsync("git", ["init", projectPath], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "config", "user.name", "AI Ops"], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "config", "user.email", "ai-ops@example.local"], { encoding: "utf8" });
await writeFile(resolve(projectPath, "README.md"), "base\n", "utf8");
await execFileAsync("git", ["-C", projectPath, "add", "README.md"], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "commit", "-m", "initial"], { encoding: "utf8" });
await writeFile(
envPath,
[
`AGENT_STATE_ROOT=${stateRoot}`,
"AGENT_WORKTREE_ROOT=.ai_ops/worktrees",
"AGENT_WORKTREE_BASE_REF=HEAD",
].join("\n"),
"utf8",
);
const runService = new UiRunService({
workspaceRoot,
envFilePath: ".env",
});
const createdSession = await runService.createSession({
projectPath,
});
await writeFile(resolve(createdSession.baseWorkspacePath, "README.md"), "base branch update\n", "utf8");
await execFileAsync("git", ["-C", createdSession.baseWorkspacePath, "add", "README.md"], { encoding: "utf8" });
await execFileAsync("git", ["-C", createdSession.baseWorkspacePath, "commit", "-m", "base update"], { encoding: "utf8" });
await writeFile(resolve(projectPath, "README.md"), "project branch update\n", "utf8");
await execFileAsync("git", ["-C", projectPath, "add", "README.md"], { encoding: "utf8" });
await execFileAsync("git", ["-C", projectPath, "commit", "-m", "project update"], { encoding: "utf8" });
const closed = await runService.closeSession({
sessionId: createdSession.sessionId,
mergeToProject: true,
});
assert.equal(closed.sessionStatus, "closed_with_conflicts");
const baseWorkspaceStats = await stat(createdSession.baseWorkspacePath);
assert.equal(baseWorkspaceStats.isDirectory(), true);
});