mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-23 03:27:50 +09:00
test: run the Hermes hook tests with a resolved Python 3 on Windows
- windows-test-env.js gains resolvePython: it probes each candidate with `-c "import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)"`, so the Store stub is rejected. - hermes-hooks.test.js launches its four hook spawns through the resolved interpreter, falling back to python3 when none is found so a machine without Python still fails loudly instead of skipping. hermes-hooks goes from 0/14 to 14/14 on Windows 11; the other users of the helper are unchanged. Product code is untouched: Hermes runs these hooks itself and the installer writes no interpreter.
This commit is contained in:
2 files changed
+36
-5
No files matched your search
@@ -16,10 +16,15 @@ const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { resolvePython } = require('./lib/helpers/windows-test-env');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const GUARD = path.join(repoRoot, 'hooks', 'hermes', 'config-protection.py');
|
||||
const LOGGER = path.join(repoRoot, 'hooks', 'hermes', 'check-console-log.py');
|
||||
// A working Python 3, not the Windows Store `python3` stub; with none found the
|
||||
// spawns use python3 as before and fail loudly rather than skip.
|
||||
const PYTHON = resolvePython() || { command: 'python3', prefix: [] };
|
||||
const runPython = (script, options) => spawnSync(PYTHON.command, [...PYTHON.prefix, script], options);
|
||||
|
||||
function test(name, fn) {
|
||||
try {
|
||||
@@ -35,7 +40,7 @@ function test(name, fn) {
|
||||
|
||||
/** Run a hook with a payload; return {rc, stdout, stderr} and assert clean spawn. */
|
||||
function runHook(script, payload) {
|
||||
const res = spawnSync('python3', [script], {
|
||||
const res = runPython(script, {
|
||||
input: JSON.stringify(payload),
|
||||
encoding: 'utf8',
|
||||
timeout: 15000,
|
||||
@@ -181,7 +186,7 @@ run('allows non-editing tools (read/search)', () => {
|
||||
// ─── config-protection: fail-open + diagnostics ────────────────────────────
|
||||
|
||||
run('malformed payload: allows AND reports on stderr', () => {
|
||||
const res = spawnSync('python3', [GUARD], { input: 'not-json{{', encoding: 'utf8', timeout: 15000 });
|
||||
const res = runPython(GUARD, { input: 'not-json{{', encoding: 'utf8', timeout: 15000 });
|
||||
assert.strictEqual(res.status, 0, 'must exit 0 (fail-open)');
|
||||
assert.strictEqual(res.stdout.trim(), '', 'no decision emitted');
|
||||
assert.ok(res.stderr.includes('unparseable payload'), 'diagnostic required');
|
||||
@@ -250,13 +255,13 @@ run('hermes-hooks module installs via the real installer and the landed guard bl
|
||||
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-guard-'));
|
||||
const target = path.join(t, 'ESLINT.CONFIG.MJS');
|
||||
fs.writeFileSync(target, 'export default {}\n');
|
||||
const g = spawnSync('python3', [landed], {
|
||||
const g = runPython(landed, {
|
||||
input: JSON.stringify({ hook_event_name: 'pre_tool_call', tool_name: 'patch', tool_input: { path: target, old_string: 'a', new_string: 'b' } }),
|
||||
encoding: 'utf8', timeout: 15000,
|
||||
});
|
||||
assert.ok((g.stdout || '').includes('"decision": "block"'), 'landed guard must block');
|
||||
fs.unlinkSync(target);
|
||||
const bootstrap = spawnSync('python3', [landed], {
|
||||
const bootstrap = runPython(landed, {
|
||||
input: JSON.stringify({ hook_event_name: 'pre_tool_call', tool_name: 'patch', tool_input: { path: target } }),
|
||||
encoding: 'utf8', timeout: 15000,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
// On Windows, spawning a bare `claude` resolves only .com/.exe on PATH, never
|
||||
// .cmd. A natively installed claude.exe therefore wins over a test's fake
|
||||
@@ -35,4 +36,29 @@ function toRelativeTarPath(cwd, archivePath) {
|
||||
return path.relative(cwd, archivePath).split(path.sep).join('/');
|
||||
}
|
||||
|
||||
module.exports = { withoutShadowingClaude, toRelativeTarPath };
|
||||
// On Windows, `python3` is usually the Microsoft Store alias stub, which exits
|
||||
// 9009 instead of running Python; python.org installs provide `python` and the
|
||||
// `py` launcher instead.
|
||||
const PYTHON_CANDIDATES = Object.freeze({
|
||||
win32: [['python'], ['py', '-3'], ['python3']],
|
||||
default: [['python3'], ['python']]
|
||||
});
|
||||
const PYTHON3_CHECK = 'import sys; sys.exit(0 if sys.version_info[0] == 3 else 1)';
|
||||
|
||||
function runsPython3(command, args) {
|
||||
return spawnSync(command, args, { encoding: 'utf8', timeout: 15000 }).status === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first candidate that actually runs Python 3, as `{ command, prefix }`
|
||||
* (spawn `command` with `[...prefix, script]`), or null when none does.
|
||||
*/
|
||||
function resolvePython({ platform = process.platform, probe = runsPython3 } = {}) {
|
||||
const candidates = platform === 'win32' ? PYTHON_CANDIDATES.win32 : PYTHON_CANDIDATES.default;
|
||||
for (const [command, ...prefix] of candidates) {
|
||||
if (probe(command, [...prefix, '-c', PYTHON3_CHECK])) return { command, prefix };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { withoutShadowingClaude, toRelativeTarPath, resolvePython };
|
||||
Reference in new issue
Block a user