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:
119
tests/property/agent-runtime.property.test.ts
Normal file
119
tests/property/agent-runtime.property.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user