187 lines
5.2 KiB
TypeScript
187 lines
5.2 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 { 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",
|
|
});
|
|
});
|