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

@@ -0,0 +1,95 @@
import { describe, it } from "vitest";
import fc from "fast-check";
import { CodexBackend } from "../../src/backends/codex-backend.js";
import type { BackendAdapterConfig } from "../../src/backends/types.js";
// Feature: multi-cli-backend, Property 2: Codex backend required flags
// **Validates: Requirements 3.2, 3.3, 3.4, 3.5**
/**
* Arbitrary for non-empty strings that won't break CLI arg parsing.
*/
const nonEmptyString = fc.string({ minLength: 1, maxLength: 200 });
/**
* Arbitrary for working directory paths (non-empty, path-like).
*/
const workingDir = fc.stringMatching(/^\/[A-Za-z0-9_/.-]{1,100}$/);
function createBackend(workDir: string): CodexBackend {
const config: BackendAdapterConfig = {
cliPath: "codex",
workingDir: workDir,
queryTimeoutMs: 60000,
allowedTools: [],
maxTurns: 25,
};
return new CodexBackend(config);
}
describe("Property 2: Codex backend required flags", () => {
it("generated args always contain the exec subcommand", () => {
fc.assert(
fc.property(
nonEmptyString,
workingDir,
(prompt, workDir) => {
const backend = createBackend(workDir);
const args = backend.buildArgs(prompt);
return args[0] === "exec";
},
),
{ numRuns: 100 },
);
});
it("generated args always contain --json", () => {
fc.assert(
fc.property(
nonEmptyString,
workingDir,
(prompt, workDir) => {
const backend = createBackend(workDir);
const args = backend.buildArgs(prompt);
return args.includes("--json");
},
),
{ numRuns: 100 },
);
});
it("generated args always contain --dangerously-bypass-approvals-and-sandbox", () => {
fc.assert(
fc.property(
nonEmptyString,
workingDir,
(prompt, workDir) => {
const backend = createBackend(workDir);
const args = backend.buildArgs(prompt);
return args.includes("--dangerously-bypass-approvals-and-sandbox");
},
),
{ numRuns: 100 },
);
});
it("generated args always contain --cd with the configured working directory", () => {
fc.assert(
fc.property(
nonEmptyString,
workingDir,
(prompt, workDir) => {
const backend = createBackend(workDir);
const args = backend.buildArgs(prompt);
const cdIndex = args.indexOf("--cd");
return cdIndex !== -1 && args[cdIndex + 1] === workDir;
},
),
{ numRuns: 100 },
);
});
});