@boyingliu01/xp-gate 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/xp-gate.js +161 -28
- package/hooks/pre-commit +27 -10
- package/lib/__tests__/check-alignment-cli.test.mjs +77 -0
- package/lib/__tests__/doctor-469-hook-env.test.js +12 -7
- package/lib/__tests__/doctor-module-resolution-parity.test.js +15 -6
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +16 -4
- package/lib/__tests__/test-alignment.test.ts +37 -0
- package/lib/__tests__/windows-bash-probe.test.js +73 -0
- package/lib/check-alignment-runner.cjs +74 -0
- package/lib/detect-deps.js +82 -19
- package/lib/gate-runner.js +12 -5
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/sprint-status.js +5 -2
- package/lib/test-alignment.ts +136 -12
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/README.md +1 -1
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +2 -2
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/src/skills.test.ts +20 -7
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/scripts/prepack.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +9 -8
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +3 -4
- package/principles/rules/__tests__/clean-code/god-class.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/large-file.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/long-function.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +2 -3
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +4 -4
- package/principles/rules/__tests__/solid/dip.test.ts +2 -4
- package/principles/rules/__tests__/solid/isp.test.ts +2 -4
- package/principles/rules/__tests__/solid/lsp.test.ts +2 -4
- package/principles/rules/__tests__/solid/ocp.test.ts +2 -4
- package/principles/rules/__tests__/solid/srp.test.ts +2 -4
- package/principles/rules/clean-code/code-duplication.ts +12 -14
- package/principles/rules/clean-code/deep-nesting.ts +18 -20
- package/principles/rules/clean-code/god-class.ts +32 -34
- package/principles/rules/clean-code/large-file.ts +14 -16
- package/principles/rules/clean-code/long-function.ts +26 -28
- package/principles/rules/clean-code/magic-numbers.ts +39 -38
- package/principles/rules/clean-code/many-exports.ts +21 -23
- package/principles/rules/clean-code/missing-error-handling.ts +13 -15
- package/principles/rules/clean-code/too-many-params.ts +15 -17
- package/principles/rules/clean-code/unused-imports.ts +13 -15
- package/principles/rules/solid/dip.ts +16 -18
- package/principles/rules/solid/isp.ts +17 -19
- package/principles/rules/solid/lsp.ts +4 -6
- package/principles/rules/solid/ocp.ts +17 -19
- package/principles/rules/solid/srp.ts +20 -22
- package/scripts/__tests__/sync-package-content.test.js +0 -1
- package/scripts/sync-package-content.js +0 -1
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/qoder/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/skills/clipboard-vision/SKILL.md +0 -48
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
package/bin/xp-gate.js
CHANGED
|
@@ -19,15 +19,127 @@ const { bootstrap } = require('../lib/bootstrap.js');
|
|
|
19
19
|
const { updateHooks } = require('../lib/update-hooks.js');
|
|
20
20
|
const gateRunner = require('../lib/gate-runner.js');
|
|
21
21
|
|
|
22
|
+
// #392-#6: a user-supplied path embedded in a shell command string lets cmd.exe
|
|
23
|
+
// interpret metacharacters (& | ^ < > % ! " and newline) and execute injected
|
|
24
|
+
// commands. On POSIX a bare spawn (shell:false) passes argv as discrete,
|
|
25
|
+
// uninterpreted bytes, so nothing is injectable. On win32 a `.cmd` wrapper such
|
|
26
|
+
// as npx.cmd cannot be launched without a shell (CreateProcess raises EINVAL),
|
|
27
|
+
// so we must shell out through cmd.exe -- and we only do so AFTER rejecting any
|
|
28
|
+
// argument that carries a cmd metacharacter. Rejection, not escaping, is the
|
|
29
|
+
// safe defense: a path containing a metachar is refused outright rather than
|
|
30
|
+
// risk a quoting mistake. This guard must stay in front of every launcher.
|
|
31
|
+
// \r joins \n: cmd treats a stray carriage return as a command separator too
|
|
32
|
+
// (Delphi walkthrough R1, MN-01).
|
|
33
|
+
const CMD_METACHAR_RE = /[&|^<>%!"\n\r]/;
|
|
34
|
+
|
|
35
|
+
// The ONLY place a child process may be launched with an engaged shell.
|
|
36
|
+
// Named so a static grep stays readable, and pinned by check-alignment-cli
|
|
37
|
+
// .test.mjs: no other launcher in this file may enable a shell.
|
|
38
|
+
const SHELL_ON_WINDOWS_ONLY = process.platform === 'win32';
|
|
39
|
+
|
|
40
|
+
function firstUnsafeArg(argv) {
|
|
41
|
+
for (const arg of argv) {
|
|
42
|
+
if (typeof arg === 'string' && CMD_METACHAR_RE.test(arg)) return arg;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function quoteForCmdShell(arg) {
|
|
48
|
+
// With an engaged shell node concatenates file+args into one cmd.exe command
|
|
49
|
+
// string WITHOUT quoting (DEP0190), so an argument carrying a space would
|
|
50
|
+
// split into multiple argv elements downstream. Double-quoting is safe here
|
|
51
|
+
// ONLY because every caller has already run firstUnsafeArg: the character "
|
|
52
|
+
// that could break out of the quote is on the rejection list.
|
|
53
|
+
return `"${arg}"`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function spawnNpx(tsxArgs) {
|
|
57
|
+
const { spawnSync } = require('child_process');
|
|
58
|
+
// Reject any user-supplied argument that could be interpreted by cmd.exe.
|
|
59
|
+
// Every element of tsxArgs beyond the fixed tokens (-y tsx <script>) is a
|
|
60
|
+
// user path; a metachar there is refused, not quoted, per #392-#6.
|
|
61
|
+
const unsafe = firstUnsafeArg(tsxArgs);
|
|
62
|
+
if (unsafe) {
|
|
63
|
+
console.error(`check-alignment: refusing path containing a shell metacharacter: ${unsafe}`);
|
|
64
|
+
return { status: 1 };
|
|
65
|
+
}
|
|
66
|
+
const isWin = process.platform === 'win32';
|
|
67
|
+
// The shell engages on win32 only, and is only safe because every user
|
|
68
|
+
// argument above is guaranteed metachar-free; each token additionally gets
|
|
69
|
+
// cmd quoting so spaces survive (see quoteForCmdShell).
|
|
70
|
+
// Guarded IO: a failed spawn must surface as a result, not crash the CLI.
|
|
71
|
+
const argv = ['-y', 'tsx', ...tsxArgs];
|
|
72
|
+
let result;
|
|
73
|
+
try {
|
|
74
|
+
result = spawnSync(isWin ? 'npx.cmd' : 'npx', isWin ? argv.map(quoteForCmdShell) : argv, {
|
|
75
|
+
stdio: 'inherit',
|
|
76
|
+
shell: SHELL_ON_WINDOWS_ONLY,
|
|
77
|
+
});
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return { status: 1, error: err };
|
|
80
|
+
}
|
|
81
|
+
return result || { status: 1 };
|
|
82
|
+
}
|
|
83
|
+
|
|
22
84
|
function handleUIReview() {
|
|
23
|
-
const { execSync } = require('child_process');
|
|
24
85
|
const uiReviewPath = path.join(__dirname, '..', 'lib', 'ui-review.ts');
|
|
86
|
+
const result = spawnNpx([uiReviewPath]);
|
|
87
|
+
process.exitCode = result.error ? 1 : (result.status === 0 ? 0 : 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function runAlignmentInProcess(specPath, testDirs, outputPath, jsonMode, runFullAlignment) {
|
|
91
|
+
const { execFileSync } = require('child_process');
|
|
92
|
+
if (!runFullAlignment) {
|
|
93
|
+
// Loaded lazily by the caller inside the tsx guard; kept optional so an
|
|
94
|
+
// in-repo direct call still works.
|
|
95
|
+
({ runFullAlignment } = require('../lib/test-alignment.ts'));
|
|
96
|
+
}
|
|
97
|
+
let headCommit = 'unknown';
|
|
25
98
|
try {
|
|
26
|
-
|
|
27
|
-
process.exit(0);
|
|
99
|
+
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() || 'unknown';
|
|
28
100
|
} catch (err) {
|
|
29
|
-
|
|
101
|
+
headCommit = 'unknown';
|
|
30
102
|
}
|
|
103
|
+
let report;
|
|
104
|
+
try {
|
|
105
|
+
report = runFullAlignment({
|
|
106
|
+
specPath,
|
|
107
|
+
testDirs: testDirs || undefined,
|
|
108
|
+
headCommit,
|
|
109
|
+
outputPath: outputPath || undefined,
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('check-alignment failed:', err instanceof Error ? err.message : String(err));
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
if (jsonMode) {
|
|
116
|
+
console.log(JSON.stringify(report, null, 2));
|
|
117
|
+
} else {
|
|
118
|
+
console.log('Status:', report.alignment_status);
|
|
119
|
+
console.log('Score:', report.score);
|
|
120
|
+
console.log('Misaligned:', report.misaligned_tests.length);
|
|
121
|
+
}
|
|
122
|
+
// exitCode-style contract (#392 review): never process.exit() right after a
|
|
123
|
+
// JSON print — buffered stdout must flush. Returning the code and letting the
|
|
124
|
+
// dispatcher end the turn naturally is what keeps --json output complete.
|
|
125
|
+
return report.alignment_status === 'PASS' ? 0 : 1;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function runAlignmentViaNpx(runnerPath, specPath, testDirs, outputPath, jsonMode) {
|
|
129
|
+
// User-supplied paths travel as discrete argv elements routed through
|
|
130
|
+
// spawnNpx, which refuses any argument carrying a cmd metacharacter before a
|
|
131
|
+
// shell can see it (#392-#6). The runner script path is a fixed internal path.
|
|
132
|
+
const argv = [runnerPath, '--spec', specPath];
|
|
133
|
+
if (testDirs) argv.push('--tests', testDirs[0]);
|
|
134
|
+
if (outputPath) argv.push('--output', outputPath);
|
|
135
|
+
if (jsonMode) argv.push('--json');
|
|
136
|
+
const result = spawnNpx(argv);
|
|
137
|
+
if (result.error) {
|
|
138
|
+
console.error('check-alignment: unable to load the alignment engine (tsx unavailable).');
|
|
139
|
+
console.error(result.error.message);
|
|
140
|
+
return 1;
|
|
141
|
+
}
|
|
142
|
+
return result.status === 0 ? 0 : 1;
|
|
31
143
|
}
|
|
32
144
|
|
|
33
145
|
async function runAlignmentCheck(subargs) {
|
|
@@ -42,32 +154,43 @@ async function runAlignmentCheck(subargs) {
|
|
|
42
154
|
|
|
43
155
|
const jsonMode = subargs.includes('--json');
|
|
44
156
|
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
157
|
+
// Injection guard (#392-#6) runs BEFORE any launcher, including the
|
|
158
|
+
// in-process fast path: a metachar path must be refused identically on both
|
|
159
|
+
// platforms (Delphi walkthrough R1, MJ-02 — behavioral, not shape-only).
|
|
160
|
+
for (const value of [specPath, ...(testDirs || []), outputPath].filter(v => v !== undefined)) {
|
|
161
|
+
const unsafe = firstUnsafeArg([value]);
|
|
162
|
+
if (unsafe) {
|
|
163
|
+
console.error(`check-alignment: refusing path containing a shell metacharacter: ${unsafe}`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const runnerPath = path.join(__dirname, '..', 'lib', 'check-alignment-runner.cjs');
|
|
48
169
|
|
|
170
|
+
// Primary: run the engine in-process via tsx/cjs (fast path, works in-repo
|
|
171
|
+
// where tsx is a devDependency). Fall back to `npx -y tsx` when tsx is not
|
|
172
|
+
// resolvable, which happens in the published global install (the package
|
|
173
|
+
// ships zero runtime deps per the zero-install contract). npx self-provisions
|
|
174
|
+
// tsx on demand, so the published command stays functional without adding a dep.
|
|
175
|
+
//
|
|
176
|
+
// The try guards ONLY the tsx loader's module-load step. The engine call sits
|
|
177
|
+
// outside it: an engine-level failure is a real bug that must surface, not be
|
|
178
|
+
// silently swallowed and misattributed to "tsx unavailable" by the fallback.
|
|
179
|
+
// Delphi walkthrough R2 CI-02: require('../lib/test-alignment.ts') also needs
|
|
180
|
+
// the loader, so it belongs INSIDE the guard — if the .ts file itself cannot
|
|
181
|
+
// load, the npx fallback re-attempts it under a provisioned tsx instead of
|
|
182
|
+
// crashing with an unhandled exception. Engine EXECUTION stays outside.
|
|
183
|
+
let runFullAlignment;
|
|
49
184
|
try {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const report = runFullAlignment({
|
|
53
|
-
specPath: '${specPath}',
|
|
54
|
-
testDirs: ${JSON.stringify(testDirs || null)},
|
|
55
|
-
headCommit: require('child_process').execSync('git rev-parse HEAD 2>/dev/null || echo unknown').toString().trim(),
|
|
56
|
-
outputPath: ${outputPath ? `'${outputPath}'` : 'undefined'},
|
|
57
|
-
});
|
|
58
|
-
if (${jsonMode}) {
|
|
59
|
-
console.log(JSON.stringify(report, null, 2));
|
|
60
|
-
} else {
|
|
61
|
-
console.log('Status:', report.alignment_status);
|
|
62
|
-
console.log('Score:', report.score);
|
|
63
|
-
console.log('Misaligned:', report.misaligned_tests.length);
|
|
64
|
-
}
|
|
65
|
-
process.exit(report.alignment_status === 'PASS' ? 0 : 1);
|
|
66
|
-
"`, { stdio: 'inherit' });
|
|
185
|
+
require('tsx/cjs');
|
|
186
|
+
({ runFullAlignment } = require('../lib/test-alignment.ts'));
|
|
67
187
|
} catch (err) {
|
|
68
|
-
|
|
188
|
+
if (!err || (err.code !== 'MODULE_NOT_FOUND' && !/Cannot find module/.test(String(err && err.message)))) {
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
return runAlignmentViaNpx(runnerPath, specPath, testDirs, outputPath, jsonMode);
|
|
69
192
|
}
|
|
70
|
-
return
|
|
193
|
+
return runAlignmentInProcess(specPath, testDirs, outputPath, jsonMode, runFullAlignment);
|
|
71
194
|
}
|
|
72
195
|
|
|
73
196
|
const COMMANDS = {
|
|
@@ -154,7 +277,7 @@ const COMMANDS = {
|
|
|
154
277
|
}
|
|
155
278
|
return doctor(subargs).then(code => process.exit(code));
|
|
156
279
|
},
|
|
157
|
-
usage: 'xp-gate doctor [--fix [--install-tools] | --sync-hooks [--force]] | --json | --format json (the two machine-readable modes print one JSON document and refuse --fix/--sync-hooks)'
|
|
280
|
+
usage: 'xp-gate doctor [--fix [--install-tools] | --sync-hooks [--force] | --refresh-env] | --json | --format json (the two machine-readable modes print one JSON document and refuse --fix/--sync-hooks; --refresh-env is read-only so it combines with them)'
|
|
158
281
|
},
|
|
159
282
|
'ui-review': {
|
|
160
283
|
description: 'Run UI review for non-sprint developers (generates .ui-gate-result.json)',
|
|
@@ -188,7 +311,17 @@ const COMMANDS = {
|
|
|
188
311
|
},
|
|
189
312
|
'check-alignment': {
|
|
190
313
|
description: 'Run test-specification alignment check (deterministic, no LLM)',
|
|
191
|
-
run: subargs => runAlignmentCheck(subargs).then(code =>
|
|
314
|
+
run: subargs => runAlignmentCheck(subargs).then(code => {
|
|
315
|
+
// exitCode, not process.exit(): the in-process --json document must flush
|
|
316
|
+
// its buffered stdout before the process ends (#392 review, MJ-01).
|
|
317
|
+
process.exitCode = code;
|
|
318
|
+
}).catch(err => {
|
|
319
|
+
// A quality gate must fail with a deterministic exit code and a real
|
|
320
|
+
// message — never an unhandled rejection whose code depends on Node's
|
|
321
|
+
// default mode (Delphi walkthrough R2, MJ-02).
|
|
322
|
+
console.error('check-alignment: unexpected failure:', err instanceof Error ? err.message : String(err));
|
|
323
|
+
process.exitCode = 1;
|
|
324
|
+
}),
|
|
192
325
|
usage: 'xp-gate check-alignment [--spec <path>] [--tests <dir>] [--json] [--output <path>]'
|
|
193
326
|
},
|
|
194
327
|
'upgrade': {
|
package/hooks/pre-commit
CHANGED
|
@@ -179,11 +179,15 @@ run_in_subdir() {
|
|
|
179
179
|
|
|
180
180
|
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ]; then
|
|
181
181
|
# Run command in subdirectory, then return to original directory
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
# #392-#10: guard cd/pushd so a missing directory cannot run the gate in
|
|
183
|
+
# the wrong cwd.
|
|
184
|
+
if pushd "$PROJECT_SUBDIR" > /dev/null 2>&1; then
|
|
185
|
+
eval "$cmd"
|
|
186
|
+
local exit_code=$?
|
|
187
|
+
popd > /dev/null 2>&1
|
|
188
|
+
return $exit_code
|
|
189
|
+
fi
|
|
190
|
+
return 1
|
|
187
191
|
else
|
|
188
192
|
# Run in current directory
|
|
189
193
|
eval "$cmd"
|
|
@@ -984,10 +988,14 @@ IN_PROJECT_SUBDIR=0
|
|
|
984
988
|
_lang_count=$(echo "$PROJECT_LANGS" | wc -w)
|
|
985
989
|
if [ -n "$PROJECT_SUBDIR" ] && [ "$PROJECT_SUBDIR" != "." ] && [ "$PROJECT_LANG" != "documentation-only" ] && [ "$_lang_count" -le 1 ]; then
|
|
986
990
|
ORIGINAL_DIR=$(pwd)
|
|
987
|
-
cd
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
+
# #392-#10: guard cd so a missing subdir cannot run gates in the wrong cwd.
|
|
992
|
+
if cd "$PROJECT_SUBDIR" 2>/dev/null; then
|
|
993
|
+
IN_PROJECT_SUBDIR=1
|
|
994
|
+
echo "Working in: $PROJECT_SUBDIR/"
|
|
995
|
+
echo ""
|
|
996
|
+
else
|
|
997
|
+
echo "WARN: could not enter $PROJECT_SUBDIR — continuing at repo root" >&2
|
|
998
|
+
fi
|
|
991
999
|
fi
|
|
992
1000
|
|
|
993
1001
|
# ============================================================================
|
|
@@ -1639,7 +1647,16 @@ else
|
|
|
1639
1647
|
echo "Running jscpd for Go duplicate code detection..."
|
|
1640
1648
|
FILES_TO_CHECK=$(echo "$CHANGED_FILES" | grep "\.go$" | head -20)
|
|
1641
1649
|
if [ -n "$FILES_TO_CHECK" ]; then
|
|
1642
|
-
jscpd
|
|
1650
|
+
# #392-#9: capture jscpd's real exit code via temp file instead of piping
|
|
1651
|
+
# through head (which would swallow the exit status under pipefail analysis).
|
|
1652
|
+
GO_JSCPD_TMP=$(mktemp)
|
|
1653
|
+
jscpd $FILES_TO_CHECK > "$GO_JSCPD_TMP" 2>&1
|
|
1654
|
+
JSCPD_GO_EXIT=$?
|
|
1655
|
+
head -10 "$GO_JSCPD_TMP"
|
|
1656
|
+
rm -f "$GO_JSCPD_TMP"
|
|
1657
|
+
if [ "$JSCPD_GO_EXIT" -ne 0 ]; then
|
|
1658
|
+
echo "⚠️ jscpd (Go) exited with code $JSCPD_GO_EXIT"
|
|
1659
|
+
fi
|
|
1643
1660
|
fi
|
|
1644
1661
|
echo "✅ PASSED - Go duplicate code check completed."
|
|
1645
1662
|
;;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test check-alignment CLI
|
|
3
|
+
* @intent Guard the #392-#6 injection fix and the zero-install fallback path:
|
|
4
|
+
* (1) no spawn/exec call in bin/xp-gate.js may enable a shell (which on
|
|
5
|
+
* win32 would let cmd.exe interpret metacharacters embedded in
|
|
6
|
+
* user-supplied paths); (2) the published runner emits a complete,
|
|
7
|
+
* parseable --json document instead of truncating it on process.exit().
|
|
8
|
+
* @covers AC-392-06 (injection), zero-install contract (fallback)
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { readFileSync } from 'fs';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
|
+
import { spawnSync } from 'child_process';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const BIN = join(here, '..', '..', 'bin', 'xp-gate.js');
|
|
18
|
+
const RUNNER = join(here, '..', 'check-alignment-runner.cjs');
|
|
19
|
+
|
|
20
|
+
describe('check-alignment CLI injection surface (#392-#6)', () => {
|
|
21
|
+
it('bin/xp-gate.js rejects shell metacharacters before any launch', () => {
|
|
22
|
+
const src = readFileSync(BIN, 'utf8');
|
|
23
|
+
// The injection guard must be in front of every subprocess launch: any
|
|
24
|
+
// argument carrying a cmd.exe metacharacter is refused outright, never
|
|
25
|
+
// escaped, before a shell can see it (#392-#6, Delphi R1-C). This holds on
|
|
26
|
+
// both platforms -- on win32 a .cmd wrapper needs cmd.exe (EINVAL without
|
|
27
|
+
// one), so the metachar guard is the only thing that makes shelling safe.
|
|
28
|
+
expect(src).toMatch(/CMD_METACHAR_RE/);
|
|
29
|
+
expect(src).toMatch(/firstUnsafeArg/);
|
|
30
|
+
// The metachar class must cover the cmd.exe separators/redirection.
|
|
31
|
+
expect(src).toMatch(/CMD_METACHAR_RE\s*=.*[&|^<>%!]/);
|
|
32
|
+
// Any shell a launcher may engage must be gated on the metachar rejection
|
|
33
|
+
// having run first, and never enabled unconditionally on win32. The single
|
|
34
|
+
// sanctioned engagement is the named SHELL_ON_WINDOWS_ONLY constant.
|
|
35
|
+
expect(src).not.toMatch(/shell\s*:\s*(?:true|process\.platform)/);
|
|
36
|
+
expect(src).toMatch(/shell:\s*SHELL_ON_WINDOWS_ONLY/);
|
|
37
|
+
// execSync interpolates argv into a command string via a shell, so it is
|
|
38
|
+
// banned; execFileSync is fine only for hardcoded binaries with non-user
|
|
39
|
+
// argv (git rev-parse HEAD). The launchers below are the user-input path.
|
|
40
|
+
expect(src).not.toMatch(/\bexecSync\s*\(/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('a metacharacter path is refused end to end before any launch (MJ-02)', () => {
|
|
44
|
+
// Behavioral, not shape: invoke the real CLI with a --spec whose path
|
|
45
|
+
// carries a cmd metacharacter and assert the documented refusal — exit 1
|
|
46
|
+
// plus the "refusing" message, never a spawned engine.
|
|
47
|
+
const evil = join(here, 'evil&echo.spec.yaml');
|
|
48
|
+
const result = spawnSync(process.execPath, [BIN, 'check-alignment', '--spec', evil, '--json'], {
|
|
49
|
+
encoding: 'utf8',
|
|
50
|
+
timeout: 60000,
|
|
51
|
+
});
|
|
52
|
+
expect(result.status).toBe(1);
|
|
53
|
+
expect(`${result.stderr}${result.stdout}`).toMatch(/metacharacter|refus/i);
|
|
54
|
+
}, 90000);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('check-alignment runner output integrity', () => {
|
|
58
|
+
it('emits a complete parseable JSON document with --json', () => {
|
|
59
|
+
const spec = join(here, 'test-alignment.test.ts');
|
|
60
|
+
const result = spawnSync(process.execPath, [RUNNER, '--spec', spec, '--json'], {
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
});
|
|
63
|
+
expect(result.error).toBeUndefined();
|
|
64
|
+
// The whole stdout stream must be one valid JSON document. A process.exit()
|
|
65
|
+
// immediately after console.log would truncate it on some platforms.
|
|
66
|
+
const parsed = JSON.parse(result.stdout);
|
|
67
|
+
expect(parsed).toHaveProperty('alignment_status');
|
|
68
|
+
expect(parsed).toHaveProperty('score');
|
|
69
|
+
expect(parsed).toHaveProperty('head_commit');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('fails fast with exit code 1 when --spec is missing', () => {
|
|
73
|
+
const result = spawnSync(process.execPath, [RUNNER], { encoding: 'utf8' });
|
|
74
|
+
expect(result.status).toBe(1);
|
|
75
|
+
expect(result.stderr).toContain('--spec <path> is required');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -17,11 +17,16 @@ const describeWin = isWindows ? describe : describe.skip;
|
|
|
17
17
|
// Walkthrough FC-02: a bare `return` inside it() silently reports PASS on a
|
|
18
18
|
// runner without bash — a false-green in the very suite that guards against
|
|
19
19
|
// false greens. Bash-gated tests must SKIP loudly instead.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
// On Windows a bare PATH 'bash' may resolve to the WSL System32 stub, whose
|
|
21
|
+
// argv handling drops `-c` positional args — probing through it answers for
|
|
22
|
+
// the wrong environment (the exact #469 failure class). Resolve the same
|
|
23
|
+
// authoritative bash the hooks get: checkBash().
|
|
24
|
+
const { checkBash } = require('../detect-deps.js');
|
|
25
|
+
const BASH_PROBE = (() => {
|
|
26
|
+
const b = checkBash();
|
|
27
|
+
return b.ok ? b.path : null;
|
|
23
28
|
})();
|
|
24
|
-
const itWithBash =
|
|
29
|
+
const itWithBash = BASH_PROBE ? it : it.skip;
|
|
25
30
|
|
|
26
31
|
describe('doctor hook-env probing (#469)', () => {
|
|
27
32
|
function mod() {
|
|
@@ -30,7 +35,7 @@ describe('doctor hook-env probing (#469)', () => {
|
|
|
30
35
|
|
|
31
36
|
itWithBash('AC-469-01: a tool the hook env sees is reported with its hook-env path', async () => {
|
|
32
37
|
const { probeToolInHookEnv } = mod();
|
|
33
|
-
const bashPath = isWindows ?
|
|
38
|
+
const bashPath = isWindows ? BASH_PROBE : '/bin/sh';
|
|
34
39
|
const p = await probeToolInHookEnv(isWindows ? 'node' : 'sh', bashPath);
|
|
35
40
|
expect(typeof p).toBe('string');
|
|
36
41
|
expect(p.length).toBeGreaterThan(0);
|
|
@@ -39,7 +44,7 @@ describe('doctor hook-env probing (#469)', () => {
|
|
|
39
44
|
itWithBash('AC-469-01: a tool absent from the hook env returns null — no fall-through', async () => {
|
|
40
45
|
const { detectCliToolAsync } = mod();
|
|
41
46
|
const result = await detectCliToolAsync('definitely-not-a-real-tool-xyz-469', isWindows, {
|
|
42
|
-
bashPath:
|
|
47
|
+
bashPath: BASH_PROBE,
|
|
43
48
|
});
|
|
44
49
|
// With bash present there is NO fall-through: `where` finding nothing is
|
|
45
50
|
// irrelevant here, the hook env verdict is final.
|
|
@@ -56,7 +61,7 @@ describe('doctor hook-env probing (#469)', () => {
|
|
|
56
61
|
|
|
57
62
|
itWithBash('AC-469-01: an installed tool is visible through the hook-env tier', async () => {
|
|
58
63
|
const { detectCliToolAsync } = mod();
|
|
59
|
-
const result = await detectCliToolAsync('node', true, { bashPath:
|
|
64
|
+
const result = await detectCliToolAsync('node', true, { bashPath: BASH_PROBE });
|
|
60
65
|
expect(result.available).toBe(true);
|
|
61
66
|
// The hook-env path is POSIX form on Windows — cmd.exe cannot run it.
|
|
62
67
|
if (isWindows) expect(result.path.startsWith('/')).toBe(true);
|
|
@@ -51,13 +51,22 @@ function matchBlock(source, pattern, label) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function bashBin() {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
if (process.env.XP_GATE_BASH_BIN) {
|
|
55
|
+
const probe = spawnSync(process.env.XP_GATE_BASH_BIN, ['-c', 'echo ok'], { encoding: 'utf8' });
|
|
56
|
+
if (probe.status !== 0 || probe.error) {
|
|
57
|
+
throw new Error(`bash runner unavailable (${process.env.XP_GATE_BASH_BIN}): ${probe.error ? probe.error.message : `exit ${probe.status}`}`);
|
|
58
|
+
}
|
|
59
|
+
return process.env.XP_GATE_BASH_BIN;
|
|
60
|
+
}
|
|
61
|
+
// Resolve the authoritative bash the hooks actually get. On Windows a bare
|
|
62
|
+
// PATH 'bash' may be the WSL System32 stub, whose `-c` scripts receive an
|
|
63
|
+
// empty $1 and resolve nothing — a vacuous resolver run, not a passing one.
|
|
64
|
+
const { checkBash } = require('../detect-deps.js');
|
|
65
|
+
const b = checkBash();
|
|
66
|
+
if (!b.ok) {
|
|
67
|
+
throw new Error(`bash runner unavailable: ${b.message || 'checkBash failed'}`);
|
|
59
68
|
}
|
|
60
|
-
return
|
|
69
|
+
return b.path;
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
/**
|
|
@@ -32,12 +32,20 @@ describe('doctor-tui skill freshness (#439)', () => {
|
|
|
32
32
|
// HOME has no platform marker, so detectPlatform() resolves to opencode.
|
|
33
33
|
function installSkill(name, content) {
|
|
34
34
|
const dir = path.join(tmpHome, '.config', 'opencode', 'skills', name);
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
try {
|
|
36
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
37
|
+
fs.writeFileSync(path.join(dir, 'SKILL.md'), content);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
throw new Error(`failed to install test skill ${name}: ${err && err.message}`);
|
|
40
|
+
}
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
function readBundled(name) {
|
|
40
|
-
|
|
44
|
+
try {
|
|
45
|
+
return fs.readFileSync(path.join(bundledSkillsDir, name, 'SKILL.md'), 'utf8');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`failed to read bundled skill ${name}: ${err && err.message}`);
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
/** Same bytes, opposite line endings — the only difference a fresh install can have. */
|
|
@@ -134,7 +142,11 @@ describe('doctor-tui skill freshness (#439)', () => {
|
|
|
134
142
|
.readdirSync(bundledSkillsDir, { withFileTypes: true })
|
|
135
143
|
.filter((d) => d.isDirectory())
|
|
136
144
|
.map((d) => d.name);
|
|
137
|
-
|
|
145
|
+
// Sanity floor only: the substantive assertion below is that every bundled
|
|
146
|
+
// skill declares a version. #522 removed clipboard-vision, dropping the
|
|
147
|
+
// bundle from 13 to 12 skills, so the floor tracks the current bundle size
|
|
148
|
+
// instead of a stale magic number (Delphi Round 1, finding A-Critical).
|
|
149
|
+
expect(skills.length).toBeGreaterThanOrEqual(12);
|
|
138
150
|
const missing = skills.filter((name) => {
|
|
139
151
|
const md = path.join(bundledSkillsDir, name, 'SKILL.md');
|
|
140
152
|
return !/^version:\s*\d+\.\d+\.\d+/m.test(fs.readFileSync(md, 'utf8'));
|
|
@@ -325,6 +325,43 @@ test('multiple assertions', () => {
|
|
|
325
325
|
expect(result.tests[0].assertions).toBeGreaterThanOrEqual(4);
|
|
326
326
|
expect(result.totalAssertions).toBeGreaterThanOrEqual(4);
|
|
327
327
|
});
|
|
328
|
+
|
|
329
|
+
it('AC-FIXTURE-ASSERT: should count assertions per-test, not per-file (#392)', async () => {
|
|
330
|
+
// Regression for #392-#1: assertion counting must be scoped to the test
|
|
331
|
+
// body, otherwise totalAssertions is inflated by fileCount x perFileCount.
|
|
332
|
+
const testTs = `
|
|
333
|
+
/**
|
|
334
|
+
* @test REQ-MULTI-001 First test
|
|
335
|
+
* @intent Assertion counting is per-test
|
|
336
|
+
* @covers AC-FIXTURE-MULTI-01
|
|
337
|
+
*/
|
|
338
|
+
test('first test', () => {
|
|
339
|
+
expect(a()).toBe(1);
|
|
340
|
+
expect(b()).toBe(2);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @test REQ-MULTI-002 Second test
|
|
345
|
+
* @intent Assertion counting is per-test
|
|
346
|
+
* @covers AC-FIXTURE-MULTI-02
|
|
347
|
+
*/
|
|
348
|
+
test('second test', () => {
|
|
349
|
+
expect(c()).toBe(3);
|
|
350
|
+
});
|
|
351
|
+
`;
|
|
352
|
+
const testFile = writeTempFile('multi-assert.test.ts', testTs);
|
|
353
|
+
const result = parseTestFiles([testFile]);
|
|
354
|
+
|
|
355
|
+
expect(result.tests).toHaveLength(2);
|
|
356
|
+
// countAssertions counts matching *patterns*, so `expect(x).toBe(y)` yields
|
|
357
|
+
// 2 (one `expect(` + one `toBe(`). First test body has 2 such assertions
|
|
358
|
+
// => 4; second has 1 => 2. The key regression: each test counts ONLY its own
|
|
359
|
+
// body (total 6), never the whole file (3 expect + 3 toBe = 6 per test,
|
|
360
|
+
// which would inflate totalAssertions to 12 under the old per-file bug).
|
|
361
|
+
expect(result.tests[0].assertions).toBe(4);
|
|
362
|
+
expect(result.tests[1].assertions).toBe(2);
|
|
363
|
+
expect(result.totalAssertions).toBe(6);
|
|
364
|
+
});
|
|
328
365
|
});
|
|
329
366
|
|
|
330
367
|
// ============================================================================
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-windows-bash probe behavior
|
|
3
|
+
* @intent resolveWindowsBashCandidates must accept only a bash that round-trips
|
|
4
|
+
* `-c` positional argv AND reports MINGW/MSYS/CYGWIN via uname — the WSL
|
|
5
|
+
* System32 stub (Linux uname, dropped $1) is rejected even when PATH
|
|
6
|
+
* leads with it. Behavior is exercised through an injected fake spawner,
|
|
7
|
+
* so no real interpreter configuration is assumed on the host (#469
|
|
8
|
+
* follow-up, Delphi walkthrough R1 MN-06).
|
|
9
|
+
* @covers AC-WINBASH-01
|
|
10
|
+
*/
|
|
11
|
+
const { describe, it, expect } = await import('vitest');
|
|
12
|
+
const { resolveWindowsBashCandidates } = require('../detect-deps.js');
|
|
13
|
+
|
|
14
|
+
// The candidate loop reads `where bash` from the host; tests inject both the
|
|
15
|
+
// discovery runner and the probe spawner, so results are deterministic on any
|
|
16
|
+
// machine regardless of what interpreter configuration it happens to have.
|
|
17
|
+
function fakeWhere(whereOutput) {
|
|
18
|
+
return (cmd) => {
|
|
19
|
+
if (/^where bash/.test(cmd)) {
|
|
20
|
+
if (whereOutput instanceof Error) throw whereOutput;
|
|
21
|
+
return whereOutput || '';
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`unexpected execSync: ${cmd}`);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fakeSpawn(behaviors) {
|
|
28
|
+
const probed = [];
|
|
29
|
+
const fn = (candidate) => {
|
|
30
|
+
probed.push(candidate);
|
|
31
|
+
const b = behaviors[candidate];
|
|
32
|
+
if (b instanceof Error) throw b;
|
|
33
|
+
// Unknown candidates model a missing file: no stdout at all.
|
|
34
|
+
return { stdout: b !== undefined ? b : '' };
|
|
35
|
+
};
|
|
36
|
+
fn.probed = probed;
|
|
37
|
+
return fn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('resolveWindowsBashCandidates (win32 authoritative bash)', () => {
|
|
41
|
+
it('AC-WINBASH-01: rejects a WSL stub whose $1 is empty and uname is Linux', () => {
|
|
42
|
+
const spawn = fakeSpawn({ 'C:\\Windows\\System32\\bash.exe': '[ ]\nLinux\n' });
|
|
43
|
+
// No MSYS candidate exists in this world (hardcoded fallbacks report no
|
|
44
|
+
// stdout), so the resolver must find nothing rather than settle for the stub.
|
|
45
|
+
expect(resolveWindowsBashCandidates(spawn, fakeWhere('C:\\Windows\\System32\\bash.exe\r\n'))).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('AC-WINBASH-01: accepts the first candidate with argv round-trip + MINGW uname', () => {
|
|
49
|
+
const spawn = fakeSpawn({
|
|
50
|
+
'C:\\a\\bash.exe': '[]\nLinux\n', // drops $1 → reject
|
|
51
|
+
'C:\\b\\bash.exe': '[XPGATE_ARGV_PROBE]\nMINGW64_NT-10.0\n', // accept
|
|
52
|
+
});
|
|
53
|
+
const result = resolveWindowsBashCandidates(spawn, fakeWhere('C:\\a\\bash.exe\r\nC:\\b\\bash.exe\r\n'));
|
|
54
|
+
expect(result).toBe('C:\\b\\bash.exe');
|
|
55
|
+
expect(spawn.probed.slice(0, 2)).toEqual(['C:\\a\\bash.exe', 'C:\\b\\bash.exe']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('AC-WINBASH-01: a Linux uname with intact argv still fails the platform test', () => {
|
|
59
|
+
// A real WSL bash answers [XPGATE_ARGV_PROBE] correctly under some
|
|
60
|
+
// invocations but its filesystem view cannot run these hooks — uname wins.
|
|
61
|
+
const spawn = fakeSpawn({ 'C:\\wsl\\bash.exe': '[XPGATE_ARGV_PROBE]\nLinux\n' });
|
|
62
|
+
expect(resolveWindowsBashCandidates(spawn, fakeWhere('C:\\wsl\\bash.exe\r\n'))).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('AC-WINBASH-01: a throwing candidate is skipped, not fatal', () => {
|
|
66
|
+
const spawn = fakeSpawn({
|
|
67
|
+
'C:\\gone\\bash.exe': new Error('ENOENT'),
|
|
68
|
+
'C:\\msys64\\usr\\bin\\bash.exe': '[XPGATE_ARGV_PROBE]\nMSYS_NT-10.0\n',
|
|
69
|
+
});
|
|
70
|
+
const result = resolveWindowsBashCandidates(spawn, fakeWhere(new Error('where unavailable')));
|
|
71
|
+
expect(result).toBe('C:\\msys64\\usr\\bin\\bash.exe');
|
|
72
|
+
});
|
|
73
|
+
});
|