Files
hermes-ui/tests/server/health-controller.test.ts
T
toor 1e9ea89d86
Build / build (push) Canceled after 0s
NPM Lockfile Check / npm ci --ignore-scripts (push) Canceled after 0s
Playwright / e2e (push) Canceled after 0s
chore: bootstrap hermes-ui skeleton from 0.6.29 snapshot
Import the client, server, skills, tests, and build scripts as the starting
point for the rebuild, then trim the surfaces this project does not ship:
Electron desktop packaging, the marketing website, and the ESP32 firmware
tree. CI keeps the web build, e2e, lockfile, release, and Docker workflows.

Rename the package and CLI entry points to hermes-ui, drop the prepare hook so
npm ci no longer triggers a build, and drop the removed tsconfig and nodemon
references. Rewrite AGENTS.md, ARCHITECTURE.md, DEVELOPMENT.md, and the READMEs
so the documented package map and hard rules match this repository.

reference_code/ is ignored: it is a read-only snapshot, not an input to the build.
2026-10-16 10:34:35 +08:00

306 lines
10 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { readFileSync } from 'fs'
import { resolve } from 'path'
function readRootPackage() {
return JSON.parse(readFileSync(resolve(process.cwd(), 'package.json'), 'utf-8')) as {
name: string
version: string
}
}
type LoadHealthControllerOptions = {
injectedVersion?: string
isDocker?: boolean
bridgeReadiness?: any
bridgeReadinessError?: Error
managerError?: Error
runtimeStateError?: Error
}
const defaultBridgeReadiness = {
endpoint: 'tcp://127.0.0.1:8123',
endpointKind: 'tcp',
status: 'ready',
reachable: true,
ready: true,
running: true,
attached: false,
starting: false,
stopping: false,
restartScheduled: false,
restartAttempts: 0,
pid: 4321,
}
async function loadHealthController(options: LoadHealthControllerOptions = {}) {
vi.resetModules()
if (typeof options.injectedVersion === 'string') {
;(globalThis as any).__APP_VERSION__ = options.injectedVersion
} else {
delete (globalThis as any).__APP_VERSION__
}
vi.doMock('../../packages/server/src/services/hermes/hermes-cli', () => ({
getVersion: vi.fn().mockResolvedValue('Hermes Agent v0.11.0\n'),
}))
const checkReadiness = options.bridgeReadinessError
? vi.fn().mockRejectedValue(options.bridgeReadinessError)
: vi.fn().mockResolvedValue(options.bridgeReadiness || defaultBridgeReadiness)
const getRuntimeState = options.runtimeStateError
? vi.fn(() => { throw options.runtimeStateError })
: vi.fn(() => ({
endpoint: options.bridgeReadiness?.endpoint || 'ipc:///tmp/hermes-agent-bridge.sock',
}))
const getAgentBridgeManager = options.managerError
? vi.fn(() => { throw options.managerError })
: vi.fn(() => ({ checkReadiness, getRuntimeState }))
vi.doMock('../../packages/server/src/services/hermes/agent-bridge/manager', () => ({
getAgentBridgeManager,
}))
vi.doMock('../../packages/server/src/services/runtime-environment', () => ({
isDockerContainer: () => options.isDocker === true,
}))
const health = await import('../../packages/server/src/controllers/health')
return {
...health,
getAgentBridgeManager,
checkReadiness,
getRuntimeState,
}
}
async function loadHealthControllerWithoutInjectedVersion(options: Omit<LoadHealthControllerOptions, 'injectedVersion'> = {}) {
return loadHealthController(options)
}
async function loadHealthControllerWithInjectedVersion(version: string) {
return loadHealthController({ injectedVersion: version })
}
function createMockCtx() {
return {
body: null as any,
}
}
describe('health controller version metadata', () => {
afterEach(() => {
vi.restoreAllMocks()
vi.resetModules()
;(globalThis as any).__APP_VERSION__ = 'test'
})
it('reads the root package version in ts-node/dev mode instead of falling back to 0.0.0', async () => {
const pkg = readRootPackage()
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))
const { healthCheck } = await loadHealthControllerWithoutInjectedVersion()
const ctx = createMockCtx()
await healthCheck(ctx)
expect(ctx.body.webui_version).toBe(pkg.version)
expect(ctx.body.webui_version).not.toBe('0.0.0')
})
it('uses the injected build version when available', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))
const { healthCheck } = await loadHealthControllerWithInjectedVersion('9.9.9-test')
const ctx = createMockCtx()
await healthCheck(ctx)
expect(ctx.body.webui_version).toBe('9.9.9-test')
})
it('checks npm latest using the root package name', async () => {
vi.spyOn(console, 'log').mockImplementation(() => {})
const pkg = readRootPackage()
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ version: '99.99.99' }),
})
vi.stubGlobal('fetch', fetchMock)
const { checkLatestVersion, healthCheck } = await loadHealthControllerWithoutInjectedVersion()
await checkLatestVersion()
expect(fetchMock).toHaveBeenCalledWith(
`https://registry.npmjs.org/${pkg.name}/latest`,
expect.objectContaining({ signal: expect.any(AbortSignal) }),
)
const ctx = createMockCtx()
await healthCheck(ctx)
expect(ctx.body.webui_latest).toBe('99.99.99')
expect(ctx.body.webui_update_available).toBe(true)
})
it('does not report a registry version lower than the local build as an update', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ version: '0.6.17' }),
}))
const { checkLatestVersion, healthCheck } = await loadHealthControllerWithInjectedVersion('0.6.18')
await checkLatestVersion()
const ctx = createMockCtx()
await healthCheck(ctx)
expect(ctx.body.webui_latest).toBe('0.6.17')
expect(ctx.body.webui_update_available).toBe(false)
expect(logSpy).not.toHaveBeenCalledWith(expect.stringContaining('Update available'))
})
it('does not throw when latest-version lookup fails', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down')))
const { checkLatestVersion } = await loadHealthControllerWithoutInjectedVersion()
await expect(checkLatestVersion()).resolves.toBeUndefined()
})
it('reports Docker while retaining version checks for upgrade guidance', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({ version: '0.6.29' }),
})
vi.stubGlobal('fetch', fetchMock)
const { checkLatestVersion, healthCheck } = await loadHealthController({
injectedVersion: '0.6.28',
isDocker: true,
})
await checkLatestVersion()
const ctx = createMockCtx()
await healthCheck(ctx)
expect(fetchMock).toHaveBeenCalledOnce()
expect(ctx.body).toEqual(expect.objectContaining({
is_docker: true,
webui_latest: '0.6.29',
webui_update_available: true,
}))
})
it('includes sanitized agent bridge readiness fields without leaking the endpoint path', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))
const { healthCheck, getAgentBridgeManager, checkReadiness } = await loadHealthControllerWithoutInjectedVersion({
bridgeReadiness: {
endpoint: 'ipc:///tmp/hermes-agent-bridge.sock',
endpointKind: 'ipc',
status: 'unreachable',
reachable: false,
ready: false,
running: false,
attached: false,
starting: false,
stopping: false,
restartScheduled: true,
restartAttempts: 3,
pid: 9876,
error: 'connect ENOENT /tmp/hermes-agent-bridge.sock',
},
})
const ctx = createMockCtx()
await healthCheck(ctx)
expect(getAgentBridgeManager).toHaveBeenCalledTimes(1)
expect(checkReadiness).toHaveBeenCalledWith({ timeoutMs: 75, connectRetryMs: 0 })
expect(ctx.body.agent_bridge).toEqual({
status: 'unreachable',
reachable: false,
ready: false,
running: false,
attached: false,
starting: false,
stopping: false,
restart_scheduled: true,
restart_attempts: 3,
endpoint_kind: 'ipc',
pid: 9876,
error: 'connect ENOENT [redacted endpoint]',
})
expect(ctx.body.agent_bridge).not.toHaveProperty('endpoint')
expect(JSON.stringify(ctx.body.agent_bridge)).not.toContain('/tmp/hermes-agent-bridge.sock')
})
it('handles agent bridge readiness probe errors without failing the health check', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))
const { healthCheck, checkReadiness, getRuntimeState } = await loadHealthControllerWithoutInjectedVersion({
bridgeReadinessError: new Error('bridge manager unavailable at ipc:///tmp/hermes-agent-bridge.sock (ENOENT /tmp/hermes-agent-bridge.sock)'),
})
const ctx = createMockCtx()
await expect(healthCheck(ctx)).resolves.toBeUndefined()
expect(checkReadiness).toHaveBeenCalledWith({ timeoutMs: 75, connectRetryMs: 0 })
expect(getRuntimeState).toHaveBeenCalledTimes(1)
expect(ctx.body.status).toBe('ok')
expect(ctx.body.gateway).toBe('running')
expect(ctx.body.agent_bridge).toEqual({
status: 'unknown',
reachable: false,
error: 'bridge manager unavailable at [redacted endpoint] (ENOENT [redacted endpoint])',
})
expect(JSON.stringify(ctx.body.agent_bridge)).not.toContain('/tmp/hermes-agent-bridge.sock')
expect(JSON.stringify(ctx.body.agent_bridge)).not.toContain('ipc:///tmp/hermes-agent-bridge.sock')
})
it('handles manager construction errors without failing the base health check', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))
const { healthCheck, getAgentBridgeManager, checkReadiness } = await loadHealthControllerWithoutInjectedVersion({
managerError: new Error('bad bridge config /tmp/hermes-agent-bridge.sock'),
})
const ctx = createMockCtx()
await expect(healthCheck(ctx)).resolves.toBeUndefined()
expect(getAgentBridgeManager).toHaveBeenCalledTimes(1)
expect(checkReadiness).not.toHaveBeenCalled()
expect(ctx.body.status).toBe('ok')
expect(ctx.body.agent_bridge).toEqual({
status: 'unknown',
reachable: false,
error: 'bad bridge config [redacted endpoint]',
})
})
it('handles runtime-state errors without failing the base health check', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }))
const { healthCheck, getRuntimeState, checkReadiness } = await loadHealthControllerWithoutInjectedVersion({
runtimeStateError: new Error('runtime state unavailable'),
})
const ctx = createMockCtx()
await expect(healthCheck(ctx)).resolves.toBeUndefined()
expect(getRuntimeState).toHaveBeenCalledTimes(1)
expect(checkReadiness).not.toHaveBeenCalled()
expect(ctx.body.status).toBe('ok')
expect(ctx.body.agent_bridge).toEqual({
status: 'unknown',
reachable: false,
error: 'runtime state unavailable',
})
})
})