mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-02 12:13:05 +00:00
Bring apps/desktop and ui-tui to a clean state for typecheck, eslint,
and prettier:
- Run prettier across both trees (printWidth/wrap drift; prettier is not
CI-enforced for these JS projects, so main had accumulated drift).
- Apply eslint --fix for padding-line-between-statements and perfectionist
import/export sorting.
- Manual fixes for non-auto-fixable rules:
- remove unused node:net import in electron/main.cjs (uses Electron net)
- replace inline `typeof import(...)` annotations with top-level
`import type * as EnvModule` in two ui-tui test files
- scoped eslint-disable no-control-regex on intentional sentinel/ANSI
regexes (mathUnicode.ts, text.ts)
- resolve react-hooks/exhaustive-deps per-case: correct swapped/missing
deps, collapse redundant session.* members, and justified disables on
settings mount-only data-load effects to preserve run-once behavior
No behavior changes; test pass/fail counts are unchanged from the main
baseline.
243 lines
9.4 KiB
JavaScript
243 lines
9.4 KiB
JavaScript
/**
|
|
* Tests for electron/desktop-uninstall.cjs.
|
|
*
|
|
* Run with: node --test electron/desktop-uninstall.test.cjs
|
|
* (Wired into npm test:desktop:platforms in package.json.)
|
|
*
|
|
* These are the pure helpers behind the desktop Chat GUI uninstaller: the
|
|
* mode → CLI-flag mapping, the running-app-bundle resolution per OS, and the
|
|
* cleanup-script builders (POSIX + Windows).
|
|
*/
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
|
|
const {
|
|
UNINSTALL_MODES,
|
|
buildPosixCleanupScript,
|
|
buildWindowsCleanupScript,
|
|
modeRemovesAgent,
|
|
modeRemovesUserData,
|
|
resolveRemovableAppPath,
|
|
shouldRemoveAppBundle,
|
|
uninstallArgsForMode
|
|
} = require('./desktop-uninstall.cjs')
|
|
|
|
// --- uninstallArgsForMode ---
|
|
|
|
test('uninstallArgsForMode maps each mode to the module-runner argv', () => {
|
|
assert.deepEqual(uninstallArgsForMode('gui'), ['-m', 'hermes_cli.uninstall', '--mode', 'gui'])
|
|
assert.deepEqual(uninstallArgsForMode('lite'), ['-m', 'hermes_cli.uninstall', '--mode', 'lite'])
|
|
assert.deepEqual(uninstallArgsForMode('full'), ['-m', 'hermes_cli.uninstall', '--mode', 'full'])
|
|
})
|
|
|
|
test('uninstallArgsForMode throws on an unknown mode (no silent full wipe)', () => {
|
|
assert.throws(() => uninstallArgsForMode('nuke'), /Unknown uninstall mode/)
|
|
assert.throws(() => uninstallArgsForMode(''), /Unknown uninstall mode/)
|
|
})
|
|
|
|
test('UNINSTALL_MODES lists exactly the three supported modes', () => {
|
|
assert.deepEqual([...UNINSTALL_MODES].sort(), ['full', 'gui', 'lite'])
|
|
})
|
|
|
|
// --- modeRemovesAgent / modeRemovesUserData ---
|
|
|
|
test('mode predicates classify what each mode removes', () => {
|
|
assert.equal(modeRemovesAgent('gui'), false)
|
|
assert.equal(modeRemovesAgent('lite'), true)
|
|
assert.equal(modeRemovesAgent('full'), true)
|
|
|
|
assert.equal(modeRemovesUserData('gui'), false)
|
|
assert.equal(modeRemovesUserData('lite'), false)
|
|
assert.equal(modeRemovesUserData('full'), true)
|
|
})
|
|
|
|
// --- resolveRemovableAppPath ---
|
|
|
|
test('resolveRemovableAppPath finds the .app bundle on macOS', () => {
|
|
assert.equal(
|
|
resolveRemovableAppPath('/Applications/Hermes.app/Contents/MacOS/Hermes', 'darwin'),
|
|
'/Applications/Hermes.app'
|
|
)
|
|
assert.equal(
|
|
resolveRemovableAppPath('/Users/x/Applications/Hermes.app/Contents/MacOS/Hermes', 'darwin'),
|
|
'/Users/x/Applications/Hermes.app'
|
|
)
|
|
})
|
|
|
|
test('resolveRemovableAppPath: dev-run .app resolves (safety is shouldRemoveAppBundle, not null)', () => {
|
|
// A dev run from node_modules' Electron DOES resolve to a .app — the real
|
|
// dev-run safety gate is shouldRemoveAppBundle(isPackaged=false,...), not a
|
|
// null return here. This test documents that contract.
|
|
assert.equal(
|
|
resolveRemovableAppPath('/repo/node_modules/electron/dist/Electron.app/Contents/MacOS/Electron', 'darwin'),
|
|
'/repo/node_modules/electron/dist/Electron.app'
|
|
)
|
|
assert.equal(shouldRemoveAppBundle(false, '/repo/node_modules/electron/dist/Electron.app'), false)
|
|
// A bare path with no .app ancestor → null.
|
|
assert.equal(resolveRemovableAppPath('/usr/bin/electron', 'darwin'), null)
|
|
})
|
|
|
|
test('resolveRemovableAppPath finds the install dir on Windows', () => {
|
|
assert.equal(
|
|
resolveRemovableAppPath('C:\\Users\\x\\AppData\\Local\\Programs\\Hermes\\Hermes.exe', 'win32'),
|
|
'C:\\Users\\x\\AppData\\Local\\Programs\\Hermes'
|
|
)
|
|
assert.equal(
|
|
resolveRemovableAppPath('C:\\Users\\x\\AppData\\Local\\hermes-desktop\\Hermes.exe', 'win32'),
|
|
'C:\\Users\\x\\AppData\\Local\\hermes-desktop'
|
|
)
|
|
})
|
|
|
|
test('resolveRemovableAppPath returns null for an unrecognized Windows dir', () => {
|
|
assert.equal(resolveRemovableAppPath('C:\\Temp\\foo\\Hermes.exe', 'win32'), null)
|
|
})
|
|
|
|
test('resolveRemovableAppPath uses APPIMAGE on Linux when set', () => {
|
|
assert.equal(
|
|
resolveRemovableAppPath('/tmp/.mount_HermesXXXX/hermes', 'linux', { APPIMAGE: '/home/x/Apps/Hermes.AppImage' }),
|
|
'/home/x/Apps/Hermes.AppImage'
|
|
)
|
|
})
|
|
|
|
test('resolveRemovableAppPath finds the unpacked dir on Linux', () => {
|
|
assert.equal(resolveRemovableAppPath('/opt/hermes/linux-unpacked/hermes', 'linux', {}), '/opt/hermes/linux-unpacked')
|
|
// A system-package install (/usr/bin) → null, left to apt/dnf.
|
|
assert.equal(resolveRemovableAppPath('/usr/bin/hermes', 'linux', {}), null)
|
|
})
|
|
|
|
test('resolveRemovableAppPath returns null for an empty exe path', () => {
|
|
assert.equal(resolveRemovableAppPath('', 'darwin'), null)
|
|
assert.equal(resolveRemovableAppPath(null, 'win32'), null)
|
|
})
|
|
|
|
// --- shouldRemoveAppBundle ---
|
|
|
|
test('shouldRemoveAppBundle requires packaged AND a resolved path', () => {
|
|
assert.equal(shouldRemoveAppBundle(true, '/Applications/Hermes.app'), true)
|
|
assert.equal(shouldRemoveAppBundle(false, '/Applications/Hermes.app'), false)
|
|
assert.equal(shouldRemoveAppBundle(true, null), false)
|
|
assert.equal(shouldRemoveAppBundle(false, null), false)
|
|
})
|
|
|
|
// --- buildPosixCleanupScript ---
|
|
|
|
test('buildPosixCleanupScript waits for the PID, runs the uninstall module, removes bundle', () => {
|
|
const script = buildPosixCleanupScript({
|
|
desktopPid: 4321,
|
|
pythonExe: '/home/x/.hermes/hermes-agent/venv/bin/python',
|
|
pythonPath: null,
|
|
agentRoot: '/home/x/.hermes/hermes-agent',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'gui'],
|
|
appPath: '/opt/hermes/linux-unpacked',
|
|
hermesHome: '/home/x/.hermes'
|
|
})
|
|
assert.match(script, /^#!\/bin\/bash/)
|
|
assert.match(script, /pid=4321/)
|
|
assert.match(script, /kill -0 "\$pid"/)
|
|
// bounded wait (~30s), not unbounded
|
|
assert.match(script, /seq 1 60/)
|
|
assert.match(script, /'-m' 'hermes_cli\.uninstall' '--mode' 'gui'/)
|
|
assert.match(script, /rm -rf '\/opt\/hermes\/linux-unpacked'/)
|
|
assert.match(script, /export HERMES_HOME='\/home\/x\/\.hermes'/)
|
|
})
|
|
|
|
test('buildPosixCleanupScript exports PYTHONPATH when pythonPath is set (lite/full)', () => {
|
|
const script = buildPosixCleanupScript({
|
|
desktopPid: 1,
|
|
pythonExe: '/usr/bin/python3',
|
|
pythonPath: '/home/x/.hermes/hermes-agent',
|
|
agentRoot: '/home/x/.hermes/hermes-agent',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'full'],
|
|
appPath: null,
|
|
hermesHome: '/home/x/.hermes'
|
|
})
|
|
// System python + source on PYTHONPATH so import hermes_cli works while the
|
|
// venv is torn down.
|
|
assert.match(script, /export PYTHONPATH='\/home\/x\/\.hermes\/hermes-agent'/)
|
|
assert.match(script, /'\/usr\/bin\/python3' '-m' 'hermes_cli\.uninstall' '--mode' 'full'/)
|
|
})
|
|
|
|
test('buildPosixCleanupScript omits PYTHONPATH when pythonPath is null (gui)', () => {
|
|
const script = buildPosixCleanupScript({
|
|
desktopPid: 1,
|
|
pythonExe: '/p/python',
|
|
pythonPath: null,
|
|
agentRoot: '/a',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'gui'],
|
|
appPath: null,
|
|
hermesHome: '/h'
|
|
})
|
|
assert.doesNotMatch(script, /export PYTHONPATH/)
|
|
})
|
|
|
|
test('buildPosixCleanupScript omits the bundle rm when appPath is null', () => {
|
|
const script = buildPosixCleanupScript({
|
|
desktopPid: 1,
|
|
pythonExe: '/p/python',
|
|
pythonPath: null,
|
|
agentRoot: '/a',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'lite'],
|
|
appPath: null,
|
|
hermesHome: '/h'
|
|
})
|
|
assert.doesNotMatch(script, /rm -rf '\//)
|
|
// Still runs the uninstall.
|
|
assert.match(script, /'-m' 'hermes_cli\.uninstall' '--mode' 'lite'/)
|
|
})
|
|
|
|
test('buildPosixCleanupScript single-quote-escapes paths with apostrophes', () => {
|
|
const script = buildPosixCleanupScript({
|
|
desktopPid: 1,
|
|
pythonExe: "/home/o'brien/python",
|
|
pythonPath: null,
|
|
agentRoot: '/a',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'gui'],
|
|
appPath: null,
|
|
hermesHome: '/h'
|
|
})
|
|
// The apostrophe is closed-escaped-reopened so the shell sees the literal.
|
|
assert.match(script, /'\/home\/o'\\''brien\/python'/)
|
|
})
|
|
|
|
// --- buildWindowsCleanupScript ---
|
|
|
|
test('buildWindowsCleanupScript waits (bounded) for PID, runs uninstall, rmdir bundle', () => {
|
|
const script = buildWindowsCleanupScript({
|
|
desktopPid: 9988,
|
|
pythonExe: 'C:\\Python313\\python.exe',
|
|
pythonPath: 'C:\\hermes',
|
|
agentRoot: 'C:\\hermes',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'full'],
|
|
appPath: 'C:\\Users\\x\\AppData\\Local\\Programs\\Hermes',
|
|
hermesHome: 'C:\\Users\\x\\AppData\\Local\\hermes'
|
|
})
|
|
assert.match(script, /@echo off/)
|
|
assert.match(script, /set "PID=9988"/)
|
|
// PYTHONPATH set so a system python can import hermes_cli from source.
|
|
assert.match(script, /set "PYTHONPATH=C:\\hermes;%PYTHONPATH%"/)
|
|
assert.match(script, /"C:\\Python313\\python.exe" "-m" "hermes_cli\.uninstall" "--mode" "full"/)
|
|
// Bounded wait-loop (no infinite loop), whole-token PID match (no substring).
|
|
assert.match(script, /if %waited% geq 60 goto waited_done/)
|
|
assert.match(script, /findstr \/r \/c:" %PID% "/)
|
|
assert.doesNotMatch(script, /find "%PID%"/) // the old substring-prone form is gone
|
|
// Removal is a retry loop (Windows releases dir handles lazily).
|
|
assert.match(script, /:rmloop/)
|
|
assert.match(script, /rmdir \/s \/q "C:\\Users\\x\\AppData\\Local\\Programs\\Hermes" >nul 2>&1/)
|
|
assert.match(script, /if %tries% geq 10 goto rmdone/)
|
|
assert.match(script, /del "%~f0"/)
|
|
})
|
|
|
|
test('buildWindowsCleanupScript omits PYTHONPATH + rmdir when not needed (gui, no bundle)', () => {
|
|
const script = buildWindowsCleanupScript({
|
|
desktopPid: 2,
|
|
pythonExe: 'C:\\h\\venv\\Scripts\\python.exe',
|
|
pythonPath: null,
|
|
agentRoot: 'C:\\h',
|
|
uninstallArgs: ['-m', 'hermes_cli.uninstall', '--mode', 'gui'],
|
|
appPath: null,
|
|
hermesHome: 'C:\\h'
|
|
})
|
|
assert.doesNotMatch(script, /rmdir/)
|
|
assert.doesNotMatch(script, /set "PYTHONPATH=/)
|
|
})
|