feat: add assistant proposal worker runtime
This commit is contained in:
+47
-2
@@ -1,4 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { AcpWorker } from "../src/acp/worker.js";
|
||||
@@ -7,10 +9,10 @@ import { BotProfileResolver } from "../src/roles/role-registry.js";
|
||||
|
||||
const fixture = path.resolve("test/fixtures/fake-acp-agent.mjs");
|
||||
|
||||
function makeWorker(promptTimeoutMs = 80) {
|
||||
function makeWorker(promptTimeoutMs = 80, env: Record<string, string> = {}) {
|
||||
const config = parseConfig({
|
||||
configVersion: 3,
|
||||
bot: { id: "test-bot", workspace: path.resolve("."), persona: "", agent: { id: "fake", command: process.execPath, args: [fixture] }, permissions: { mode: "deny" } },
|
||||
bot: { id: "test-bot", workspace: path.resolve("."), persona: "", agent: { id: "fake", command: process.execPath, args: [fixture], env }, permissions: { mode: "deny" } },
|
||||
gateway: { platform: { type: "webhook", secret: "secret" } },
|
||||
runtime: { acp: { promptTimeoutMs, cancelGraceMs: 30 } }
|
||||
});
|
||||
@@ -33,3 +35,46 @@ test("worker reports an unexpected ACP subprocess exit", async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
assert.equal(current.crashes(), 1);
|
||||
});
|
||||
|
||||
test("immediate termination kills ACP descendants in the worker process group", async (t) => {
|
||||
await assertDescendantTermination(t, (worker) => { worker.terminateImmediately(); });
|
||||
});
|
||||
|
||||
test("normal termination SIGKILLs descendants that ignore SIGTERM", async (t) => {
|
||||
await assertDescendantTermination(t, (worker) => worker.terminate());
|
||||
});
|
||||
|
||||
test("persisted worker token safely identifies and kills an orphaned process group", async () => {
|
||||
const { worker } = makeWorker(2_000);
|
||||
await worker.start();
|
||||
assert.ok(worker.processGroup);
|
||||
await AcpWorker.terminatePersistedGroup(worker.processGroup, 100);
|
||||
assert.equal(processGroupExists(worker.processGroup.pgid), false);
|
||||
});
|
||||
|
||||
async function assertDescendantTermination(t: test.TestContext, terminate: (worker: AcpWorker) => void | Promise<void>): Promise<void> {
|
||||
const directory = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gori-acp-group-"));
|
||||
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
||||
const pidFile = path.join(directory, "descendant.pid");
|
||||
const { worker } = makeWorker(2_000, { FAKE_ACP_DESCENDANT_PID_FILE: pidFile });
|
||||
await worker.start();
|
||||
const prompt = worker.prompt("spawn descendant");
|
||||
const promptRejected = assert.rejects(prompt);
|
||||
for (let count = 0; count < 100 && !fs.existsSync(pidFile); count++) await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const pid = Number(fs.readFileSync(pidFile, "utf8").trim());
|
||||
assert.equal(processExists(pid), true);
|
||||
await terminate(worker);
|
||||
await promptRejected;
|
||||
for (let count = 0; count < 100 && processExists(pid); count++) await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
assert.equal(processExists(pid), false);
|
||||
}
|
||||
|
||||
function processExists(pid: number): boolean {
|
||||
try { process.kill(pid, 0); return true; }
|
||||
catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
|
||||
}
|
||||
|
||||
function processGroupExists(pgid: number): boolean {
|
||||
try { process.kill(-pgid, 0); return true; }
|
||||
catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user