mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-12 13:52:15 +00:00
Broadens Tranquil-Flow's profile-startup timeout fix (#48518) from getProfiles + refreshActiveProfile to the rest of the calls the desktop fires during connect: /api/config, /api/config/defaults, /api/model/info, /api/model/options, /api/cron/jobs. On a profile-heavy or remote install any of these can exceed the 15s DEFAULT_FETCH_TIMEOUT_MS while the backend is alive-but-busy (e.g. list_profiles walks the skill tree per profile), surfacing as the spurious "Timed out connecting to Hermes backend after 15000ms" that hangs the UI (#48504). Uses the surgical per-call mechanism (renamed STARTUP_PROFILE_REQUEST_TIMEOUT_MS → STARTUP_REQUEST_TIMEOUT_MS) rather than raising the global default (the alternative in #48526): the liveness poll /api/status and all interactive/ runtime calls keep the short default, so a genuinely-dead backend is still detected fast and the boot readiness probe (waitForHermes) is untouched. Supersedes #48518 (carried as the base commit) and #48526 (global-default raise). Fixes #48504. Co-authored-by: YapBi <129007007+HeLLGURD@users.noreply.github.com> Co-authored-by: Tranquil-Flow <66773372+Tranquil-Flow@users.noreply.github.com>
139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
getCronJobs,
|
|
getGlobalModelInfo,
|
|
getGlobalModelOptions,
|
|
getHermesConfig,
|
|
getHermesConfigDefaults,
|
|
getProfiles,
|
|
getSessionMessages,
|
|
getStatus,
|
|
listAllProfileSessions,
|
|
listSessions
|
|
} from './hermes'
|
|
import { refreshActiveProfile } from './store/profile'
|
|
|
|
const emptySessionsResponse = {
|
|
limit: 0,
|
|
offset: 0,
|
|
sessions: [],
|
|
total: 0
|
|
}
|
|
|
|
describe('Hermes REST session helpers', () => {
|
|
let api: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
api = vi.fn().mockResolvedValue(emptySessionsResponse)
|
|
Object.defineProperty(window, 'hermesDesktop', {
|
|
configurable: true,
|
|
value: { api }
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
Reflect.deleteProperty(window, 'hermesDesktop')
|
|
})
|
|
|
|
it('uses a longer timeout for the single-profile session list', async () => {
|
|
await listSessions(50, 1)
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: '/api/sessions?limit=50&offset=0&min_messages=1&archived=exclude&order=recent',
|
|
timeoutMs: 60_000
|
|
})
|
|
)
|
|
})
|
|
|
|
it('uses a longer timeout for the all-profile session list', async () => {
|
|
await listAllProfileSessions(50, 1)
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: '/api/profiles/sessions?limit=50&offset=0&min_messages=1&archived=exclude&order=recent&profile=all',
|
|
timeoutMs: 60_000
|
|
})
|
|
)
|
|
})
|
|
|
|
it('uses a longer timeout for profile listing during desktop startup', async () => {
|
|
api.mockResolvedValue({ profiles: [] })
|
|
|
|
await getProfiles()
|
|
|
|
expect(api).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
path: '/api/profiles',
|
|
timeoutMs: 60_000
|
|
})
|
|
)
|
|
})
|
|
|
|
it('uses a longer timeout for active profile refresh during desktop startup', async () => {
|
|
api
|
|
.mockResolvedValueOnce({ current: 'default' })
|
|
.mockResolvedValueOnce({ profiles: [] })
|
|
|
|
await refreshActiveProfile()
|
|
|
|
expect(api).toHaveBeenNthCalledWith(
|
|
1,
|
|
expect.objectContaining({
|
|
path: '/api/profiles/active',
|
|
timeoutMs: 60_000
|
|
})
|
|
)
|
|
expect(api).toHaveBeenNthCalledWith(
|
|
2,
|
|
expect.objectContaining({
|
|
path: '/api/profiles',
|
|
timeoutMs: 60_000
|
|
})
|
|
)
|
|
})
|
|
|
|
it('gives the whole startup data burst the long timeout, not just profiles', async () => {
|
|
api.mockResolvedValue({})
|
|
|
|
const bootCalls: [() => Promise<unknown>, string][] = [
|
|
[getHermesConfig, '/api/config'],
|
|
[getHermesConfigDefaults, '/api/config/defaults'],
|
|
[getGlobalModelInfo, '/api/model/info'],
|
|
[() => getGlobalModelOptions(), '/api/model/options'],
|
|
[getCronJobs, '/api/cron/jobs']
|
|
]
|
|
|
|
for (const [call, path] of bootCalls) {
|
|
api.mockClear()
|
|
await call()
|
|
expect(api).toHaveBeenCalledWith(expect.objectContaining({ path, timeoutMs: 60_000 }))
|
|
}
|
|
})
|
|
|
|
it('keeps the liveness poll on the short default so a dead backend fails fast', async () => {
|
|
api.mockResolvedValue({})
|
|
api.mockClear()
|
|
|
|
await getStatus()
|
|
|
|
// /api/status must NOT carry the long startup timeout — it is the runtime
|
|
// liveness probe and has to fail quickly when the backend drops.
|
|
const call = api.mock.calls[0]?.[0] as { path: string; timeoutMs?: number }
|
|
expect(call.path).toBe('/api/status')
|
|
expect(call.timeoutMs).toBeUndefined()
|
|
})
|
|
|
|
it('tags cross-profile message reads for Electron routing and backend lookup', async () => {
|
|
api.mockResolvedValue({ messages: [], session_id: 'session-1' })
|
|
|
|
await getSessionMessages('session-1', 'xiaoxuxu')
|
|
|
|
expect(api).toHaveBeenCalledWith({
|
|
path: '/api/sessions/session-1/messages?profile=xiaoxuxu',
|
|
profile: 'xiaoxuxu'
|
|
})
|
|
})
|
|
})
|