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
+57 -21
View File
@@ -1,31 +1,67 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import { parseConfig } from "../src/config.js";
const platforms = {
qq: { enabled: true, appId: "id", clientSecret: "secret", connectionMode: "websocket" as const },
feishu: {}, wecom: {}, webhook: {}, weixin: {}
};
function raw(overrides: Record<string, unknown> = {}) {
return {
configVersion: 3,
bot: {
id: "test-bot", workspace: "/tmp", persona: "test",
agent: { id: "kimi", command: "kimi", args: ["acp"], env: {} },
skills: [], permissions: { mode: "deny", allowedTools: [], allowedCommandPatterns: [] }
},
gateway: { platform: { type: "webhook", secret: "secret" } },
runtime: {},
...overrides
};
}
test("migrates a v1 Kimi config and preserves QQ fields", () => {
const config = parseConfig({
server: { port: 8787 }, policy: {}, defaultAgent: "kimi",
agents: [{ name: "kimi", command: "/home/ubuntu/.kimi-code/bin/kimi", args: ["-p"], cwd: "/tmp" }], platforms
});
assert.equal(config.configVersion, 2);
assert.deepEqual(config.backends[0].args, ["acp"]);
assert.equal(config.roles[0].workspace, "/tmp");
assert.equal(config.platforms.qq.clientSecret, "secret");
assert.equal(config.platforms.qq.connectionMode, "websocket");
test("parses Config v3 defaults for one Bot, agent, and platform", () => {
const config = parseConfig(raw());
assert.equal(config.configVersion, 3);
assert.equal(config.bot.id, "test-bot");
assert.equal(config.bot.agent.id, "kimi");
assert.equal(config.gateway.platform.type, "webhook");
assert.equal(config.runtime.acp.promptTimeoutMs, 7_200_000);
assert.equal(config.bot.permissions.mode, "deny");
});
test("does not silently migrate a non-Kimi CLI agent", () => {
assert.throws(() => parseConfig({ defaultAgent: "echo", agents: [{ name: "echo", command: "node" }], platforms }), /only supports a Kimi/);
test("explicitly rejects non-v3 configuration and unknown fields", () => {
assert.throws(() => parseConfig({ configVersion: 2 }), /requires Config v3/);
assert.throws(() => parseConfig({ defaultAgent: "kimi" }), /requires Config v3/);
assert.throws(() => parseConfig(raw({ bot: { ...(raw().bot as object), legacyRole: "ops" } })), /Unrecognized key/);
assert.throws(() => parseConfig(raw({ gateway: { platform: { type: "webhook", secret: "secret", enabled: true } } })), /Unrecognized key/);
});
test("validates role references and absolute workspace", () => {
assert.throws(() => parseConfig({
configVersion: 2, backends: [{ id: "kimi", command: "kimi", args: ["acp"] }], defaultRole: "a",
roles: [{ id: "a", backend: "missing", workspace: "relative" }], platforms
}), /workspace must be absolute/);
test("validates bot ID, absolute workspace, skills, and command regex", () => {
assert.throws(() => parseConfig(raw({ bot: { ...(raw().bot as object), id: "../bad" } })), /bot\.id/);
assert.throws(() => parseConfig(raw({ bot: { ...(raw().bot as object), workspace: "relative" } })), /workspace must be absolute/);
assert.throws(() => parseConfig(raw({ bot: { ...(raw().bot as object), skills: [{ id: "x", file: "relative" }] } })), /file must be absolute/);
assert.throws(() => parseConfig(raw({ bot: { ...(raw().bot as object), permissions: { mode: "allowlist", allowedTools: ["bash"], allowedCommandPatterns: ["["] } } })), /invalid command pattern/);
});
test("validates gateway server host and port", () => {
for (const host of ["localhost", "0.0.0.0", "::1", "gateway.example.com"]) {
assert.equal(parseConfig(raw({ gateway: { server: { host, port: 8787 }, platform: { type: "webhook", secret: "secret" } } })).gateway.server.host, host);
}
for (const host of ["http://localhost", "localhost:8787", "[::1]", "bad host", "host/path"]) {
assert.throws(() => parseConfig(raw({ gateway: { server: { host, port: 8787 }, platform: { type: "webhook", secret: "secret" } } })), /server host/);
}
for (const port of [0, 65_536, 1.5]) {
assert.throws(() => parseConfig(raw({ gateway: { server: { host: "localhost", port }, platform: { type: "webhook", secret: "secret" } } })), /gateway/);
}
});
test("config.example.json is a parseable, secret-free documentation template", () => {
const text = fs.readFileSync(path.resolve("config.example.json"), "utf8");
const config = parseConfig(JSON.parse(text) as unknown);
assert.equal(config.configVersion, 3);
assert.equal(config.bot.id, "BOT_ID");
assert.equal(config.bot.permissions.mode, "deny");
assert.equal(config.runtime.acp.promptTimeoutMs, 7_200_000);
assert.equal(config.gateway.platform.type, "qq");
assert.match(text, /QQ_CLIENT_SECRET/);
assert.doesNotMatch(text, /configVersion"\s*:\s*[12]/);
});