Refactor runtime around isolated bot instances
This commit is contained in:
+57
-21
@@ -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]/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user