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.
92 lines
3.8 KiB
JavaScript
92 lines
3.8 KiB
JavaScript
/**
|
|
* Tests for electron/backend-probes.cjs.
|
|
*
|
|
* Run with: node --test electron/backend-probes.test.cjs
|
|
* (Wired into npm test:desktop:platforms in package.json.)
|
|
*/
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const os = require('node:os')
|
|
const path = require('node:path')
|
|
|
|
const { canImportHermesCli, hermesRuntimeImportProbe, verifyHermesCli } = require('./backend-probes.cjs')
|
|
|
|
// Resolve the host's own Node binary -- guaranteed to be on disk and
|
|
// runnable. We use it as both a stand-in for "a python that doesn't
|
|
// have hermes_cli" (since `node -c "import hermes_cli"` will exit
|
|
// non-zero) and as a way to script verifyHermesCli's success path
|
|
// (a tiny script we write to disk that exits 0 on --version).
|
|
const NODE_BIN = process.execPath
|
|
|
|
test('canImportHermesCli returns false when path is falsy', () => {
|
|
assert.equal(canImportHermesCli(''), false)
|
|
assert.equal(canImportHermesCli(null), false)
|
|
assert.equal(canImportHermesCli(undefined), false)
|
|
})
|
|
|
|
test('canImportHermesCli returns false when interpreter cannot run -c', () => {
|
|
// node IS an interpreter, but `node -c "import hermes_cli"` is a
|
|
// SyntaxError -- different exit reason from a real Python's
|
|
// ModuleNotFoundError, but the predicate is "exit 0 or not" and
|
|
// both land on "not", which is exactly what we want for the
|
|
// resolver fall-through.
|
|
assert.equal(canImportHermesCli(NODE_BIN), false)
|
|
})
|
|
|
|
test('canImportHermesCli returns false when binary does not exist', () => {
|
|
const ghost = path.join(os.tmpdir(), 'hermes-probes-ghost-' + Date.now() + '.exe')
|
|
assert.equal(canImportHermesCli(ghost), false)
|
|
})
|
|
|
|
test('hermes runtime import probe checks config dependencies', () => {
|
|
const probe = hermesRuntimeImportProbe()
|
|
assert.match(probe, /\bimport yaml\b/)
|
|
// dotenv is the first third-party import on the CLI boot path
|
|
// (hermes_cli/env_loader.py); a mid-update venv missing python-dotenv
|
|
// passed the old probe and produced an unrecoverable boot loop.
|
|
assert.match(probe, /\bimport dotenv\b/)
|
|
assert.match(probe, /\bimport hermes_cli\.config\b/)
|
|
})
|
|
|
|
test('verifyHermesCli returns false when command is falsy', () => {
|
|
assert.equal(verifyHermesCli(''), false)
|
|
assert.equal(verifyHermesCli(null), false)
|
|
assert.equal(verifyHermesCli(undefined), false)
|
|
})
|
|
|
|
test('verifyHermesCli returns false when binary does not exist', () => {
|
|
const ghost = path.join(os.tmpdir(), 'hermes-probes-ghost-' + Date.now() + '.exe')
|
|
assert.equal(verifyHermesCli(ghost), false)
|
|
})
|
|
|
|
test('verifyHermesCli returns true when --version exits 0', () => {
|
|
// Write a tiny script that exits 0 regardless of args, then invoke
|
|
// it through node. This stands in for a working hermes binary --
|
|
// verifyHermesCli only cares about the exit code.
|
|
const scriptPath = path.join(os.tmpdir(), `hermes-probes-ok-${Date.now()}-${process.pid}.cjs`)
|
|
fs.writeFileSync(scriptPath, 'process.exit(0)\n')
|
|
try {
|
|
// Use node as the launcher and our script as the "command". Pass
|
|
// shell:false (default) -- node is a real binary, no shim.
|
|
// execFileSync passes ['--version'] as args, which node ignores
|
|
// gracefully (well, it prints its version and exits 0, which is
|
|
// perfect -- exit code 0 is the only signal we read).
|
|
assert.equal(verifyHermesCli(NODE_BIN), true)
|
|
} finally {
|
|
try {
|
|
fs.unlinkSync(scriptPath)
|
|
} catch {
|
|
void 0
|
|
}
|
|
}
|
|
})
|
|
|
|
test('verifyHermesCli swallows timeouts (does not throw)', () => {
|
|
// We can't easily provoke a real 5s hang in CI without slowing the
|
|
// suite, but we CAN confirm that an invocation that DOES throw
|
|
// (because the binary is missing) returns false rather than
|
|
// propagating. Same code path the timeout case takes.
|
|
assert.equal(verifyHermesCli('/definitely/not/a/real/binary/anywhere'), false)
|
|
})
|