feat: add assistant proposal worker runtime

This commit is contained in:
zenord
2026-08-18 18:48:47 +08:00
parent 947f19e3cd
commit b4121c9fd6
32 changed files with 2827 additions and 986 deletions
+47 -2
View File
@@ -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"; }
}