mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-16 14:32:34 +00:00
85 lines
3.9 KiB
TypeScript
85 lines
3.9 KiB
TypeScript
// Regression guards for Windows `hermes` resolution in main.ts.
|
|
//
|
|
// main.ts has no module.exports, so these follow the repo's source-assertion
|
|
// test pattern (see windows-child-process.test.ts). 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.
|
|
|
|
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
function readMain() {
|
|
return fs.readFileSync(path.join(__dirname, 'main.ts'), '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.ts')
|
|
// 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'
|
|
)
|
|
})
|