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.
114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { resolveBackendName, createBackend } from "../../src/backends/registry.js";
|
|
import type { BackendAdapter, BackendAdapterConfig } from "../../src/backends/types.js";
|
|
|
|
const defaultAdapterConfig: BackendAdapterConfig = {
|
|
cliPath: "/usr/bin/claude",
|
|
workingDir: "/tmp",
|
|
queryTimeoutMs: 30000,
|
|
allowedTools: [],
|
|
maxTurns: 25,
|
|
};
|
|
|
|
describe("Startup validation flow", () => {
|
|
describe("valid backend creation and validation", () => {
|
|
it("should create a claude backend and validate successfully when binary is accessible", async () => {
|
|
const backend = createBackend("claude", defaultAdapterConfig);
|
|
expect(backend.name()).toBe("claude");
|
|
// validate() checks fs access — we test the integration via the registry
|
|
expect(typeof backend.validate).toBe("function");
|
|
});
|
|
|
|
it("should create each valid backend type", () => {
|
|
const names = ["claude", "codex", "gemini", "opencode"] as const;
|
|
for (const name of names) {
|
|
const backend = createBackend(name, defaultAdapterConfig);
|
|
expect(backend.name()).toBe(name);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("invalid backend name", () => {
|
|
it("should throw a descriptive error for an invalid backend name", () => {
|
|
expect(() => resolveBackendName("invalid-backend")).toThrow(
|
|
'Invalid backend name "invalid-backend". Valid options are: claude, codex, gemini, opencode',
|
|
);
|
|
});
|
|
|
|
it("should throw for empty string backend name", () => {
|
|
expect(() => resolveBackendName("")).toThrow(
|
|
'Invalid backend name "". Valid options are: claude, codex, gemini, opencode',
|
|
);
|
|
});
|
|
|
|
it("should default to claude when backend name is undefined", () => {
|
|
expect(resolveBackendName(undefined)).toBe("claude");
|
|
});
|
|
});
|
|
|
|
describe("missing CLI binary (validate returns false)", () => {
|
|
it("should return false from validate() when CLI path does not exist", async () => {
|
|
const backend = createBackend("claude", {
|
|
...defaultAdapterConfig,
|
|
cliPath: "/nonexistent/path/to/cli",
|
|
});
|
|
const isValid = await backend.validate();
|
|
expect(isValid).toBe(false);
|
|
});
|
|
|
|
it("should return false from validate() for codex backend with missing binary", async () => {
|
|
const backend = createBackend("codex", {
|
|
...defaultAdapterConfig,
|
|
cliPath: "/nonexistent/codex-binary",
|
|
});
|
|
const isValid = await backend.validate();
|
|
expect(isValid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("startup wiring simulation", () => {
|
|
let exitSpy: ReturnType<typeof vi.spyOn>;
|
|
|
|
beforeEach(() => {
|
|
exitSpy = vi.spyOn(process, "exit").mockImplementation((() => {}) as any);
|
|
});
|
|
|
|
afterEach(() => {
|
|
exitSpy.mockRestore();
|
|
});
|
|
|
|
it("should exit with code 1 when backend validation fails", async () => {
|
|
const backendName = resolveBackendName("claude");
|
|
const backend = createBackend(backendName, {
|
|
...defaultAdapterConfig,
|
|
cliPath: "/nonexistent/binary",
|
|
});
|
|
|
|
const isValid = await backend.validate();
|
|
if (!isValid) {
|
|
process.exit(1);
|
|
}
|
|
|
|
expect(isValid).toBe(false);
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
it("should not exit when backend validation succeeds", async () => {
|
|
// Create a mock backend that validates successfully
|
|
const mockBackend: BackendAdapter = {
|
|
name: () => "claude",
|
|
execute: vi.fn(),
|
|
validate: vi.fn().mockResolvedValue(true),
|
|
};
|
|
|
|
const isValid = await mockBackend.validate();
|
|
if (!isValid) {
|
|
process.exit(1);
|
|
}
|
|
|
|
expect(isValid).toBe(true);
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|