Files
hermes-desktop/tests/server/hermes-process.test.ts
T
toor 01d9e301fc
Build / build (push) Canceled after 0s
Playwright / browser (push) Canceled after 0s
initialize Electron desktop app and remove unused files
2026-10-25 01:09:01 +08:00

158 lines
6.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
const execFileCalls = vi.hoisted(() => [] as Array<{ command: string; args: string[]; options: any }>)
const execResult = vi.hoisted(() => ({ error: null as any, delayMs: 0 }))
const spawnCalls = vi.hoisted(() => [] as Array<{ command: string; args: string[]; options: any }>)
vi.mock('child_process', () => ({
execFile: vi.fn((command: string, args: string[], options: any, callback: (error: Error | null, stdout: string, stderr: string) => void) => {
execFileCalls.push({ command, args, options })
if (execResult.delayMs) setTimeout(() => callback(execResult.error, 'ok\n', ''), execResult.delayMs)
else callback(execResult.error, 'ok\n', '')
}),
spawn: vi.fn((command: string, args: string[], options: any) => {
spawnCalls.push({ command, args, options })
return {} as any
}),
}))
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform })
}
afterEach(() => {
execResult.error = null
execResult.delayMs = 0
vi.useRealTimers()
execFileCalls.length = 0
spawnCalls.length = 0
delete process.env.HERMES_AGENT_BRIDGE_PYTHON
delete process.env.HERMES_AGENT_CLI_PYTHON
if (originalPlatform) Object.defineProperty(process, 'platform', originalPlatform)
vi.resetModules()
})
describe('Hermes process invocation', () => {
it('bypasses the uv hermes.exe trampoline on Windows packaged installs', async () => {
setPlatform('win32')
process.env.HERMES_AGENT_CLI_PYTHON = 'C:\\Users\\me\\AppData\\Local\\Programs\\Hermes Studio\\resources\\python\\python.exe'
const { execHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
const result = await execHermesWithBin(
'C:\\Users\\me\\AppData\\Local\\Programs\\Hermes Studio\\resources\\python\\Scripts\\hermes.exe',
['kanban', '--board', 'default', 'create', 'demo', '--json'],
{ windowsHide: true },
)
expect(result.stdout).toBe('ok\n')
expect(execFileCalls[0]).toMatchObject({
command: process.env.HERMES_AGENT_CLI_PYTHON,
args: ['-m', 'hermes_cli.main', 'kanban', '--board', 'default', 'create', 'demo', '--json'],
options: expect.objectContaining({ windowsHide: true }),
})
})
it('discovers sibling python.exe for a Windows hermes.exe launcher', async () => {
setPlatform('win32')
const root = mkdtempSync(join(tmpdir(), 'hermes-process-'))
try {
const scripts = join(root, 'Scripts')
mkdirSync(scripts)
writeFileSync(join(root, 'python.exe'), '')
writeFileSync(join(scripts, 'hermes.exe'), '')
const { execHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
await execHermesWithBin(join(scripts, 'hermes.exe'), ['--version'])
expect(execFileCalls[0]).toMatchObject({
command: join(root, 'python.exe'),
args: ['-m', 'hermes_cli.main', '--version'],
options: expect.objectContaining({ windowsHide: true }),
})
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('discovers sibling python.exe for a Windows hermes.cmd launcher', async () => {
setPlatform('win32')
const root = mkdtempSync(join(tmpdir(), 'hermes-process-'))
try {
const scripts = join(root, 'Scripts')
mkdirSync(scripts)
writeFileSync(join(root, 'python.exe'), '')
writeFileSync(join(scripts, 'hermes.cmd'), '')
const { execHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
await execHermesWithBin(join(scripts, 'hermes.cmd'), ['--version'])
expect(execFileCalls[0]).toMatchObject({
command: join(root, 'python.exe'),
args: ['-m', 'hermes_cli.main', '--version'],
options: expect.objectContaining({ windowsHide: true }),
})
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it('keeps normal Hermes command execution unchanged on non-Windows platforms', async () => {
setPlatform('darwin')
const { execHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
await execHermesWithBin('/opt/hermes/bin/hermes', ['--version'], { windowsHide: true })
expect(execFileCalls[0]).toMatchObject({
command: '/opt/hermes/bin/hermes',
args: ['--version'],
})
})
it('defaults spawned Windows Hermes processes to hidden windows', async () => {
setPlatform('win32')
process.env.HERMES_AGENT_CLI_PYTHON = 'C:\\Hermes Studio\\resources\\python\\python.exe'
const { spawnHermesWithBin } = await import('../../packages/server/src/services/hermes/hermes-process')
spawnHermesWithBin('C:\\Hermes Studio\\resources\\python\\Scripts\\hermes.exe', ['gateway', 'run'])
expect(spawnCalls[0]).toMatchObject({
command: process.env.HERMES_AGENT_CLI_PYTHON,
args: ['-m', 'hermes_cli.main', 'gateway', 'run'],
options: expect.objectContaining({ windowsHide: true }),
})
})
})
describe('Hermes installation probe', () => {
it('uses local help instead of the network-dependent version check and accepts slow startup', async () => {
vi.useFakeTimers()
execResult.delayMs = 7800
const { probeHermesCli } = await import('../../packages/server/src/services/hermes/hermes-process')
const pending = probeHermesCli('/opt/hermes/bin/hermes')
await vi.advanceTimersByTimeAsync(7800)
await expect(pending).resolves.toEqual({ available: true, reason: 'ready' })
expect(execFileCalls[0]).toMatchObject({ args: ['--help'], options: { timeout: 15000 } })
})
it.each([
[{ code: 'ENOENT' }, 'missing'],
[{ code: 'ENOTDIR' }, 'missing'],
[{ code: 127 }, 'missing'],
[{ code: 'EACCES' }, 'permission_denied'],
[{ code: 'EPERM' }, 'permission_denied'],
[{ code: 126 }, 'permission_denied'],
[{ killed: true, signal: 'SIGTERM' }, 'timeout'],
[{ code: 'ETIMEDOUT' }, 'timeout'],
[{ code: 1 }, 'failed'],
])('distinguishes %j from a missing installation', async (error, reason) => {
execResult.error = error
const { probeHermesCli } = await import('../../packages/server/src/services/hermes/hermes-process')
await expect(probeHermesCli('hermes')).resolves.toEqual({ available: false, reason })
})
})