Refactor runtime around isolated bot instances

This commit is contained in:
zenord
2026-08-16 23:45:09 +08:00
parent 914579a94c
commit f9fb4ef776
51 changed files with 2355 additions and 1459 deletions
+52 -13
View File
@@ -3,27 +3,31 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { AcpBackendRegistry } from "../src/acp/backend-registry.js";
import { AcpSessionManager } from "../src/acp/session-manager.js";
import { parseConfig } from "../src/config.js";
import { DurableSessionStore } from "../src/core/durable-session-store.js";
import { RoleRegistry } from "../src/roles/role-registry.js";
import { BotProfileResolver } from "../src/roles/role-registry.js";
const fixture = path.resolve("test/fixtures/fake-acp-agent.mjs");
function config(stateFile: string, logFile: string) {
function config(stateFile: string, logFile: string, persona = "test", agentEnv: Record<string, string> = {}) {
return parseConfig({
configVersion: 2, acp: { stateFile, promptTimeoutMs: 2_000, cancelGraceMs: 100, idleTimeoutMs: 100, sweepIntervalMs: 20, maxProcesses: 2 },
backends: [{ id: "kimi", command: process.execPath, args: [fixture], env: { FAKE_ACP_LOG: logFile } }],
skills: [], defaultRole: "assistant",
roles: [{ id: "assistant", backend: "kimi", workspace: path.resolve("."), persona: "test", skills: [], policy: { permissionMode: "deny" } }],
platforms: {}
configVersion: 3,
bot: {
id: "test-bot", workspace: path.resolve("."), persona,
agent: { id: "fake", command: process.execPath, args: [fixture], env: { FAKE_ACP_LOG: logFile, ...agentEnv } },
skills: [], permissions: { mode: "deny" }
},
gateway: { platform: { type: "qq", appId: "id", clientSecret: "secret", botNames: ["bot"] } },
runtime: { acp: { stateFile, promptTimeoutMs: 2_000, cancelGraceMs: 100, idleTimeoutMs: 100, sweepIntervalMs: 20, maxProcesses: 2 } }
});
}
async function runtime(stateFile: string, logFile: string) {
const cfg = config(stateFile, logFile); const store = new DurableSessionStore(stateFile); await store.open();
return { cfg, store, manager: new AcpSessionManager(cfg.acp, new AcpBackendRegistry(cfg.backends), new RoleRegistry(cfg), store) };
async function runtime(stateFile: string, logFile: string, persona = "test", agentEnv: Record<string, string> = {}) {
const cfg = config(stateFile, logFile, persona, agentEnv);
const store = new DurableSessionStore(stateFile, { botId: cfg.bot.id, platform: cfg.gateway.platform.type }); await store.open();
const bot = new BotProfileResolver(cfg).bot;
return { cfg, store, bot, manager: new AcpSessionManager(cfg.runtime.acp, bot, store) };
}
test("creates, persists, idles, and resumes the same native session", async () => {
@@ -31,7 +35,7 @@ test("creates, persists, idles, and resumes the same native session", async () =
const first = await runtime(state, log);
const response = await first.manager.prompt({ platform: "qq", chatId: "chat", userId: "user", text: "one" });
const sessionId = response.text.split(":")[1];
assert.match(response.text, /reply:fake-/);
assert.equal(response.botId, "test-bot"); assert.match(response.text, /reply:fake-/);
await new Promise((resolve) => setTimeout(resolve, 180));
await first.manager.prompt({ platform: "qq", chatId: "chat", userId: "user", text: "two" });
await first.manager.shutdown(); await first.store.close();
@@ -44,7 +48,33 @@ test("creates, persists, idles, and resumes the same native session", async () =
await second.manager.shutdown(); await second.store.close();
});
test("cancel reaches a hanging ACP prompt and new unbinds", async () => {
test("resume falls back to load and failed restore creates a new session", async () => {
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gori-acp-restore-"));
const state = path.join(dir, "state.json"); const log = path.join(dir, "fake.log");
const resumeFail = path.join(dir, "resume-fail"); const loadFail = path.join(dir, "load-fail");
const agentEnv = { FAKE_ACP_RESUME_FAIL_FILE: resumeFail, FAKE_ACP_LOAD_FAIL_FILE: loadFail };
const first = await runtime(state, log, "test", agentEnv);
const initial = await first.manager.prompt({ platform: "qq", chatId: "chat", userId: "user", text: "one" });
const initialSession = initial.text.split(":")[1];
await first.manager.shutdown(); await first.store.close();
await fs.promises.writeFile(resumeFail, "1");
const fallback = await runtime(state, log, "test", agentEnv);
const loaded = await fallback.manager.prompt({ platform: "qq", chatId: "chat", userId: "user", text: "two" });
assert.equal(loaded.text, `reply:${initialSession}:two`);
await fallback.manager.shutdown(); await fallback.store.close();
await fs.promises.writeFile(loadFail, "1");
const replacement = await runtime(state, log, "test", agentEnv);
const fresh = await replacement.manager.prompt({ platform: "qq", chatId: "chat", userId: "user", text: "three" });
assert.notEqual(fresh.text.split(":")[1], initialSession);
await replacement.manager.shutdown(); await replacement.store.close();
const entries = (await fs.promises.readFile(log, "utf8")).trim().split("\n").map(JSON.parse);
assert.ok(entries.some((entry) => entry.method === "session/load" && entry.sessionId === initialSession));
});
test("cancel reaches hanging prompt, reset unbinds, and fingerprint changes session", async () => {
const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "gori-acp-cancel-")); const state = path.join(dir, "state.json"); const log = path.join(dir, "fake.log");
const current = await runtime(state, log);
await current.manager.prompt({ platform: "qq", chatId: "chat", userId: "user", text: "ready" });
@@ -55,4 +85,13 @@ test("cancel reaches a hanging ACP prompt and new unbinds", async () => {
await current.manager.reset("qq", "chat");
assert.equal(current.store.stats().bindings, 0);
await current.manager.shutdown(); await current.store.close();
const original = await runtime(state, log, "one");
const first = await original.manager.prompt({ platform: "qq", chatId: "other", userId: "user", text: "first" });
const firstSession = first.text.split(":")[1];
await original.manager.shutdown(); await original.store.close();
const changed = await runtime(state, log, "two");
const second = await changed.manager.prompt({ platform: "qq", chatId: "other", userId: "user", text: "second" });
assert.notEqual(second.text.split(":")[1], firstSession);
await changed.manager.shutdown(); await changed.store.close();
});