test(backends): cover external cli runner contract
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user