Files
2026-08-17 00:49:29 +08:00

242 lines
9.6 KiB
TypeScript

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 type { DiscoveredBackend } from "../src/acp/discovery.js";
import { loadConfigFile, writeConfigFile } from "../src/cli/config-file.js";
import type { Choice, PromptSession } from "../src/cli/prompt.js";
import { runSetup } from "../src/cli/setup.js";
import { parseConfig } from "../src/config.js";
const testSecret = "test-webhook-secret";
const readyAgent: DiscoveredBackend = {
id: "test-agent",
command: process.execPath,
args: ["test/fixtures/fake-acp-agent.mjs"],
status: "ready"
};
class FakePrompt implements PromptSession {
readonly defaults = new Map<string, string | undefined>();
closeCount = 0;
constructor(
private readonly answers: Record<string, string[]> = {},
private readonly secretAnswer = testSecret
) {}
async ask(question: string, defaultValue?: string): Promise<string> {
this.defaults.set(question, defaultValue);
const queued = this.answers[question];
return queued && queued.length > 0 ? queued.shift()! : defaultValue || "";
}
async askSecret(): Promise<string> { return this.secretAnswer; }
async askBoolean(_question: string, defaultValue = false): Promise<boolean> { return defaultValue; }
async askList(_question: string, defaultValues: string[] = []): Promise<string[]> { return defaultValues; }
async choose<T>(_question: string, choices: Choice<T>[], defaultIndex = 0): Promise<T> {
return (choices.find((choice) => choice.label === "Generic webhook") || choices[defaultIndex] || choices[0]).value;
}
close(): void { this.closeCount++; }
}
function peerConfig(port: number): unknown {
return {
configVersion: 3,
bot: {
id: "peer-bot",
workspace: os.tmpdir(),
persona: "peer",
agent: { id: readyAgent.id, command: readyAgent.command, args: readyAgent.args, env: {} },
skills: [],
permissions: { mode: "deny", allowedTools: [], allowedCommandPatterns: [] }
},
gateway: {
server: { host: "127.0.0.1", port, publicBaseUrl: "" },
policy: { allowedUsers: [], allowedChats: [], requireMentionInGroup: true },
platform: { type: "webhook", secret: "peer-test-secret" }
},
runtime: {}
};
}
function existingConfig(host = "0.0.0.0", port = 8787) {
return parseConfig({
configVersion: 3,
bot: {
id: "existing-bot",
workspace: os.tmpdir(),
persona: "existing persona",
agent: {
id: "custom-kimi",
command: readyAgent.command,
args: ["-m", "gori-gpt/gpt-5.5", "acp"],
env: { TEST_AGENT_SETTING: "preserved" }
},
skills: [{ id: "test-skill", file: path.join(os.tmpdir(), "test-skill.md"), maxBytes: 1234 }],
permissions: { mode: "allowlist", allowedTools: ["Read"], allowedCommandPatterns: ["^true$"] }
},
gateway: {
server: { host, port, publicBaseUrl: "https://public.example.test" },
policy: { allowedUsers: ["allowed-user"], allowedChats: ["allowed-chat"], requireMentionInGroup: false },
platform: { type: "webhook", secret: testSecret }
},
runtime: { acp: { promptTimeoutMs: 123456, maxProcesses: 3 } }
});
}
test("new setup suggests the next unassigned instance port", async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gori-setup-new-"));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
for (const [name, port] of [["peer-one", 8787], ["peer-two", 8788]] as const) {
const directory = path.join(root, name);
fs.mkdirSync(directory);
fs.writeFileSync(path.join(directory, "config.json"), JSON.stringify(peerConfig(port)));
}
const target = path.join(root, "new-bot", "config.json");
const prompt = new FakePrompt();
const messages: string[] = [];
await runSetup(target, {
botId: "new-bot",
requireNew: true,
writeWithoutConfirmation: true,
prompt,
discoverAgents: async () => [readyAgent],
instancesDirectory: root,
log: (message) => messages.push(message)
});
const config = loadConfigFile(target).config;
assert.equal(prompt.defaults.get("Gateway server port"), "8789");
assert.equal(config.gateway.server.port, 8789);
assert.equal(config.gateway.server.host, "0.0.0.0");
assert.equal(fs.statSync(target).mode & 0o777, 0o600);
assert.equal(prompt.closeCount, 1);
assert.ok(messages.some((message) => message.includes("suggested port: 8789")));
assert.ok(messages.every((message) => !message.includes(testSecret)));
});
test("existing setup can change host and port while preserving public URL and secret", async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gori-setup-existing-"));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const target = path.join(root, "existing-bot", "config.json");
writeConfigFile(target, existingConfig());
const peerDirectory = path.join(root, "peer");
fs.mkdirSync(peerDirectory);
fs.writeFileSync(path.join(peerDirectory, "config.json"), JSON.stringify(peerConfig(8787)));
const prompt = new FakePrompt({
"Gateway server host": ["http://bad-host", "127.0.0.1"],
"Gateway server port": ["not-a-port", "9001"]
}, "");
const messages: string[] = [];
await runSetup(target, {
botId: "existing-bot",
writeWithoutConfirmation: true,
prompt,
discoverAgents: async () => [readyAgent],
instancesDirectory: root,
log: (message) => messages.push(message)
});
const config = loadConfigFile(target).config;
assert.equal(prompt.defaults.get("Gateway server port"), "8787");
assert.equal(config.gateway.server.host, "127.0.0.1");
assert.equal(config.gateway.server.port, 9001);
assert.equal(config.gateway.server.publicBaseUrl, "https://public.example.test");
assert.equal(config.gateway.platform.type, "webhook");
assert.equal(config.gateway.platform.secret, testSecret);
assert.deepEqual(config.bot.agent, {
id: "custom-kimi",
command: readyAgent.command,
args: ["-m", "gori-gpt/gpt-5.5", "acp"],
env: { TEST_AGENT_SETTING: "preserved" }
});
assert.deepEqual(config.bot.skills, [{ id: "test-skill", file: path.join(os.tmpdir(), "test-skill.md"), maxBytes: 1234 }]);
assert.deepEqual(config.bot.permissions, { mode: "allowlist", allowedTools: ["Read"], allowedCommandPatterns: ["^true$"] });
assert.deepEqual(config.gateway.policy, { allowedUsers: ["allowed-user"], allowedChats: ["allowed-chat"], requireMentionInGroup: false });
assert.equal(config.runtime.acp.promptTimeoutMs, 123456);
assert.equal(config.runtime.acp.maxProcesses, 3);
assert.equal(prompt.closeCount, 1);
assert.ok(messages.some((message) => message.startsWith("ERROR: server host")));
assert.ok(messages.some((message) => message.startsWith("ERROR: server port")));
assert.ok(messages.some((message) => message.includes("suggested port: 8788")));
assert.ok(messages.every((message) => !message.includes(testSecret)));
});
test("existing setup keeps host and port when defaults are accepted", async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gori-setup-keep-server-"));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const target = path.join(root, "existing-bot", "config.json");
writeConfigFile(target, existingConfig("localhost", 9123));
const prompt = new FakePrompt({}, "");
await runSetup(target, {
botId: "existing-bot",
writeWithoutConfirmation: true,
prompt,
discoverAgents: async () => [readyAgent],
instancesDirectory: root,
log: () => undefined
});
const config = loadConfigFile(target).config;
assert.equal(prompt.defaults.get("Gateway server host"), "localhost");
assert.equal(prompt.defaults.get("Gateway server port"), "9123");
assert.equal(config.gateway.server.host, "localhost");
assert.equal(config.gateway.server.port, 9123);
assert.equal(prompt.closeCount, 1);
});
test("existing setup succeeds without a ready discovered agent and preserves its agent", async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gori-setup-no-ready-"));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const target = path.join(root, "existing-bot", "config.json");
writeConfigFile(target, existingConfig());
await runSetup(target, {
botId: "existing-bot",
writeWithoutConfirmation: true,
prompt: new FakePrompt({}, ""),
discoverAgents: async () => [{ id: "kimi", command: "kimi", args: ["acp"], status: "not-found" }],
instancesDirectory: root,
log: () => undefined
});
assert.deepEqual(loadConfigFile(target).config.bot.agent, existingConfig().bot.agent);
});
test("new setup still fails when no ACP agent is ready", async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gori-setup-new-no-ready-"));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const prompt = new FakePrompt();
await assert.rejects(runSetup(path.join(root, "new-bot", "config.json"), {
botId: "new-bot",
requireNew: true,
writeWithoutConfirmation: true,
prompt,
discoverAgents: async () => [],
instancesDirectory: root,
log: () => undefined
}), /No ACP agent is available/);
assert.equal(prompt.closeCount, 1);
});
test("existing setup rejects attempts to change bot identity", async (t) => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "gori-setup-identity-"));
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
const target = path.join(root, "existing-bot", "config.json");
writeConfigFile(target, existingConfig());
await assert.rejects(runSetup(target, {
botId: "different-bot",
prompt: new FakePrompt(),
discoverAgents: async () => []
}), /Cannot change existing bot\.id/);
});