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(); closeCount = 0; constructor( private readonly answers: Record = {}, private readonly secretAnswer = testSecret ) {} async ask(question: string, defaultValue?: string): Promise { this.defaults.set(question, defaultValue); const queued = this.answers[question]; return queued && queued.length > 0 ? queued.shift()! : defaultValue || ""; } async askSecret(): Promise { return this.secretAnswer; } async askBoolean(_question: string, defaultValue = false): Promise { return defaultValue; } async askList(_question: string, defaultValues: string[] = []): Promise { return defaultValues; } async choose(_question: string, choices: Choice[], defaultIndex = 0): Promise { 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: readyAgent.id, command: readyAgent.command, args: readyAgent.args, env: {} }, skills: [], permissions: { mode: "deny", allowedTools: [], allowedCommandPatterns: [] } }, gateway: { server: { host, port, publicBaseUrl: "https://public.example.test" }, policy: { allowedUsers: [], allowedChats: [], requireMentionInGroup: true }, platform: { type: "webhook", secret: testSecret } }, runtime: {} }); } 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.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); });