mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
test(desktop): pin the sidebar spinner's liveness contract
Investigating the missing-spinner report turned up no defect in the seeding path: the active_list poll already lights a row for a turn the renderer never saw start, holds it across polls, and follows a recycled runtime id onto its new stored session. Pin all three so the reap change can't silently regress turn-start while fixing turn-end. Two boundaries worth naming rather than rediscovering: - `starting` is deliberately NOT working. It means agent_build_started without agent_ready, and _start_agent_build runs on any incidental RPC that needs the agent — not just a prompt — so treating it as a turn would spin the row on merely opening a session. - $workingSessionIds is keyed by STORED id and drops entries whose storedSessionId is null, while message.start flips busy without carrying one. A runtime that was never seeded with a stored id therefore goes busy invisibly. That is the remaining path by which a spinner can go missing.
This commit is contained in:
parent
6b273f419a
commit
19d84d1071
2 changed files with 109 additions and 0 deletions
|
|
@ -0,0 +1,77 @@
|
|||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { $selectedStoredSessionId, $unreadFinishedSessionIds } from '@/store/session'
|
||||
import { $workingSessionIds, clearAllSessionStates } from '@/store/session-states'
|
||||
|
||||
import { rehydrateLiveSessionStatuses, resetLiveRuntimeTracking } from './use-background-sync'
|
||||
|
||||
/**
|
||||
* (C) The sidebar spinner is driven by `$workingSessionIds`, which is keyed by
|
||||
* STORED session id. A turn that STARTS while Desktop isn't receiving stream
|
||||
* events — a background profile, a degraded remote socket, a session opened on
|
||||
* another surface — is only ever learned about through the `session.active_list`
|
||||
* poll. If that poll can't seed a row the renderer has never seen, the thread
|
||||
* name never gets its arc even though the backend is plainly working.
|
||||
*/
|
||||
describe('rehydrateLiveSessionStatuses — seeding a turn the renderer never saw start', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
$selectedStoredSessionId.set(null)
|
||||
$unreadFinishedSessionIds.set([])
|
||||
resetLiveRuntimeTracking()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers()
|
||||
vi.useRealTimers()
|
||||
clearAllSessionStates()
|
||||
resetLiveRuntimeTracking()
|
||||
$unreadFinishedSessionIds.set([])
|
||||
})
|
||||
|
||||
it('shows the spinner for a turn that started with no stream events', () => {
|
||||
rehydrateLiveSessionStatuses({
|
||||
sessions: [{ id: 'runtime-cold', session_key: 'stored-cold', status: 'working' }]
|
||||
})
|
||||
|
||||
expect($workingSessionIds.get()).toContain('stored-cold')
|
||||
})
|
||||
|
||||
it('keeps the spinner across polls while the turn is still running', () => {
|
||||
rehydrateLiveSessionStatuses({
|
||||
sessions: [{ id: 'runtime-cold', session_key: 'stored-cold', status: 'working' }]
|
||||
})
|
||||
rehydrateLiveSessionStatuses({
|
||||
sessions: [{ id: 'runtime-cold', session_key: 'stored-cold', status: 'working' }]
|
||||
})
|
||||
|
||||
expect($workingSessionIds.get()).toContain('stored-cold')
|
||||
})
|
||||
|
||||
it('shows the spinner when a runtime id is recycled onto a new stored session', () => {
|
||||
// A respawned backend can mint the same runtime id for a different stored
|
||||
// session. The row for the NEW stored id must light up, not the stale one.
|
||||
rehydrateLiveSessionStatuses({
|
||||
sessions: [{ id: 'runtime-1', session_key: 'stored-old', status: 'working' }]
|
||||
})
|
||||
|
||||
rehydrateLiveSessionStatuses({
|
||||
sessions: [{ id: 'runtime-1', session_key: 'stored-new', status: 'working' }]
|
||||
})
|
||||
|
||||
expect($workingSessionIds.get()).toContain('stored-new')
|
||||
expect($workingSessionIds.get()).not.toContain('stored-old')
|
||||
})
|
||||
|
||||
it('leaves a starting session idle — the agent build is not proof of a turn', () => {
|
||||
// `starting` = `agent_build_started` without `agent_ready`. _start_agent_build
|
||||
// runs on the first prompt OR any incidental RPC that needs the agent, so it
|
||||
// is not proof of a turn — lighting the spinner here would fire on merely
|
||||
// opening a session. A real turn arrives as `working`.
|
||||
rehydrateLiveSessionStatuses({
|
||||
sessions: [{ id: 'runtime-boot', session_key: 'stored-boot', status: 'starting' }]
|
||||
})
|
||||
|
||||
expect($workingSessionIds.get()).not.toContain('stored-boot')
|
||||
})
|
||||
})
|
||||
32
apps/desktop/src/store/working-ids-stored-id.test.ts
Normal file
32
apps/desktop/src/store/working-ids-stored-id.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { $workingSessionIds, clearAllSessionStates, publishSessionState } from '@/store/session-states'
|
||||
|
||||
/**
|
||||
* (C) The sidebar spinner reads `$workingSessionIds`, which projects
|
||||
* `$sessionStates` down to STORED session ids and drops any entry whose
|
||||
* `storedSessionId` is null. `message.start` flips `busy` without carrying a
|
||||
* stored id, so a runtime that was never seeded with one goes busy invisibly:
|
||||
* the backend works, the thread name stays bare.
|
||||
*/
|
||||
describe('$workingSessionIds — a busy runtime with no stored id', () => {
|
||||
afterEach(() => {
|
||||
clearAllSessionStates()
|
||||
})
|
||||
|
||||
it('cannot show a spinner for a busy runtime that has no stored id', () => {
|
||||
publishSessionState('runtime-unmapped', { ...createClientSessionState(null), busy: true })
|
||||
|
||||
// Documents the constraint rather than asserting the bug is fine: the
|
||||
// projection is keyed by stored id, so an unmapped runtime is unreachable
|
||||
// from the sidebar no matter how busy it is.
|
||||
expect($workingSessionIds.get()).toEqual([])
|
||||
})
|
||||
|
||||
it('shows the spinner as soon as the stored id is known', () => {
|
||||
publishSessionState('runtime-mapped', { ...createClientSessionState('stored-x'), busy: true })
|
||||
|
||||
expect($workingSessionIds.get()).toEqual(['stored-x'])
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue