mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-11 13:41:53 +00:00
A Windows venv broken mid-update (e.g. python-dotenv missing after a partial pip install) still has python.exe + Scripts\hermes.exe on disk. unwrapWindowsVenvHermesCommand() returned that interpreter with no probe -- bypassing even the caller's --version smoke test -- so every recovery action (Retry, Repair install, Use local gateway) re-resolved the same dead backend: ModuleNotFoundError: No module named 'dotenv', same overlay, forever. - unwrapWindowsVenvHermesCommand now runs canImportHermesCli() on the venv python (checkout on PYTHONPATH, mirroring isActiveRuntimeUsable) and returns null on failure so the resolver falls through to the bootstrap installer, which actually repairs the venv. - hermesRuntimeImportProbe() adds 'import dotenv' -- the first third-party import on the CLI boot path (hermes_cli/env_loader.py) -- so a venv missing python-dotenv fails the probe everywhere it's used (isActiveRuntimeUsable, system-python rung, and the new unwrap gate). - Regression tests: probe content + source assertion that the unwrap path probes and falls through.
84 lines
3.8 KiB
JavaScript
84 lines
3.8 KiB
JavaScript
'use strict'
|
|
|
|
// Regression guards for Windows `hermes` resolution in main.cjs.
|
|
//
|
|
// main.cjs has no module.exports, so these follow the repo's source-assertion
|
|
// test pattern (see windows-child-process.test.cjs). They pin the two Windows
|
|
// resolution bugs that caused desktop reinstall loops:
|
|
// 1. findOnPath() tried the empty extension FIRST, so an extensionless
|
|
// Git-Bash `hermes` shim shadowed the real hermes.cmd/hermes.exe; the
|
|
// shim then failed the --version probe and the desktop fell through to a
|
|
// spurious bootstrap/repair.
|
|
// 2. handOffWindowsBootstrapRecovery() chose --update vs the destructive
|
|
// --repair by checking ONLY venv\Scripts\hermes.exe (the console-script
|
|
// shim, written at the END of venv setup and absent in interrupted
|
|
// states), so it escalated to a full venv recreate even on healthy
|
|
// installs.
|
|
// 3. unwrapWindowsVenvHermesCommand() returned the venv python with NO
|
|
// runtime probe (bypassing the caller's --version check too), so a venv
|
|
// broken mid-update (e.g. missing python-dotenv) was re-selected forever:
|
|
// Retry / "Repair install" resolved the same dead interpreter instead of
|
|
// falling through to the bootstrap installer.
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
function readMain() {
|
|
return fs.readFileSync(path.join(__dirname, 'main.cjs'), 'utf8').replace(/\r\n/g, '\n')
|
|
}
|
|
|
|
test('findOnPath tries PATHEXT extensions before the bare (empty) name on Windows', () => {
|
|
const source = readMain()
|
|
// Fixed order: PATHEXT first, empty string LAST.
|
|
assert.match(
|
|
source,
|
|
/\(process\.env\.PATHEXT \|\| '\.COM;\.EXE;\.BAT;\.CMD'\)\.split\(';'\)\.filter\(Boolean\), ''\]/,
|
|
'extensions array must end with the empty string, not start with it'
|
|
)
|
|
// The buggy empty-first order must not return.
|
|
assert.doesNotMatch(
|
|
source,
|
|
/\['', \.\.\.\(process\.env\.PATHEXT/,
|
|
'empty-extension-first order regressed: an extensionless shim can shadow hermes.cmd/.exe'
|
|
)
|
|
})
|
|
|
|
test('Windows bootstrap recovery chooses --update when any real-install signal is present', () => {
|
|
const source = readMain()
|
|
assert.match(source, /const haveRealInstall =/, 'recovery must compute haveRealInstall')
|
|
assert.match(source, /fileExists\(venvPython\)/, 'recovery must accept the venv interpreter as a real-install signal')
|
|
assert.match(
|
|
source,
|
|
/\.hermes-bootstrap-complete/,
|
|
'recovery must accept the bootstrap-complete marker as a real-install signal'
|
|
)
|
|
assert.match(source, /updaterArgs = haveRealInstall \? \['--update'/, 'updaterArgs must gate on haveRealInstall')
|
|
// The old too-narrow check (only venv\Scripts\hermes.exe) must not return.
|
|
assert.doesNotMatch(
|
|
source,
|
|
/updaterArgs = fileExists\(venvHermes\) \?/,
|
|
'recovery regressed to gating only on the hermes.exe shim, which forces destructive --repair'
|
|
)
|
|
})
|
|
|
|
test('unwrapWindowsVenvHermesCommand smoke-tests the venv python before trusting it', () => {
|
|
const source = readMain()
|
|
const fnStart = source.indexOf('function unwrapWindowsVenvHermesCommand(')
|
|
assert.notEqual(fnStart, -1, 'unwrapWindowsVenvHermesCommand must exist in main.cjs')
|
|
// Slice out just the function body (up to the next top-level function decl)
|
|
const fnEnd = source.indexOf('\nfunction ', fnStart + 1)
|
|
const body = source.slice(fnStart, fnEnd === -1 ? undefined : fnEnd)
|
|
assert.match(
|
|
body,
|
|
/canImportHermesCli\(python/,
|
|
'unwrap must probe the venv interpreter; returning it unprobed re-selects a broken venv ' +
|
|
'forever (Retry/Repair loop on a mid-update venv missing e.g. python-dotenv)'
|
|
)
|
|
assert.match(
|
|
body,
|
|
/return null\s*\n\s*\}\s*\n\s*return \{/,
|
|
'a failed probe must fall through (return null) so the resolver reaches the bootstrap rung'
|
|
)
|
|
})
|