mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-01 12:02:05 +00:00
fix(desktop/windows): resolve real hermes over extensionless shim + prefer --update on recovery
Two Windows-only desktop boot bugs that caused spurious reinstall/repair loops: 1. findOnPath() searched the empty extension BEFORE PATHEXT, so an extensionless Git-Bash `hermes` shim shadowed the real hermes.cmd/.exe. The shim then failed the shell:false --version probe and the resolver fell through to bootstrap/repair even though a working CLI was on PATH. Fix: try PATHEXT extensions first, keep the empty entry LAST so callers that already include the extension (py.exe, pwsh.exe) still resolve. 2. handOffWindowsBootstrapRecovery() chose the destructive --repair over the gentle --update by checking only venv\Scripts\hermes.exe -- the setuptools console-script shim, written at the END of venv setup and absent in interrupted/quarantined states. Fix: take --update when ANY real-install signal is present (venv python, the shim, or .hermes-bootstrap-complete). Adds windows-hermes-resolution.test.cjs (source-assertion pattern, wired into test:desktop:platforms) guarding both regressions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0229246ab8
commit
ba37c910e0
3 changed files with 87 additions and 3 deletions
|
|
@ -1285,8 +1285,14 @@ function findOnPath(command) {
|
|||
const pathEntries = String(process.env.PATH || '')
|
||||
.split(path.delimiter)
|
||||
.filter(Boolean)
|
||||
// On Windows, try PATHEXT extensions BEFORE the bare (empty-extension) name.
|
||||
// A real command must resolve via its .exe/.cmd (Windows command-resolution
|
||||
// semantics consult PATHEXT); an extensionless file — e.g. a Git-Bash
|
||||
// shell-script shim named `hermes` — must not shadow `hermes.cmd`/`hermes.exe`.
|
||||
// The empty entry is kept LAST so callers that already include the extension
|
||||
// (py.exe, pwsh.exe, powershell.exe) still resolve.
|
||||
const extensions = IS_WINDOWS
|
||||
? ['', ...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]
|
||||
? [...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean), '']
|
||||
: ['']
|
||||
|
||||
for (const entry of pathEntries) {
|
||||
|
|
@ -2243,7 +2249,18 @@ async function handOffWindowsBootstrapRecovery(reason) {
|
|||
: configuredBranch || DEFAULT_UPDATE_BRANCH
|
||||
const venvBin = path.join(updateRoot, 'venv', IS_WINDOWS ? 'Scripts' : 'bin')
|
||||
const venvHermes = path.join(venvBin, IS_WINDOWS ? 'hermes.exe' : 'hermes')
|
||||
const updaterArgs = fileExists(venvHermes) ? ['--update', '--branch', branch] : ['--repair', '--branch', branch]
|
||||
const venvPython = path.join(venvBin, IS_WINDOWS ? 'python.exe' : 'python')
|
||||
// Choose the gentle in-place --update when ANY real-install signal is present,
|
||||
// not just the `hermes.exe` console-script shim. That shim is generated at the
|
||||
// END of venv setup and is absent in exactly the interrupted/quarantined states
|
||||
// this recovery exists to heal — gating on it alone forced the destructive
|
||||
// --repair (full venv recreate) and drove reinstall loops. The venv interpreter
|
||||
// and the bootstrap-complete marker are present earlier and are better signals.
|
||||
const haveRealInstall =
|
||||
fileExists(venvPython) ||
|
||||
fileExists(venvHermes) ||
|
||||
fileExists(path.join(updateRoot, '.hermes-bootstrap-complete'))
|
||||
const updaterArgs = haveRealInstall ? ['--update', '--branch', branch] : ['--repair', '--branch', branch]
|
||||
|
||||
await releaseBackendLockForUpdate(updateRoot)
|
||||
|
||||
|
|
|
|||
67
apps/desktop/electron/windows-hermes-resolution.test.cjs
Normal file
67
apps/desktop/electron/windows-hermes-resolution.test.cjs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
'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.
|
||||
|
||||
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'
|
||||
)
|
||||
})
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
"test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
|
||||
"test:desktop:existing": "node scripts/test-desktop.mjs existing",
|
||||
"test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/backend-ready.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/git-worktree-ops.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-count.test.cjs electron/update-rebuild.test.cjs electron/update-marker.test.cjs electron/update-relaunch.test.cjs electron/windows-user-env.test.cjs electron/wsl-clipboard-image.test.cjs electron/titlebar-overlay-width.test.cjs electron/window-state.test.cjs",
|
||||
"test:desktop:platforms": "node --test electron/bootstrap-platform.test.cjs electron/hardening.test.cjs electron/backend-env.test.cjs electron/backend-probes.test.cjs electron/backend-ready.test.cjs electron/bootstrap-runner.test.cjs electron/connection-config.test.cjs electron/dashboard-token.test.cjs electron/gateway-ws-probe.test.cjs electron/oauth-net-request.test.cjs electron/desktop-uninstall.test.cjs electron/session-windows.test.cjs electron/link-title-window.test.cjs electron/workspace-cwd.test.cjs electron/fs-read-dir.test.cjs electron/git-root.test.cjs electron/git-worktree-ops.test.cjs electron/windows-child-process.test.cjs electron/update-remote.test.cjs electron/update-count.test.cjs electron/update-rebuild.test.cjs electron/update-marker.test.cjs electron/update-relaunch.test.cjs electron/windows-user-env.test.cjs electron/wsl-clipboard-image.test.cjs electron/titlebar-overlay-width.test.cjs electron/window-state.test.cjs electron/windows-hermes-resolution.test.cjs",
|
||||
"typecheck": "tsc -p . --noEmit",
|
||||
"lint": "eslint src/ electron/",
|
||||
"lint:fix": "eslint src/ electron/ --fix",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue