Files
aetheel-2/tests/unit/startup-validation.test.ts
tanmay11k 453389f55c 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.
2026-02-22 23:41:30 -05:00

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();
});
});
});