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.
120 lines
4.1 KiB
TypeScript
120 lines
4.1 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import fc from "fast-check";
|
|
import { mapBackendEventResult } from "../../src/agent-runtime.js";
|
|
import { SessionManager } from "../../src/session-manager.js";
|
|
import type { BackendEventResult } from "../../src/backends/types.js";
|
|
|
|
// Feature: multi-cli-backend, Property 9: EventResult mapping preserves semantics
|
|
// **Validates: Requirements 10.3**
|
|
|
|
/** Arbitrary that produces a BackendEventResult */
|
|
const backendEventResult: fc.Arbitrary<BackendEventResult> = fc.record({
|
|
responseText: fc.option(fc.string({ minLength: 0, maxLength: 500 }), { nil: undefined }),
|
|
sessionId: fc.option(fc.string({ minLength: 1, maxLength: 100 }), { nil: undefined }),
|
|
isError: fc.boolean(),
|
|
});
|
|
|
|
/** Arbitrary for channel IDs */
|
|
const channelId = fc.option(fc.string({ minLength: 1, maxLength: 50 }), { nil: undefined });
|
|
|
|
describe("Property 9: EventResult mapping preserves semantics", () => {
|
|
it("sets error to responseText when isError is true, with no responseText on gateway result", () => {
|
|
fc.assert(
|
|
fc.property(
|
|
backendEventResult.filter((r) => r.isError),
|
|
channelId,
|
|
(result, chId) => {
|
|
const mapped = mapBackendEventResult(result, chId);
|
|
expect(mapped.error).toBe(result.responseText);
|
|
expect(mapped.responseText).toBeUndefined();
|
|
expect(mapped.sessionId).toBeUndefined();
|
|
expect(mapped.targetChannelId).toBe(chId);
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("sets responseText and sessionId when isError is false, with no error on gateway result", () => {
|
|
fc.assert(
|
|
fc.property(
|
|
backendEventResult.filter((r) => !r.isError),
|
|
channelId,
|
|
(result, chId) => {
|
|
const mapped = mapBackendEventResult(result, chId);
|
|
expect(mapped.responseText).toBe(result.responseText);
|
|
expect(mapped.sessionId).toBe(result.sessionId);
|
|
expect(mapped.error).toBeUndefined();
|
|
expect(mapped.targetChannelId).toBe(chId);
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("always sets targetChannelId regardless of isError", () => {
|
|
fc.assert(
|
|
fc.property(backendEventResult, channelId, (result, chId) => {
|
|
const mapped = mapBackendEventResult(result, chId);
|
|
expect(mapped.targetChannelId).toBe(chId);
|
|
}),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
});
|
|
|
|
|
|
// Feature: multi-cli-backend, Property 10: Session ID storage after backend execution
|
|
// **Validates: Requirements 10.4**
|
|
|
|
describe("Property 10: Session ID storage after backend execution", () => {
|
|
it("stores sessionId in SessionManager when BackendEventResult has a sessionId", () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.string({ minLength: 1, maxLength: 50 }),
|
|
fc.string({ minLength: 1, maxLength: 100 }),
|
|
(chId, sessionId) => {
|
|
const sessionManager = new SessionManager();
|
|
const backendResult: BackendEventResult = {
|
|
responseText: "some response",
|
|
sessionId,
|
|
isError: false,
|
|
};
|
|
|
|
// Simulate what AgentRuntime.processMessage does after backend execution
|
|
if (backendResult.sessionId && chId) {
|
|
sessionManager.setSessionId(chId, backendResult.sessionId);
|
|
}
|
|
|
|
expect(sessionManager.getSessionId(chId)).toBe(sessionId);
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
|
|
it("does not update SessionManager when sessionId is undefined", () => {
|
|
fc.assert(
|
|
fc.property(
|
|
fc.string({ minLength: 1, maxLength: 50 }),
|
|
(chId) => {
|
|
const sessionManager = new SessionManager();
|
|
const backendResult: BackendEventResult = {
|
|
responseText: "some response",
|
|
sessionId: undefined,
|
|
isError: false,
|
|
};
|
|
|
|
// Simulate what AgentRuntime.processMessage does after backend execution
|
|
if (backendResult.sessionId && chId) {
|
|
sessionManager.setSessionId(chId, backendResult.sessionId);
|
|
}
|
|
|
|
expect(sessionManager.getSessionId(chId)).toBeUndefined();
|
|
},
|
|
),
|
|
{ numRuns: 100 },
|
|
);
|
|
});
|
|
});
|