Files
hermes-ui/tests/server/hermes-plugins-env.test.ts
T
toor a1fff482bf make the test suite and openapi generator platform-independent
Windows checkouts turned LF into CRLF (core.autocrlf=true), and several
tests plus the openapi generator hardcoded LF or POSIX path separators.
The result was a suite that was green on Linux CI but red on Windows, and
an openapi.json whose /api/hermes/* routes silently disappeared when
generated on Windows.

Pin the repository to LF via .gitattributes, strip shebangs before
Vite's SSR transform, and key route files by their posix path. Add a
manually triggered macOS verification workflow so the same checks run
on a self-hosted macOS runner.
2026-10-20 11:17:34 +08:00

103 lines
4.4 KiB
TypeScript

import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
// 这个用例的假 python 是一段 #!/bin/sh 脚本,验证的是「从 hermes 的 shebang
// 解析出 venv python」这条 POSIX 链路。Windows 没有 shebang 执行语义,
// venv 布局也是 .venv\Scripts\python.exe,这条链路在那边根本不存在,
// 所以在 Windows 上整组跳过,而不是伪造一个假的 Windows 结果。
describe.skipIf(process.platform === 'win32')('Hermes plugin discovery environment', () => {
const originalEnv = { ...process.env }
let tempDir = ''
beforeEach(() => {
vi.resetModules()
tempDir = mkdtempSync(join(tmpdir(), 'hermes-plugins-env-'))
process.env = { ...originalEnv }
})
afterEach(() => {
process.env = { ...originalEnv }
if (tempDir) rmSync(tempDir, { recursive: true, force: true })
})
it('uses the same venv python and agent root resolved from the hermes binary as the bridge', async () => {
const agentRoot = join(tempDir, 'agent')
const venvBin = join(agentRoot, '.venv', 'bin')
const hermesCliDir = join(agentRoot, 'hermes_cli')
const captureFile = join(tempDir, 'capture.txt')
const fakePython = join(venvBin, 'python')
const fakeHermes = join(venvBin, 'hermes')
mkdirSync(venvBin, { recursive: true })
mkdirSync(hermesCliDir, { recursive: true })
writeFileSync(join(agentRoot, 'run_agent.py'), '')
writeFileSync(join(hermesCliDir, 'plugins.py'), '')
writeFileSync(fakePython, [
'#!/bin/sh',
'printf "%s\\n%s\\n%s\\n%s\\n" "$0" "$1" "$2" "$HERMES_AGENT_ROOT_RESOLVED" > "$CAPTURE_FILE"',
'printf "%s\\n" \'{"plugins":[],"warnings":[],"metadata":{"hermesAgentRoot":"","pythonExecutable":"","cwd":"","projectPluginsEnabled":false}}\'',
'',
].join('\n'))
chmodSync(fakePython, 0o755)
writeFileSync(fakeHermes, `#!${fakePython}\n`)
chmodSync(fakeHermes, 0o755)
delete process.env.HERMES_AGENT_ROOT
delete process.env.HERMES_AGENT_BRIDGE_PYTHON
delete process.env.HERMES_AGENT_BRIDGE_UV
delete process.env.HERMES_PYTHON
process.env.HERMES_HOME = join(tempDir, 'home')
process.env.HERMES_BIN = fakeHermes
process.env.CAPTURE_FILE = captureFile
const { listHermesPlugins } = await import('../../packages/server/src/services/hermes/plugins')
await expect(listHermesPlugins()).resolves.toMatchObject({ plugins: [] })
const [command, firstArg, secondArg, resolvedRoot] = readFileSync(captureFile, 'utf8').trim().split('\n')
expect(command).toBe(fakePython)
expect(firstArg).toBe('-I')
expect(secondArg).toBe('-c')
expect(resolvedRoot).toBe(agentRoot)
})
it('uses package Python without isolated mode when no source root is resolved', async () => {
const binDir = join(tempDir, 'bin')
const captureFile = join(tempDir, 'capture-package.txt')
const fakePython = join(binDir, 'python')
const fakeHermes = join(binDir, 'hermes')
mkdirSync(binDir, { recursive: true })
writeFileSync(fakePython, [
'#!/bin/sh',
'printf "%s\\n%s\\n%s\\n%s\\n" "$0" "$1" "${PYTHONPATH-unset}" "${PYTHONHOME-unset}" > "$CAPTURE_FILE"',
'printf "%s\\n" \'{"plugins":[],"warnings":[],"metadata":{"hermesAgentRoot":"","pythonExecutable":"","cwd":"","projectPluginsEnabled":false}}\'',
'',
].join('\n'))
chmodSync(fakePython, 0o755)
writeFileSync(fakeHermes, `#!${fakePython}\n`)
chmodSync(fakeHermes, 0o755)
delete process.env.HERMES_AGENT_ROOT
delete process.env.HERMES_AGENT_BRIDGE_PYTHON
delete process.env.HERMES_AGENT_BRIDGE_UV
delete process.env.UV
delete process.env.HERMES_PYTHON
process.env.HERMES_HOME = join(tempDir, 'home')
process.env.HERMES_BIN = fakeHermes
process.env.CAPTURE_FILE = captureFile
process.env.PYTHONPATH = join(tempDir, 'shadow-path')
process.env.PYTHONHOME = join(tempDir, 'shadow-home')
const { listHermesPlugins } = await import('../../packages/server/src/services/hermes/plugins')
await expect(listHermesPlugins()).resolves.toMatchObject({ plugins: [] })
const [command, firstArg, pythonPath, pythonHome] = readFileSync(captureFile, 'utf8').trim().split('\n')
expect(command).toBe(fakePython)
expect(firstArg).toBe('-c')
expect(pythonPath).toBe('unset')
expect(pythonHome).toBe('unset')
})
})