Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .aiox-core/core/orchestration/workflow-executor.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ class WorkflowExecutor {
'`wsl --install` (https://learn.microsoft.com/windows/wsl/install), ' +
'then install the CodeRabbit CLI inside the WSL distribution. ' +
'See docs/guides/installation-troubleshooting.md Issue 10. ' +
`To bypass this check, set coderabbit.installation_mode='native' in your config.`
'To bypass this check, set coderabbit.installation_mode=\'native\' in your config.',
);
}
const wslPath = this.projectRoot
Expand All @@ -829,7 +829,7 @@ class WorkflowExecutor {
});

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

return {
success: true,
Expand All @@ -845,6 +845,16 @@ class WorkflowExecutor {
issues: [],
};
}
if (error.code !== undefined && error.code !== 0) {
return {
success: false,
error:
`CodeRabbit CLI exited with code ${error.code}. ` +
`stdout: ${(error.stdout || '').slice(0, 200)}, ` +
`stderr: ${(error.stderr || '').slice(0, 200)}`,
issues: [],
};
}
return {
success: false,
error: error.message,
Expand Down
7 changes: 7 additions & 0 deletions .aiox-core/core/quality-gates/layer2-pr-automation.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,13 @@ class Layer2PRAutomation extends BaseLayer {
}

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

// Parse CodeRabbit output for issues
const issues = this.parseCodeRabbitOutput(result.stdout + result.stderr);
Expand Down
10 changes: 5 additions & 5 deletions .aiox-core/install-manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# - File types for categorization
#
version: 5.2.7
generated_at: "2026-05-18T11:40:45.332Z"
generated_at: "2026-05-18T15:42:34.055Z"
generator: scripts/generate-install-manifest.js
file_count: 1128
files:
Expand Down Expand Up @@ -989,9 +989,9 @@ files:
type: core
size: 31469
- path: core/orchestration/workflow-executor.js
hash: sha256:5bc1c72b7565f3ae5794d4d379d2090c605ca6c383ebbb94a042c5f96b2b9102
hash: sha256:8c56facc975f0452d686183d25ab1c19211afd127814b2ade963b4950352872f
type: core
size: 38289
size: 38673
- path: core/orchestration/workflow-orchestrator.js
hash: sha256:82816bd5e6fecc9bbb77292444e53254c72bf93e5c04260784743354c6a58627
type: core
Expand Down Expand Up @@ -1037,9 +1037,9 @@ files:
type: core
size: 9393
- path: core/quality-gates/layer2-pr-automation.js
hash: sha256:19e52369efe5cf0df9d0b6a9675dd555fdd46c1ab19cf6dc45d9173ad6e6524e
hash: sha256:40a7b03d6294c79741e9313ae91a8d6a30797dca1df4e3ca406edfe2911b4322
type: core
size: 11907
size: 12213
- path: core/quality-gates/layer3-human-review.js
hash: sha256:9bf79d5adfddae55d7dddfda777cd2775aa76f82204ddd0f660f6edbd093b16b
type: core
Expand Down
134 changes: 134 additions & 0 deletions tests/core/orchestration/workflow-executor-coderabbit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/**
* WorkflowExecutor CodeRabbit invocation tests
*
* Regression guards for #763/#764:
* - non-zero CodeRabbit subprocess exits must not pass silently
* - workflow-executor WSL probing mirrors Layer 2 behavior
*/

'use strict';

const childProcess = require('child_process');
const os = require('os');
const path = require('path');

const { WorkflowExecutor } = require('../../../.aiox-core/core/orchestration/workflow-executor');

const expectedDefaultCliPath = path.join(os.homedir(), '/.local/bin/coderabbit');

describe('WorkflowExecutor CodeRabbit analysis', () => {
let originalPlatform;
let spawnSyncSpy;
let execSpy;

beforeEach(() => {
originalPlatform = process.platform;
spawnSyncSpy = jest.spyOn(childProcess, 'spawnSync').mockImplementation((cmd) => {
if (cmd === 'wsl') {
return { status: 0, stdout: 'Ubuntu\n', stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
});
execSpy = jest.spyOn(childProcess, 'exec').mockImplementation((command, options, callback) => {
callback(null, '', '');
return { pid: 1234 };
});
});

afterEach(() => {
Object.defineProperty(process, 'platform', { value: originalPlatform });
spawnSyncSpy.mockRestore();
execSpy.mockRestore();
});

const setPlatform = (value) => {
Object.defineProperty(process, 'platform', { value });
};

const runAnalysis = (config = {}, projectRoot = process.cwd()) => {
const executor = new WorkflowExecutor(projectRoot, { saveState: false, debug: false });
return executor.runCodeRabbitAnalysis({
self_healing: { timeout_minutes: 1 },
...config,
});
};

it('returns an informative failure when CodeRabbit exits non-zero', async () => {
setPlatform('darwin');
execSpy.mockImplementation((command, options, callback) => {
const error = new Error('Command failed');
error.code = 137;
error.stdout = '';
error.stderr = '';
callback(error, '', '');
return { pid: 1234 };
});

const result = await runAnalysis({ installation_mode: 'native' });

expect(result.success).toBe(false);
expect(result.error).toMatch(/CodeRabbit CLI exited with code 137/);
expect(result.error).toContain('stdout:');
expect(result.error).toContain('stderr:');
});

it('surfaces a clear error when WSL binary is missing (ENOENT)', async () => {
setPlatform('darwin');
spawnSyncSpy.mockImplementation((cmd) => {
if (cmd === 'wsl') {
return { error: Object.assign(new Error('ENOENT'), { code: 'ENOENT' }), status: null };
}
return { status: 0, stdout: '', stderr: '' };
});

const result = await runAnalysis({ installation_mode: 'wsl' });

expect(spawnSyncSpy).toHaveBeenCalledWith('wsl', ['-l'], expect.any(Object));
expect(result.success).toBe(false);
expect(result.error).toMatch(/CodeRabbit CLI requires WSL/);
expect(result.error).toMatch(/wsl --install/);
expect(result.error).toMatch(/installation-troubleshooting/);
expect(execSpy).not.toHaveBeenCalled();
});

it('surfaces a clear error when WSL has no usable distribution', async () => {
setPlatform('darwin');
spawnSyncSpy.mockImplementation((cmd) => {
if (cmd === 'wsl') {
return { status: 1, stdout: '', stderr: 'no distros\n' };
}
return { status: 0, stdout: '', stderr: '' };
});

const result = await runAnalysis({ installation_mode: 'wsl' });

expect(spawnSyncSpy).toHaveBeenCalledWith('wsl', ['-l'], expect.any(Object));
expect(result.success).toBe(false);
expect(result.error).toMatch(/CodeRabbit CLI requires WSL/);
expect(execSpy).not.toHaveBeenCalled();
});

it('does not probe WSL when installation_mode=native on Windows', async () => {
setPlatform('win32');

await runAnalysis({ installation_mode: 'native' });

expect(execSpy).toHaveBeenCalled();
const command = execSpy.mock.calls[0][0];
expect(command).toBe(`${expectedDefaultCliPath} --prompt-only -t uncommitted`);
const wslProbeCalls = spawnSyncSpy.mock.calls.filter((c) => c[0] === 'wsl');
expect(wslProbeCalls).toHaveLength(0);
});

it('does not probe WSL for default native mode on macOS', async () => {
setPlatform('darwin');

await runAnalysis();

expect(execSpy).toHaveBeenCalled();
const command = execSpy.mock.calls[0][0];
expect(command).toBe(`${expectedDefaultCliPath} --prompt-only -t uncommitted`);
const wslProbeCalls = spawnSyncSpy.mock.calls.filter((c) => c[0] === 'wsl');
expect(wslProbeCalls).toHaveLength(0);
});
});
16 changes: 16 additions & 0 deletions tests/unit/quality-gates/layer2-pr-automation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,22 @@ describe('Layer2PRAutomation', () => {
expect(result.skipped).toBe(true);
expect(result.message).toContain('not installed');
});

it('should fail when CodeRabbit exits non-zero even without output', async () => {
layer.runCommand = jest.fn().mockResolvedValue({
exitCode: 137,
stdout: '',
stderr: '',
duration: 100,
});

const result = await layer.runCodeRabbit();

expect(result.pass).toBe(false);
expect(result.error).toMatch(/CodeRabbit CLI exited with code 137/);
expect(result.error).toContain('stdout:');
expect(result.error).toContain('stderr:');
});
});

describe('runQuinnReview', () => {
Expand Down
Loading