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"; function raw(overrides: Record = {}) { 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("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("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 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]/); });