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.
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
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 },
|
|
);
|
|
});
|
|
});
|