Skip to content

Commit dc4149d

Browse files
authored
fix(quality-gates): fail coderabbit on non-zero exit
Fixes #763 and #764. Validated with full CI, CodeRabbit, manifest, Jest matrix, typecheck, lint, and local targeted/full tests.
1 parent f31254d commit dc4149d

5 files changed

Lines changed: 174 additions & 7 deletions

File tree

.aiox-core/core/orchestration/workflow-executor.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ class WorkflowExecutor {
804804
'`wsl --install` (https://learn.microsoft.com/windows/wsl/install), ' +
805805
'then install the CodeRabbit CLI inside the WSL distribution. ' +
806806
'See docs/guides/installation-troubleshooting.md Issue 10. ' +
807-
`To bypass this check, set coderabbit.installation_mode='native' in your config.`
807+
'To bypass this check, set coderabbit.installation_mode=\'native\' in your config.',
808808
);
809809
}
810810
const wslPath = this.projectRoot
@@ -829,7 +829,7 @@ class WorkflowExecutor {
829829
});
830830

831831
// Parse CodeRabbit output to extract issues
832-
const issues = this.parseCodeRabbitOutput(stdout);
832+
const issues = this.parseCodeRabbitOutput([stdout || '', stderr || ''].join('\n'));
833833

834834
return {
835835
success: true,
@@ -845,6 +845,16 @@ class WorkflowExecutor {
845845
issues: [],
846846
};
847847
}
848+
if (error.code !== undefined && error.code !== 0) {
849+
return {
850+
success: false,
851+
error:
852+
`CodeRabbit CLI exited with code ${error.code}. ` +
853+
`stdout: ${(error.stdout || '').slice(0, 200)}, ` +
854+
`stderr: ${(error.stderr || '').slice(0, 200)}`,
855+
issues: [],
856+
};
857+
}
848858
return {
849859
success: false,
850860
error: error.message,

.aiox-core/core/quality-gates/layer2-pr-automation.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ class Layer2PRAutomation extends BaseLayer {
147147
}
148148

149149
const result = await this.runCommand(command, timeout);
150+
if (result.exitCode !== undefined && result.exitCode !== 0) {
151+
throw new Error(
152+
`CodeRabbit CLI exited with code ${result.exitCode}. ` +
153+
`stdout: ${(result.stdout || '').slice(0, 200)}, ` +
154+
`stderr: ${(result.stderr || '').slice(0, 200)}`,
155+
);
156+
}
150157

151158
// Parse CodeRabbit output for issues
152159
const issues = this.parseCodeRabbitOutput(result.stdout + result.stderr);

.aiox-core/install-manifest.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# - File types for categorization
99
#
1010
version: 5.2.7
11-
generated_at: "2026-05-18T11:40:45.332Z"
11+
generated_at: "2026-05-18T15:42:34.055Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1128
1414
files:
@@ -989,9 +989,9 @@ files:
989989
type: core
990990
size: 31469
991991
- path: core/orchestration/workflow-executor.js
992-
hash: sha256:5bc1c72b7565f3ae5794d4d379d2090c605ca6c383ebbb94a042c5f96b2b9102
992+
hash: sha256:8c56facc975f0452d686183d25ab1c19211afd127814b2ade963b4950352872f
993993
type: core
994-
size: 38289
994+
size: 38673
995995
- path: core/orchestration/workflow-orchestrator.js
996996
hash: sha256:82816bd5e6fecc9bbb77292444e53254c72bf93e5c04260784743354c6a58627
997997
type: core
@@ -1037,9 +1037,9 @@ files:
10371037
type: core
10381038
size: 9393
10391039
- path: core/quality-gates/layer2-pr-automation.js
1040-
hash: sha256:19e52369efe5cf0df9d0b6a9675dd555fdd46c1ab19cf6dc45d9173ad6e6524e
1040+
hash: sha256:40a7b03d6294c79741e9313ae91a8d6a30797dca1df4e3ca406edfe2911b4322
10411041
type: core
1042-
size: 11907
1042+
size: 12213
10431043
- path: core/quality-gates/layer3-human-review.js
10441044
hash: sha256:9bf79d5adfddae55d7dddfda777cd2775aa76f82204ddd0f660f6edbd093b16b
10451045
type: core
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* WorkflowExecutor CodeRabbit invocation tests
3+
*
4+
* Regression guards for #763/#764:
5+
* - non-zero CodeRabbit subprocess exits must not pass silently
6+
* - workflow-executor WSL probing mirrors Layer 2 behavior
7+
*/
8+
9+
'use strict';
10+
11+
const childProcess = require('child_process');
12+
const os = require('os');
13+
const path = require('path');
14+
15+
const { WorkflowExecutor } = require('../../../.aiox-core/core/orchestration/workflow-executor');
16+
17+
const expectedDefaultCliPath = path.join(os.homedir(), '/.local/bin/coderabbit');
18+
19+
describe('WorkflowExecutor CodeRabbit analysis', () => {
20+
let originalPlatform;
21+
let spawnSyncSpy;
22+
let execSpy;
23+
24+
beforeEach(() => {
25+
originalPlatform = process.platform;
26+
spawnSyncSpy = jest.spyOn(childProcess, 'spawnSync').mockImplementation((cmd) => {
27+
if (cmd === 'wsl') {
28+
return { status: 0, stdout: 'Ubuntu\n', stderr: '' };
29+
}
30+
return { status: 0, stdout: '', stderr: '' };
31+
});
32+
execSpy = jest.spyOn(childProcess, 'exec').mockImplementation((command, options, callback) => {
33+
callback(null, '', '');
34+
return { pid: 1234 };
35+
});
36+
});
37+
38+
afterEach(() => {
39+
Object.defineProperty(process, 'platform', { value: originalPlatform });
40+
spawnSyncSpy.mockRestore();
41+
execSpy.mockRestore();
42+
});
43+
44+
const setPlatform = (value) => {
45+
Object.defineProperty(process, 'platform', { value });
46+
};
47+
48+
const runAnalysis = (config = {}, projectRoot = process.cwd()) => {
49+
const executor = new WorkflowExecutor(projectRoot, { saveState: false, debug: false });
50+
return executor.runCodeRabbitAnalysis({
51+
self_healing: { timeout_minutes: 1 },
52+
...config,
53+
});
54+
};
55+
56+
it('returns an informative failure when CodeRabbit exits non-zero', async () => {
57+
setPlatform('darwin');
58+
execSpy.mockImplementation((command, options, callback) => {
59+
const error = new Error('Command failed');
60+
error.code = 137;
61+
error.stdout = '';
62+
error.stderr = '';
63+
callback(error, '', '');
64+
return { pid: 1234 };
65+
});
66+
67+
const result = await runAnalysis({ installation_mode: 'native' });
68+
69+
expect(result.success).toBe(false);
70+
expect(result.error).toMatch(/CodeRabbit CLI exited with code 137/);
71+
expect(result.error).toContain('stdout:');
72+
expect(result.error).toContain('stderr:');
73+
});
74+
75+
it('surfaces a clear error when WSL binary is missing (ENOENT)', async () => {
76+
setPlatform('darwin');
77+
spawnSyncSpy.mockImplementation((cmd) => {
78+
if (cmd === 'wsl') {
79+
return { error: Object.assign(new Error('ENOENT'), { code: 'ENOENT' }), status: null };
80+
}
81+
return { status: 0, stdout: '', stderr: '' };
82+
});
83+
84+
const result = await runAnalysis({ installation_mode: 'wsl' });
85+
86+
expect(spawnSyncSpy).toHaveBeenCalledWith('wsl', ['-l'], expect.any(Object));
87+
expect(result.success).toBe(false);
88+
expect(result.error).toMatch(/CodeRabbit CLI requires WSL/);
89+
expect(result.error).toMatch(/wsl --install/);
90+
expect(result.error).toMatch(/installation-troubleshooting/);
91+
expect(execSpy).not.toHaveBeenCalled();
92+
});
93+
94+
it('surfaces a clear error when WSL has no usable distribution', async () => {
95+
setPlatform('darwin');
96+
spawnSyncSpy.mockImplementation((cmd) => {
97+
if (cmd === 'wsl') {
98+
return { status: 1, stdout: '', stderr: 'no distros\n' };
99+
}
100+
return { status: 0, stdout: '', stderr: '' };
101+
});
102+
103+
const result = await runAnalysis({ installation_mode: 'wsl' });
104+
105+
expect(spawnSyncSpy).toHaveBeenCalledWith('wsl', ['-l'], expect.any(Object));
106+
expect(result.success).toBe(false);
107+
expect(result.error).toMatch(/CodeRabbit CLI requires WSL/);
108+
expect(execSpy).not.toHaveBeenCalled();
109+
});
110+
111+
it('does not probe WSL when installation_mode=native on Windows', async () => {
112+
setPlatform('win32');
113+
114+
await runAnalysis({ installation_mode: 'native' });
115+
116+
expect(execSpy).toHaveBeenCalled();
117+
const command = execSpy.mock.calls[0][0];
118+
expect(command).toBe(`${expectedDefaultCliPath} --prompt-only -t uncommitted`);
119+
const wslProbeCalls = spawnSyncSpy.mock.calls.filter((c) => c[0] === 'wsl');
120+
expect(wslProbeCalls).toHaveLength(0);
121+
});
122+
123+
it('does not probe WSL for default native mode on macOS', async () => {
124+
setPlatform('darwin');
125+
126+
await runAnalysis();
127+
128+
expect(execSpy).toHaveBeenCalled();
129+
const command = execSpy.mock.calls[0][0];
130+
expect(command).toBe(`${expectedDefaultCliPath} --prompt-only -t uncommitted`);
131+
const wslProbeCalls = spawnSyncSpy.mock.calls.filter((c) => c[0] === 'wsl');
132+
expect(wslProbeCalls).toHaveLength(0);
133+
});
134+
});

tests/unit/quality-gates/layer2-pr-automation.test.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ describe('Layer2PRAutomation', () => {
127127
expect(result.skipped).toBe(true);
128128
expect(result.message).toContain('not installed');
129129
});
130+
131+
it('should fail when CodeRabbit exits non-zero even without output', async () => {
132+
layer.runCommand = jest.fn().mockResolvedValue({
133+
exitCode: 137,
134+
stdout: '',
135+
stderr: '',
136+
duration: 100,
137+
});
138+
139+
const result = await layer.runCodeRabbit();
140+
141+
expect(result.pass).toBe(false);
142+
expect(result.error).toMatch(/CodeRabbit CLI exited with code 137/);
143+
expect(result.error).toContain('stdout:');
144+
expect(result.error).toContain('stderr:');
145+
});
130146
});
131147

132148
describe('runQuinnReview', () => {

0 commit comments

Comments
 (0)