Refactor runtime around isolated bot instances
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
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: 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);
|
||||
});
|
||||
Reference in New Issue
Block a user