Files
ECC/tests/lib/helpers/windows-test-env.js
T
montjeffrey 72f2105cf6 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.
2026-10-11 14:21:25 -04:00

65 lines
2.5 KiB
JavaScript

/**
* Keep tests independent of tools installed on a Windows developer machine.
*
* Linux CI never sees these leaks, so they surface only as local failures.
*/
'use strict';
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
// claude.cmd even when the fake's directory comes first on PATH.
const SHADOWING_CLAUDE_BINARIES = ['claude.com', 'claude.exe'];
/**
* Drop PATH entries holding a real Claude binary that would shadow a fake
* `claude.cmd` launcher. A no-op off Windows, where PATH order is honoured.
*/
function withoutShadowingClaude(pathValue, platform = process.platform) {
if (platform !== 'win32') return pathValue || '';
return String(pathValue || '')
.split(path.win32.delimiter)
.filter(dir => dir && !SHADOWING_CLAUDE_BINARIES.some(name => fs.existsSync(path.join(dir, name))))
.join(path.win32.delimiter);
}
/**
* Name an archive relative to the directory tar runs in, with forward slashes.
* GNU tar, which Git for Windows puts first on PATH, reads an archive argument
* containing a colon (C:\...) as a host:path remote spec; bsdtar does not.
*/
function toRelativeTarPath(cwd, archivePath) {
return path.relative(cwd, archivePath).split(path.sep).join('/');
}
// 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 };