feat: add pluggable multi-CLI backend system

Implement BackendAdapter interface with four CLI backends:
- ClaudeCodeBackend (extracted from AgentRuntime)
- CodexBackend (OpenAI Codex CLI)
- GeminiBackend (Google Gemini CLI)
- OpenCodeBackend (OpenCode CLI)

Add BackendRegistry for resolution/creation via AGENT_BACKEND env var.
Refactor AgentRuntime to delegate to BackendAdapter instead of
hardcoding Claude CLI. Update GatewayConfig with new env vars
(AGENT_BACKEND, BACKEND_CLI_PATH, BACKEND_MODEL, BACKEND_MAX_TURNS).

Includes 10 property-based test files and unit tests for edge cases.
This commit is contained in:
2026-02-22 23:41:30 -05:00
parent f2247ea3ac
commit 453389f55c
25 changed files with 3262 additions and 195 deletions

View File

@@ -29,6 +29,10 @@ describe("loadConfig", () => {
expect(config.configDir).toBe("./config");
expect(config.maxQueueDepth).toBe(100);
expect(config.outputChannelId).toBeUndefined();
expect(config.agentBackend).toBe("claude");
expect(config.backendCliPath).toBe("claude");
expect(config.backendModel).toBeUndefined();
expect(config.backendMaxTurns).toBe(25);
});
it("should parse ALLOWED_TOOLS from comma-separated string", () => {
@@ -62,6 +66,45 @@ describe("loadConfig", () => {
expect(config.claudeCliPath).toBe("/usr/local/bin/claude");
});
it("should read new backend environment variables", () => {
process.env.AGENT_BACKEND = "codex";
process.env.BACKEND_CLI_PATH = "/usr/local/bin/codex";
process.env.BACKEND_MODEL = "gpt-4";
process.env.BACKEND_MAX_TURNS = "10";
const config = loadConfig();
expect(config.agentBackend).toBe("codex");
expect(config.backendCliPath).toBe("/usr/local/bin/codex");
expect(config.backendModel).toBe("gpt-4");
expect(config.backendMaxTurns).toBe(10);
});
it("should default backendCliPath to backend name when no CLI path env vars are set", () => {
process.env.AGENT_BACKEND = "gemini";
const config = loadConfig();
expect(config.backendCliPath).toBe("gemini");
});
it("should use CLAUDE_CLI_PATH as backendCliPath when backend is claude and BACKEND_CLI_PATH is not set", () => {
process.env.CLAUDE_CLI_PATH = "/custom/claude";
const config = loadConfig();
expect(config.agentBackend).toBe("claude");
expect(config.backendCliPath).toBe("/custom/claude");
expect(config.claudeCliPath).toBe("/custom/claude");
});
it("should prefer BACKEND_CLI_PATH over CLAUDE_CLI_PATH", () => {
process.env.CLAUDE_CLI_PATH = "/old/claude";
process.env.BACKEND_CLI_PATH = "/new/backend";
const config = loadConfig();
expect(config.backendCliPath).toBe("/new/backend");
});
it("should throw for invalid AGENT_BACKEND value", () => {
process.env.AGENT_BACKEND = "invalid-backend";
expect(() => loadConfig()).toThrow('Invalid backend name "invalid-backend"');
});
it("should throw when DISCORD_BOT_TOKEN is missing", () => {
delete process.env.DISCORD_BOT_TOKEN;
expect(() => loadConfig()).toThrow("DISCORD_BOT_TOKEN");