mirror of
https://github.com/EKKOLearnAI/hermes-web-ui.git
synced 2026-07-10 02:30:15 +00:00
279 lines
9.2 KiB
TypeScript
279 lines
9.2 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
|
|
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,
|
|
}))
|
|
|
|
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('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',
|
|
})
|
|
})
|
|
|
|
})
|