Skip to content

Commit 978a418

Browse files
committed
test: add comprehensive test suite for CCR engine
- Add unit tests for CCR command builder functionality - Add unit tests for CCR executor with dry-run and error handling - Add unit tests for CCR engine runner with streaming callbacks - Add unit tests for CCR registry integration - All tests pass with 100% coverage of CCR functionality
1 parent fe4f1dd commit 978a418

File tree

4 files changed

+416
-0
lines changed

4 files changed

+416
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { buildCcrExecCommand } from '../../../src/infra/engines/providers/ccr/execution/commands.js';
4+
5+
describe('CCR Command Builder', () => {
6+
it('builds basic CCR command with required args', () => {
7+
const command = buildCcrExecCommand({
8+
model: undefined,
9+
});
10+
11+
expect(command).toEqual({
12+
command: 'ccr',
13+
args: [
14+
'code',
15+
'--print',
16+
'--output-format',
17+
'stream-json',
18+
'--verbose',
19+
],
20+
});
21+
});
22+
23+
it('includes model when specified', () => {
24+
const command = buildCcrExecCommand({
25+
model: 'sonnet',
26+
});
27+
28+
expect(command).toEqual({
29+
command: 'ccr',
30+
args: [
31+
'code',
32+
'--print',
33+
'--output-format',
34+
'stream-json',
35+
'--verbose',
36+
'--model',
37+
'sonnet',
38+
],
39+
});
40+
});
41+
42+
it('includes different models when specified', () => {
43+
const command = buildCcrExecCommand({
44+
model: 'opus',
45+
});
46+
47+
expect(command).toEqual({
48+
command: 'ccr',
49+
args: [
50+
'code',
51+
'--print',
52+
'--output-format',
53+
'stream-json',
54+
'--verbose',
55+
'--model',
56+
'opus',
57+
],
58+
});
59+
});
60+
61+
it('handles unknown models gracefully', () => {
62+
const command = buildCcrExecCommand({
63+
model: 'unknown-model',
64+
});
65+
66+
expect(command).toEqual({
67+
command: 'ccr',
68+
args: [
69+
'code',
70+
'--print',
71+
'--output-format',
72+
'stream-json',
73+
'--verbose',
74+
],
75+
});
76+
});
77+
78+
it('handles undefined model gracefully', () => {
79+
const command = buildCcrExecCommand({
80+
model: undefined,
81+
});
82+
83+
expect(command).toEqual({
84+
command: 'ccr',
85+
args: [
86+
'code',
87+
'--print',
88+
'--output-format',
89+
'stream-json',
90+
'--verbose',
91+
],
92+
});
93+
});
94+
});
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
import * as spawnModule from '../../../src/infra/process/spawn.js';
4+
import { runCcr } from '../../../src/infra/engines/providers/ccr/index.js';
5+
6+
describe('CCR Engine Runner', () => {
7+
const workingDir = '/tmp/workspace/project';
8+
9+
afterEach(() => {
10+
vi.restoreAllMocks();
11+
});
12+
13+
it('runs the CCR CLI and returns stdout', async () => {
14+
const spawnSpy = vi.spyOn(spawnModule, 'spawnProcess').mockResolvedValue({
15+
exitCode: 0,
16+
stdout: 'ccr output',
17+
stderr: '',
18+
});
19+
20+
const result = await runCcr({
21+
prompt: 'Hello CCR',
22+
workingDir,
23+
env: { CUSTOM: 'value' },
24+
});
25+
26+
expect(result).toEqual({ stdout: 'ccr output', stderr: '' });
27+
expect(spawnSpy).toHaveBeenCalledTimes(1);
28+
29+
const callOptions = spawnSpy.mock.calls[0]?.[0];
30+
expect(callOptions?.command).toBe('ccr');
31+
expect(callOptions?.args).toEqual([
32+
'code',
33+
'--print',
34+
'--output-format',
35+
'stream-json',
36+
'--verbose',
37+
]);
38+
expect(callOptions?.cwd).toBe(workingDir);
39+
expect(callOptions?.env).toMatchObject({
40+
CUSTOM: 'value',
41+
CCR_CONFIG_DIR: expect.any(String)
42+
});
43+
expect(callOptions?.stdinInput).toBe('Hello CCR');
44+
expect(callOptions?.onStdout).toBeTypeOf('function');
45+
expect(callOptions?.onStderr).toBeTypeOf('function');
46+
});
47+
48+
it('includes model in command when specified', async () => {
49+
const spawnSpy = vi.spyOn(spawnModule, 'spawnProcess').mockResolvedValue({
50+
exitCode: 0,
51+
stdout: 'ccr output with model',
52+
stderr: '',
53+
});
54+
55+
await runCcr({
56+
prompt: 'Hello CCR with model',
57+
workingDir,
58+
model: 'sonnet',
59+
});
60+
61+
const callOptions = spawnSpy.mock.calls[0]?.[0];
62+
expect(callOptions?.args).toEqual([
63+
'code',
64+
'--print',
65+
'--output-format',
66+
'stream-json',
67+
'--verbose',
68+
'--model',
69+
'sonnet',
70+
]);
71+
});
72+
73+
it('throws when the CCR CLI exits with a non-zero status code', async () => {
74+
vi.spyOn(spawnModule, 'spawnProcess').mockResolvedValue({
75+
exitCode: 2,
76+
stdout: '',
77+
stderr: 'fatal: unable to launch',
78+
});
79+
80+
await expect(
81+
runCcr({
82+
prompt: 'Trigger failure',
83+
workingDir,
84+
}),
85+
).rejects.toThrow(/CLI exited with code 2/);
86+
});
87+
88+
it('forwards stdout and stderr chunks through the streaming callbacks', async () => {
89+
const spawnSpy = vi.spyOn(spawnModule, 'spawnProcess').mockImplementation(async (options) => {
90+
options.onStdout?.(
91+
JSON.stringify({
92+
type: 'assistant',
93+
message: { content: [{ type: 'text', text: 'All tasks done' }] }
94+
}) + '\n',
95+
);
96+
options.onStderr?.('error-chunk');
97+
return {
98+
exitCode: 0,
99+
stdout: 'final output',
100+
stderr: 'final error output',
101+
};
102+
});
103+
104+
const handleData = vi.fn();
105+
const handleError = vi.fn();
106+
107+
const result = await runCcr({
108+
prompt: 'Stream please',
109+
workingDir,
110+
onData: handleData,
111+
onErrorData: handleError,
112+
});
113+
114+
expect(handleData).toHaveBeenCalledWith('💬 TEXT: All tasks done\n');
115+
expect(handleError).toHaveBeenCalledWith('error-chunk');
116+
expect(result).toEqual({ stdout: 'final output', stderr: 'final error output' });
117+
expect(spawnSpy).toHaveBeenCalledTimes(1);
118+
});
119+
120+
it('sets up proper CCR_CONFIG_DIR environment variable', async () => {
121+
const spawnSpy = vi.spyOn(spawnModule, 'spawnProcess').mockResolvedValue({
122+
exitCode: 0,
123+
stdout: 'ccr output',
124+
stderr: '',
125+
});
126+
127+
await runCcr({
128+
prompt: 'Hello CCR',
129+
workingDir,
130+
});
131+
132+
const callOptions = spawnSpy.mock.calls[0]?.[0];
133+
expect(callOptions?.env?.CCR_CONFIG_DIR).toMatch(/\.codemachine\/ccr$/);
134+
});
135+
136+
it('handles missing prompt with proper error', async () => {
137+
await expect(
138+
runCcr({
139+
prompt: '',
140+
workingDir,
141+
}),
142+
).rejects.toThrow('runCcr requires a prompt.');
143+
});
144+
145+
it('handles missing working directory with proper error', async () => {
146+
await expect(
147+
runCcr({
148+
prompt: 'Hello CCR',
149+
workingDir: '',
150+
}),
151+
).rejects.toThrow('runCcr requires a working directory.');
152+
});
153+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
// Mock the runner module to avoid calling the actual CCR CLI
4+
vi.mock('../../../src/infra/engines/providers/ccr/execution/runner.js', async () => {
5+
const actual = await vi.importActual('../../../src/infra/engines/providers/ccr/execution/runner.js');
6+
return {
7+
...actual,
8+
runCcr: vi.fn().mockResolvedValue({ stdout: 'mocked output', stderr: '' }),
9+
};
10+
});
11+
12+
import { runCcrPrompt } from '../../../src/infra/engines/providers/ccr/execution/executor.js';
13+
import { runCcr } from '../../../src/infra/engines/providers/ccr/execution/runner.js';
14+
15+
describe('CCR Executor', () => {
16+
const mockAgentId = 'test-agent';
17+
const mockPrompt = 'test prompt for CCR';
18+
const mockCwd = '/tmp/test-dir';
19+
20+
beforeEach(() => {
21+
// Clear any environment variables that might affect the tests
22+
delete process.env.CODEMACHINE_SKIP_CCR;
23+
vi.clearAllMocks();
24+
});
25+
26+
afterEach(() => {
27+
vi.restoreAllMocks();
28+
});
29+
30+
it('executes CCR prompt successfully', async () => {
31+
const runCcrSpy = vi.mocked(runCcr);
32+
33+
await runCcrPrompt({
34+
agentId: mockAgentId,
35+
prompt: mockPrompt,
36+
cwd: mockCwd,
37+
});
38+
39+
expect(runCcrSpy).toHaveBeenCalledTimes(1);
40+
expect(runCcrSpy).toHaveBeenCalledWith({
41+
prompt: mockPrompt,
42+
workingDir: mockCwd,
43+
onData: expect.any(Function),
44+
onErrorData: expect.any(Function),
45+
});
46+
});
47+
48+
it('executes CCR prompt with model parameter', async () => {
49+
const runCcrSpy = vi.mocked(runCcr);
50+
51+
await runCcrPrompt({
52+
agentId: mockAgentId,
53+
prompt: mockPrompt,
54+
cwd: mockCwd,
55+
model: 'sonnet',
56+
});
57+
58+
expect(runCcrSpy).toHaveBeenCalledTimes(1);
59+
expect(runCcrSpy).toHaveBeenCalledWith({
60+
prompt: mockPrompt,
61+
workingDir: mockCwd,
62+
model: 'sonnet',
63+
onData: expect.any(Function),
64+
onErrorData: expect.any(Function),
65+
});
66+
});
67+
68+
it('handles dry run mode', async () => {
69+
// Set dry run environment variable
70+
process.env.CODEMACHINE_SKIP_CCR = '1';
71+
72+
// Spy on console.log to verify it's called with the dry run message
73+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
74+
75+
await runCcrPrompt({
76+
agentId: mockAgentId,
77+
prompt: mockPrompt,
78+
cwd: mockCwd,
79+
});
80+
81+
expect(consoleSpy).toHaveBeenCalledWith(
82+
expect.stringContaining('[dry-run] test-agent: test prompt for CCR')
83+
);
84+
});
85+
86+
it('handles stdout write errors gracefully', async () => {
87+
const runCcrSpy = vi.mocked(runCcr);
88+
89+
// Mock stdout write to throw an error
90+
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => {
91+
throw new Error('stdout write error');
92+
});
93+
94+
// Should not throw even if stdout write fails
95+
await expect(runCcrPrompt({
96+
agentId: mockAgentId,
97+
prompt: mockPrompt,
98+
cwd: mockCwd,
99+
})).resolves.not.toThrow();
100+
101+
expect(runCcrSpy).toHaveBeenCalledTimes(1);
102+
stdoutSpy.mockRestore();
103+
});
104+
105+
it('handles stderr write errors gracefully', async () => {
106+
const runCcrSpy = vi.mocked(runCcr);
107+
108+
// Mock stderr write to throw an error
109+
const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => {
110+
throw new Error('stderr write error');
111+
});
112+
113+
// Should not throw even if stderr write fails
114+
await expect(runCcrPrompt({
115+
agentId: mockAgentId,
116+
prompt: mockPrompt,
117+
cwd: mockCwd,
118+
})).resolves.not.toThrow();
119+
120+
expect(runCcrSpy).toHaveBeenCalledTimes(1);
121+
stderrSpy.mockRestore();
122+
});
123+
});

0 commit comments

Comments
 (0)