test(backends): cover external cli runner contract
This commit is contained in:
@@ -3684,6 +3684,17 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/daemon/routing.test.ts + pnpm typecheck passing"
|
"test_status": "pnpm test:run src/daemon/routing.test.ts + pnpm typecheck passing"
|
||||||
|
},
|
||||||
|
"external-backend-runner-test-coverage": {
|
||||||
|
"status": "completed",
|
||||||
|
"date": "2026-02-17",
|
||||||
|
"updated": "2026-02-17",
|
||||||
|
"summary": "Added unit coverage for external CLI backend execution contract (argument inference, `{prompt}` substitution, empty-output failure path, and backend constructor defaults) across codex/claude_code/opencode/gemini adapters.",
|
||||||
|
"files_modified": [
|
||||||
|
"src/backends/external.test.ts",
|
||||||
|
"docs/plans/state.json"
|
||||||
|
],
|
||||||
|
"test_status": "pnpm test:run src/backends/external.test.ts + pnpm typecheck passing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overall_progress": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -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