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 { promisify } from "node:util"; import { UiRunService, readRunMetaBySession } from "../src/ui/run-service.js"; 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", }); });