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:
84
tests/property/registry.property.test.ts
Normal file
84
tests/property/registry.property.test.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import fc from "fast-check";
|
||||
import { resolveBackendName, createBackend } from "../../src/backends/registry.js";
|
||||
import { ClaudeCodeBackend } from "../../src/backends/claude-backend.js";
|
||||
import { CodexBackend } from "../../src/backends/codex-backend.js";
|
||||
import { GeminiBackend } from "../../src/backends/gemini-backend.js";
|
||||
import { OpenCodeBackend } from "../../src/backends/opencode-backend.js";
|
||||
import type { BackendAdapterConfig, BackendName } from "../../src/backends/types.js";
|
||||
|
||||
// Feature: multi-cli-backend, Property 7: Backend name resolution
|
||||
// **Validates: Requirements 6.1, 6.2, 6.3, 6.5**
|
||||
|
||||
const VALID_NAMES: BackendName[] = ["claude", "codex", "gemini", "opencode"];
|
||||
|
||||
/** Arbitrary that produces one of the four valid backend names */
|
||||
const validBackendName = fc.constantFrom(...VALID_NAMES);
|
||||
|
||||
/** Arbitrary that produces strings which are NOT valid backend names and NOT undefined */
|
||||
const invalidBackendName = fc
|
||||
.string({ minLength: 1, maxLength: 100 })
|
||||
.filter((s) => !VALID_NAMES.includes(s as BackendName));
|
||||
|
||||
describe("Property 7: Backend name resolution", () => {
|
||||
it("returns the corresponding BackendName for any valid backend name string", () => {
|
||||
fc.assert(
|
||||
fc.property(validBackendName, (name) => {
|
||||
const result = resolveBackendName(name);
|
||||
return result === name;
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("returns 'claude' when input is undefined", () => {
|
||||
expect(resolveBackendName(undefined)).toBe("claude");
|
||||
});
|
||||
|
||||
it("throws a descriptive error for any invalid string value", () => {
|
||||
fc.assert(
|
||||
fc.property(invalidBackendName, (name) => {
|
||||
try {
|
||||
resolveBackendName(name);
|
||||
return false; // Should have thrown
|
||||
} catch (err) {
|
||||
const message = (err as Error).message;
|
||||
// Error must mention the invalid value and list valid options
|
||||
return (
|
||||
message.includes(name) &&
|
||||
VALID_NAMES.every((valid) => message.includes(valid))
|
||||
);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
|
||||
it("createBackend returns the correct implementation for each valid name", () => {
|
||||
const config: BackendAdapterConfig = {
|
||||
cliPath: "/usr/bin/test",
|
||||
workingDir: "/tmp",
|
||||
queryTimeoutMs: 30000,
|
||||
allowedTools: [],
|
||||
maxTurns: 25,
|
||||
};
|
||||
|
||||
const expectedTypes: Record<BackendName, new (cfg: BackendAdapterConfig) => unknown> = {
|
||||
claude: ClaudeCodeBackend,
|
||||
codex: CodexBackend,
|
||||
gemini: GeminiBackend,
|
||||
opencode: OpenCodeBackend,
|
||||
};
|
||||
|
||||
fc.assert(
|
||||
fc.property(validBackendName, (name) => {
|
||||
const backend = createBackend(name, config);
|
||||
return (
|
||||
backend instanceof expectedTypes[name] &&
|
||||
backend.name() === name
|
||||
);
|
||||
}),
|
||||
{ numRuns: 100 },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user