Skip to content

Commit 6216cda

Browse files
rafaelscostaclaude
andauthored
feat(quality-gates): probe WSL availability before CodeRabbit invocation [#757] (#761)
When running on Windows with WSL mode (default), the runtime now probes `spawnSync('wsl', ['-l'])` before constructing the wsl bash command. If the probe fails with ENOENT (binary missing) or non-zero exit (WSL feature present but no distribution installed), throw a clear error pointing to `wsl --install` documentation, the installation-troubleshooting guide, and the `installation_mode='native'` bypass. Replaces cmd.exe's generic "'wsl' is not recognized as an internal or external command" with framework-aware guidance. ~1ms latency per CodeRabbit invocation on Windows hosts. ## Changes - `.aiox-core/core/orchestration/workflow-executor.js`: switch to namespaced `childProcess` import (was destructured) so jest spies can intercept; add probe at the top of the wsl branch. - `.aiox-core/core/quality-gates/layer2-pr-automation.js`: same pattern. Both files now use `childProcess.spawnSync` directly. - `tests/unit/quality-gates/cross-platform-coderabbit.test.js`: mock `spawnSync` in beforeEach so the existing 12 WSL-mode tests continue to pass on macOS/Linux dev boxes; add 4 new tests covering: - ENOENT probe failure surfaces actionable error message - non-zero exit probe failure surfaces same actionable error message - probe is NOT invoked when installation_mode=native (bypass works) - probe is NOT invoked on macOS native invocation Suite: 16/16 pass (was 12/12). Total 77/77 across all touched files. Closes #757 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e1f64d commit 6216cda

4 files changed

Lines changed: 99 additions & 9 deletions

File tree

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -775,9 +775,9 @@ class WorkflowExecutor {
775775
*/
776776
async runCodeRabbitAnalysis(coderabbitConfig) {
777777
try {
778-
const { exec } = require('child_process');
778+
const childProcess = require('child_process');
779779
const { promisify } = require('util');
780-
const execAsync = promisify(exec);
780+
const execAsync = promisify(childProcess.exec);
781781

782782
// Build command for current platform.
783783
// - Explicit installation_mode: 'wsl' | 'native' wins (lets ops override).
@@ -793,6 +793,20 @@ class WorkflowExecutor {
793793
(process.platform === 'win32' ? 'wsl' : 'native');
794794
let command;
795795
if (mode === 'wsl') {
796+
// Probe WSL availability before building the command. Gives a clearer
797+
// diagnostic than cmd.exe's generic "'wsl' is not recognized" when WSL
798+
// is not installed. ENOENT (binary missing) and non-zero exit (WSL
799+
// feature present but no distribution installed) both fail this check.
800+
const wslProbe = childProcess.spawnSync('wsl', ['-l'], { encoding: 'utf8' });
801+
if (wslProbe.error || wslProbe.status !== 0) {
802+
throw new Error(
803+
'CodeRabbit CLI requires WSL on Windows hosts. Install WSL via ' +
804+
'`wsl --install` (https://learn.microsoft.com/windows/wsl/install), ' +
805+
'then install the CodeRabbit CLI inside the WSL distribution. ' +
806+
'See docs/guides/installation-troubleshooting.md Issue 10. ' +
807+
`To bypass this check, set coderabbit.installation_mode='native' in your config.`
808+
);
809+
}
796810
const wslPath = this.projectRoot
797811
.replace(/^([A-Za-z]):/, (_, drive) => `/mnt/${drive.toLowerCase()}`)
798812
.replace(/\\/g, '/');

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* @story 2.10 - Quality Gate Manager
1111
*/
1212

13-
const { spawn } = require('child_process');
13+
const childProcess = require('child_process');
1414
const fs = require('fs').promises;
1515
const os = require('os');
1616
const path = require('path');
@@ -117,6 +117,21 @@ class Layer2PRAutomation extends BaseLayer {
117117
this.coderabbit.installation_mode ||
118118
(process.platform === 'win32' ? 'wsl' : 'native');
119119
if (mode === 'wsl') {
120+
// Probe WSL availability before building the command. Gives a clearer
121+
// diagnostic than cmd.exe's generic "'wsl' is not recognized" when
122+
// WSL is not installed. ENOENT (binary missing) and non-zero exit
123+
// (WSL feature present but no distribution installed) both fail this
124+
// check.
125+
const wslProbe = childProcess.spawnSync('wsl', ['-l'], { encoding: 'utf8' });
126+
if (wslProbe.error || wslProbe.status !== 0) {
127+
throw new Error(
128+
'CodeRabbit CLI requires WSL on Windows hosts. Install WSL via ' +
129+
'`wsl --install` (https://learn.microsoft.com/windows/wsl/install), ' +
130+
'then install the CodeRabbit CLI inside the WSL distribution. ' +
131+
'See docs/guides/installation-troubleshooting.md Issue 10. ' +
132+
'To bypass this check, set coderabbit.installation_mode=\'native\' in your config.',
133+
);
134+
}
120135
const projectRoot = this.coderabbit.projectRoot || process.cwd();
121136
const wslProjectPath = projectRoot
122137
.replace(/\\/g, '/')
@@ -322,7 +337,7 @@ class Layer2PRAutomation extends BaseLayer {
322337
env: { ...process.env },
323338
};
324339

325-
const child = spawn(command, [], options);
340+
const child = childProcess.spawn(command, [], options);
326341

327342
let stdout = '';
328343
let 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-18T05:37:21.641Z"
11+
generated_at: "2026-05-18T11:40:45.332Z"
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:ddc880683df489ec7203ab7ecd8e066a08e2f72b011fe0dfe578d31155eb513b
992+
hash: sha256:5bc1c72b7565f3ae5794d4d379d2090c605ca6c383ebbb94a042c5f96b2b9102
993993
type: core
994-
size: 37348
994+
size: 38289
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:53e1f05e3ece4731848ad818f13a97ce18f553327b6f2e385e4d299492cc890a
1040+
hash: sha256:19e52369efe5cf0df9d0b6a9675dd555fdd46c1ab19cf6dc45d9173ad6e6524e
10411041
type: core
1042-
size: 10923
1042+
size: 11907
10431043
- path: core/quality-gates/layer3-human-review.js
10441044
hash: sha256:9bf79d5adfddae55d7dddfda777cd2775aa76f82204ddd0f660f6edbd093b16b
10451045
type: core

tests/unit/quality-gates/cross-platform-coderabbit.test.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
const os = require('os');
1919
const path = require('path');
20+
const childProcess = require('child_process');
2021

2122
const { Layer2PRAutomation } = require('../../../.aiox-core/core/quality-gates/layer2-pr-automation');
2223

@@ -27,11 +28,21 @@ describe('Cross-platform CodeRabbit invocation (Issue #731)', () => {
2728
let originalCwd;
2829
let layer;
2930
let capturedCommand;
31+
let spawnSyncSpy;
3032

3133
beforeEach(() => {
3234
originalPlatform = process.platform;
3335
originalCwd = process.cwd;
3436
capturedCommand = null;
37+
// Mock the WSL availability probe added for #757 so legacy command-shape
38+
// tests don't fail on macOS/Linux dev boxes where `wsl` isn't installed.
39+
// Tests covering the probe failure path override this on a per-test basis.
40+
spawnSyncSpy = jest.spyOn(childProcess, 'spawnSync').mockImplementation((cmd) => {
41+
if (cmd === 'wsl') {
42+
return { status: 0, stdout: 'Ubuntu\n', stderr: '' };
43+
}
44+
return { status: 0, stdout: '', stderr: '' };
45+
});
3546
layer = new Layer2PRAutomation({
3647
enabled: true,
3748
coderabbit: { enabled: true },
@@ -47,6 +58,7 @@ describe('Cross-platform CodeRabbit invocation (Issue #731)', () => {
4758
afterEach(() => {
4859
Object.defineProperty(process, 'platform', { value: originalPlatform });
4960
process.cwd = originalCwd;
61+
spawnSyncSpy.mockRestore();
5062
});
5163

5264
const setPlatform = (value) => {
@@ -148,4 +160,53 @@ describe('Cross-platform CodeRabbit invocation (Issue #731)', () => {
148160
// that WSL cannot resolve.
149161
expect(capturedCommand).toContain('~/custom/coderabbit --prompt-only -t uncommitted');
150162
});
163+
164+
// Issue #757 — WSL availability probe before invoking CodeRabbit on Windows
165+
describe('WSL availability probe (Issue #757)', () => {
166+
it('surfaces a clear error when WSL binary is missing (ENOENT)', async () => {
167+
setPlatform('win32');
168+
spawnSyncSpy.mockImplementation((cmd) => {
169+
if (cmd === 'wsl') {
170+
return { error: Object.assign(new Error('ENOENT'), { code: 'ENOENT' }), status: null };
171+
}
172+
return { status: 0, stdout: '', stderr: '' };
173+
});
174+
const result = await layer.runCodeRabbit();
175+
expect(spawnSyncSpy).toHaveBeenCalledWith('wsl', ['-l'], expect.any(Object));
176+
expect(result.pass).toBe(false);
177+
expect(result.error).toMatch(/CodeRabbit CLI requires WSL/);
178+
expect(result.error).toMatch(/wsl --install/);
179+
expect(result.error).toMatch(/installation-troubleshooting/);
180+
});
181+
182+
it('surfaces a clear error when WSL is installed but has no distribution (exit != 0)', async () => {
183+
setPlatform('win32');
184+
spawnSyncSpy.mockImplementation((cmd) => {
185+
if (cmd === 'wsl') {
186+
return { status: 1, stdout: '', stderr: 'no distros\n' };
187+
}
188+
return { status: 0, stdout: '', stderr: '' };
189+
});
190+
const result = await layer.runCodeRabbit();
191+
expect(spawnSyncSpy).toHaveBeenCalledWith('wsl', ['-l'], expect.any(Object));
192+
expect(result.pass).toBe(false);
193+
expect(result.error).toMatch(/CodeRabbit CLI requires WSL/);
194+
});
195+
196+
it('does NOT probe WSL when installation_mode=native on Windows', async () => {
197+
setPlatform('win32');
198+
layer.coderabbit.installation_mode = 'native';
199+
await layer.runCodeRabbit();
200+
// The probe MUST NOT have been called when native mode is explicit.
201+
const wslProbeCalls = spawnSyncSpy.mock.calls.filter((c) => c[0] === 'wsl');
202+
expect(wslProbeCalls).toHaveLength(0);
203+
});
204+
205+
it('does NOT probe WSL on macOS native invocation', async () => {
206+
setPlatform('darwin');
207+
await layer.runCodeRabbit();
208+
const wslProbeCalls = spawnSyncSpy.mock.calls.filter((c) => c[0] === 'wsl');
209+
expect(wslProbeCalls).toHaveLength(0);
210+
});
211+
});
151212
});

0 commit comments

Comments
 (0)