test(backends): cover external cli runner contract

This commit is contained in:
William Valentin
2026-02-17 10:40:46 -08:00
parent 70bb14a6f6
commit bfb073ca5f
2 changed files with 118 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { execFile } from 'child_process';
import {
ExternalCliBackend,
ClaudeCodeBackend,
OpenCodeBackend,
CodexBackend,
GeminiBackend,
} from './external.js';
vi.mock('child_process', () => ({
execFile: vi.fn(),
}));
const mockExecFile = vi.mocked(execFile);
describe('ExternalCliBackend', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('uses inferred -p args for codex backend', async () => {
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
if (typeof callback === 'function') {
callback(null, 'ok', '');
}
return {} as never;
});
const backend = new CodexBackend('/usr/bin/codex', ['run']);
const result = await backend.process({ prompt: 'hello', history: [] });
expect(result).toBe('ok');
expect(mockExecFile).toHaveBeenCalledWith(
'/usr/bin/codex',
['run', '-p', 'USER: hello'],
expect.any(Object),
expect.any(Function),
);
});
it('uses inferred --print args for claude_code backend', async () => {
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
if (typeof callback === 'function') {
callback(null, 'ok', '');
}
return {} as never;
});
const backend = new ClaudeCodeBackend('/usr/bin/claude', []);
await backend.process({ prompt: 'hello', history: [] });
expect(mockExecFile).toHaveBeenCalledWith(
'/usr/bin/claude',
['--print', 'USER: hello'],
expect.any(Object),
expect.any(Function),
);
});
it('supports {prompt} substitution in configured args', async () => {
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
if (typeof callback === 'function') {
callback(null, 'ok', '');
}
return {} as never;
});
const backend = new ExternalCliBackend({
name: 'opencode',
command: 'opencode',
args: ['--message', '{prompt}'],
});
await backend.process({
prompt: 'new question',
history: [{ role: 'assistant', content: 'previous reply' }],
});
expect(mockExecFile).toHaveBeenCalledWith(
'opencode',
['--message', 'ASSISTANT: previous reply\n\nUSER: new question'],
expect.any(Object),
expect.any(Function),
);
});
it('throws when backend returns no output', async () => {
mockExecFile.mockImplementation((_cmd, _args, _opts, callback) => {
if (typeof callback === 'function') {
callback(null, ' ', '');
}
return {} as never;
});
const backend = new GeminiBackend('/usr/bin/gemini', []);
await expect(backend.process({ prompt: 'hello', history: [] }))
.rejects.toThrow('returned no output');
});
it('constructs default commands for opencode and gemini backends', () => {
const opencode = new OpenCodeBackend();
const gemini = new GeminiBackend();
expect(opencode.name).toBe('opencode');
expect(gemini.name).toBe('gemini');
});
});