mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-26 17:38:36 +00:00
test(desktop): pin visible agent-init-failure surfacing + optimistic-message survival (#63078)
Client half of leg 2. The issue's failure mode was the desktop clearing the optimistic first message and showing nothing when the backend dropped the turn. With the gateway now preserving the message across slow builds and emitting a real error event on genuine failure, these tests pin the desktop contract that makes that visible: - an agent-init error event renders an in-transcript assistant error bubble, keeps the user's optimistic first message (never silently cleared), fires the global error toast, and releases busy/awaitingResponse so the composer is usable again; - the #65567 pre-ready cancel emit renders the same way. Covers failAssistantMessage + the gateway-event error branch, which no test exercised at the session-state level (todo-cleanup only asserted todo eviction).
This commit is contained in:
parent
60c8fc6290
commit
36703753e0
1 changed files with 128 additions and 0 deletions
|
|
@ -0,0 +1,128 @@
|
|||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { act, cleanup, render, waitFor } from '@testing-library/react'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { ClientSessionState } from '@/app/types'
|
||||
import { textPart } from '@/lib/chat-messages'
|
||||
import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { $notifications, clearNotifications } from '@/store/notifications'
|
||||
import type { RpcEvent } from '@/types/hermes'
|
||||
|
||||
import { useMessageStream } from './index'
|
||||
|
||||
const SID = 'rt-new-session'
|
||||
|
||||
let handleEvent: ((event: RpcEvent) => void) | null = null
|
||||
let sessionStates: Map<string, ClientSessionState> | null = null
|
||||
|
||||
function Harness() {
|
||||
const activeSessionIdRef = useRef<string | null>(SID)
|
||||
const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>())
|
||||
const queryClientRef = useRef(new QueryClient())
|
||||
|
||||
const stream = useMessageStream({
|
||||
activeSessionIdRef,
|
||||
hydrateFromStoredSession: vi.fn(async () => undefined),
|
||||
queryClient: queryClientRef.current,
|
||||
refreshHermesConfig: vi.fn(async () => undefined),
|
||||
refreshSessions: vi.fn(async () => undefined),
|
||||
sessionStateByRuntimeIdRef,
|
||||
updateSessionState: (sessionId, updater) => {
|
||||
const current = sessionStateByRuntimeIdRef.current.get(sessionId) ?? createClientSessionState()
|
||||
const next = updater(current)
|
||||
sessionStateByRuntimeIdRef.current.set(sessionId, next)
|
||||
|
||||
return next
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
handleEvent = stream.handleGatewayEvent
|
||||
sessionStates = sessionStateByRuntimeIdRef.current
|
||||
}, [stream.handleGatewayEvent])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function mountStream() {
|
||||
render(<Harness />)
|
||||
await waitFor(() => expect(handleEvent).not.toBeNull())
|
||||
}
|
||||
|
||||
/** Seed the session as it looks right after a first-message submit: the
|
||||
* optimistic user row is present, the turn is awaiting its response. */
|
||||
function seedOptimisticFirstMessage() {
|
||||
sessionStates!.set(SID, {
|
||||
...createClientSessionState('stored-new-session', [
|
||||
{ id: 'user-123-abc', role: 'user', parts: [textPart('first message of a new chat')] }
|
||||
]),
|
||||
busy: true,
|
||||
awaitingResponse: true
|
||||
})
|
||||
}
|
||||
|
||||
describe('useMessageStream agent-init error surfacing (#63078)', () => {
|
||||
beforeEach(() => {
|
||||
handleEvent = null
|
||||
sessionStates = null
|
||||
clearNotifications()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
clearNotifications()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('renders an agent-init failure as a visible in-transcript error and keeps the optimistic first message', async () => {
|
||||
await mountStream()
|
||||
seedOptimisticFirstMessage()
|
||||
|
||||
act(() =>
|
||||
handleEvent!({
|
||||
payload: { message: 'agent initialization timed out after 601s — your message was not sent; retry once the session is ready' },
|
||||
session_id: SID,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
|
||||
const state = sessionStates!.get(SID)!
|
||||
|
||||
// The user's optimistic first message must survive — the failure mode of
|
||||
// #63078 was the message silently vanishing into a blank session.
|
||||
const userRows = state.messages.filter(m => m.role === 'user')
|
||||
expect(userRows).toHaveLength(1)
|
||||
expect(userRows[0]!.id).toBe('user-123-abc')
|
||||
|
||||
// The failure is VISIBLE in the session view: an assistant error bubble...
|
||||
const errorRows = state.messages.filter(m => m.role === 'assistant' && m.error)
|
||||
expect(errorRows).toHaveLength(1)
|
||||
expect(errorRows[0]!.error).toContain('your message was not sent')
|
||||
|
||||
// ...and the composer is released (no forever-spinner on a dead turn).
|
||||
expect(state.busy).toBe(false)
|
||||
expect(state.awaitingResponse).toBe(false)
|
||||
|
||||
// A global toast also fired (turn-ending errors are easy to miss inline).
|
||||
expect($notifications.get().some(n => n.kind === 'error' && n.message?.includes('was not sent'))).toBe(true)
|
||||
})
|
||||
|
||||
it('renders the pre-ready cancel error event (#65567 server emit) visibly', async () => {
|
||||
await mountStream()
|
||||
seedOptimisticFirstMessage()
|
||||
|
||||
act(() =>
|
||||
handleEvent!({
|
||||
payload: { message: 'Turn cancelled before the agent was ready' },
|
||||
session_id: SID,
|
||||
type: 'error'
|
||||
})
|
||||
)
|
||||
|
||||
const state = sessionStates!.get(SID)!
|
||||
expect(state.messages.some(m => m.role === 'assistant' && m.error?.includes('cancelled'))).toBe(true)
|
||||
expect(state.messages.some(m => m.id === 'user-123-abc')).toBe(true)
|
||||
expect(state.busy).toBe(false)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue