mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
test(desktop): move Stop/rewind background tests to their own file
index.test.tsx sits over the file-size ratchet; the interrupt-vs-rewind suite lives in index.background-interrupt.test.tsx instead.
This commit is contained in:
1 parent
5c1484f759
commit
0ebc4a85cd
2 files changed
+142
-111
No files matched your search
+141
@@ -0,0 +1,141 @@
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { getLatestSessionMessages } from '@/hermes'
|
||||
import { textPart } from '@/lib/chat-messages'
|
||||
import { $backgroundStatusBySession, reconcileBackgroundProcesses } from '@/store/composer-status'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
import { clearNotifications } from '@/store/notifications'
|
||||
import { $busy, setMessages } from '@/store/session'
|
||||
import { $subagentsBySession, upsertSubagent } from '@/store/subagents'
|
||||
|
||||
import { actRender, Harness, type HarnessHandle, RUNTIME_SESSION_ID } from './index-test-utils'
|
||||
import { clearSingleFlightSessionResumeState } from './single-flight-resume'
|
||||
|
||||
beforeEach(() => {
|
||||
clearSingleFlightSessionResumeState()
|
||||
vi.mocked(getLatestSessionMessages).mockReset()
|
||||
vi.mocked(getLatestSessionMessages).mockImplementation(async () => ({ messages: [], session_id: 'session' }))
|
||||
})
|
||||
|
||||
vi.mock('@/hermes', () => ({
|
||||
getLatestSessionMessages: vi.fn(async () => ({ messages: [], session_id: 'session' })),
|
||||
getProfiles: vi.fn(async () => ({ profiles: [] })),
|
||||
getSession: vi.fn(),
|
||||
PROMPT_SUBMIT_REQUEST_TIMEOUT_MS: 1_800_000,
|
||||
setApiRequestProfile: vi.fn(),
|
||||
transcribeAudio: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/store/gateway', async importOriginal => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
requestGatewayForAgent: vi.fn()
|
||||
}))
|
||||
|
||||
// Plain interrupts must not kill background work; rewinds must. cancelRun is
|
||||
// Stop and "send now" on a queued message while busy — the timeline survives,
|
||||
// and the gateway's session.interrupt leaves terminal(background=true) running.
|
||||
// reload / restore / edit truncate history, so processes from the discarded
|
||||
// turns get reaped.
|
||||
describe('usePromptActions background processes: interrupt vs rewind', () => {
|
||||
const seed = [
|
||||
{ id: 'u1', parts: [textPart('first')], role: 'user' as const, timestamp: 0 },
|
||||
{ id: 'a1', parts: [textPart('reply')], role: 'assistant' as const, timestamp: 1 }
|
||||
]
|
||||
|
||||
let gatewayCalls: Array<{ method: string; params?: Record<string, unknown> }>
|
||||
// resetSessionBackground remembers dismissed ids per session at module
|
||||
// level, so each test seeds a fresh process id.
|
||||
let procSeq = 0
|
||||
let procId = ''
|
||||
|
||||
beforeEach(() => {
|
||||
procId = `proc_live_${++procSeq}`
|
||||
$busy.set(false)
|
||||
$backgroundStatusBySession.set({})
|
||||
setMessages(seed as never)
|
||||
gatewayCalls = []
|
||||
$gateway.set({
|
||||
request: async (method: string, params?: Record<string, unknown>) => {
|
||||
gatewayCalls.push({ method, params })
|
||||
|
||||
return {} as never
|
||||
}
|
||||
} as never)
|
||||
reconcileBackgroundProcesses(RUNTIME_SESSION_ID, [
|
||||
{ command: 'npm run dev', session_id: procId, status: 'running' }
|
||||
] as never)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
clearNotifications()
|
||||
setMessages([])
|
||||
$busy.set(false)
|
||||
$backgroundStatusBySession.set({})
|
||||
$gateway.set(null)
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const killed = () => gatewayCalls.some(call => call.method === 'process.kill' && call.params?.process_id === procId)
|
||||
|
||||
async function mount(requestGateway: ReturnType<typeof vi.fn>) {
|
||||
let handle: HarnessHandle | null = null
|
||||
await actRender(
|
||||
<Harness
|
||||
onReady={h => (handle = h)}
|
||||
refreshSessions={async () => undefined}
|
||||
requestGateway={requestGateway as never}
|
||||
seedMessages={seed}
|
||||
/>
|
||||
)
|
||||
|
||||
return handle!
|
||||
}
|
||||
|
||||
it('cancelRun interrupts the turn but leaves running background processes and subagents alone', async () => {
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
const handle = await mount(requestGateway)
|
||||
upsertSubagent(RUNTIME_SESSION_ID, { goal: 'review', status: 'running', subagent_id: 'bg-review', task_index: 0 })
|
||||
upsertSubagent(RUNTIME_SESSION_ID, { goal: 'old', status: 'completed', subagent_id: 'done-1', task_index: 1 })
|
||||
|
||||
await handle.cancelRun()
|
||||
|
||||
expect(($subagentsBySession.get()[RUNTIME_SESSION_ID] ?? []).map(row => [row.id, row.status])).toEqual([
|
||||
['bg-review', 'running']
|
||||
])
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledWith('session.interrupt', { session_id: RUNTIME_SESSION_ID })
|
||||
expect(requestGateway).not.toHaveBeenCalledWith('process.kill', expect.anything())
|
||||
expect(killed()).toBe(false)
|
||||
|
||||
const rows = $backgroundStatusBySession.get()[RUNTIME_SESSION_ID] ?? []
|
||||
|
||||
expect(rows.map(row => [row.id, row.state])).toEqual([[procId, 'running']])
|
||||
})
|
||||
|
||||
it('reloadFromMessage (regenerate) kills processes from the truncated turns', async () => {
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
const handle = await mount(requestGateway)
|
||||
|
||||
await handle.reloadFromMessage('u1')
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledWith(
|
||||
'prompt.submit',
|
||||
expect.objectContaining({ confirm_truncate: true, truncate_before_message_id: 'u1' }),
|
||||
expect.anything()
|
||||
)
|
||||
expect(killed()).toBe(true)
|
||||
expect($backgroundStatusBySession.get()[RUNTIME_SESSION_ID] ?? []).toEqual([])
|
||||
})
|
||||
|
||||
it('restoreToMessage still kills processes from the discarded turns', async () => {
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
const handle = await mount(requestGateway)
|
||||
|
||||
await handle.restoreToMessage('u1')
|
||||
|
||||
expect(killed()).toBe(true)
|
||||
expect($backgroundStatusBySession.get()[RUNTIME_SESSION_ID] ?? []).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -11,9 +11,8 @@ import { createClientSessionState } from '@/lib/chat-runtime'
|
||||
import { $compactingSessions, setSessionCompacting } from '@/store/compaction'
|
||||
import { $composerAttachments, $composerDraft, type ComposerAttachment, setComposerDraft } from '@/store/composer'
|
||||
import { $queuedPromptsBySession, getQueuedPrompts } from '@/store/composer-queue'
|
||||
import { $backgroundStatusBySession, reconcileBackgroundProcesses } from '@/store/composer-status'
|
||||
import { $confirmRequest, settleConfirm } from '@/store/confirm'
|
||||
import { $gateway, requestGatewayForAgent } from '@/store/gateway'
|
||||
import { requestGatewayForAgent } from '@/store/gateway'
|
||||
import { $goalsBySession, setSessionGoal } from '@/store/goals'
|
||||
import { $hudMode } from '@/store/hud'
|
||||
import { $notifications, clearNotifications } from '@/store/notifications'
|
||||
@@ -31,7 +30,6 @@ import {
|
||||
setSessions
|
||||
} from '@/store/session'
|
||||
import { dropSessionState, publishSessionState } from '@/store/session-states'
|
||||
import { $subagentsBySession, upsertSubagent } from '@/store/subagents'
|
||||
import { $wakeWord, resetWakeWordState } from '@/store/wake-word'
|
||||
|
||||
import { actRender, Harness, type HarnessHandle, RUNTIME_SESSION_ID, sessionInfo } from './index-test-utils'
|
||||
@@ -5095,114 +5093,6 @@ describe('usePromptActions cancelRun', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Plain interrupts must not kill background work; rewinds must. cancelRun is
|
||||
// Stop and "send now" on a queued message while busy — the timeline survives,
|
||||
// and the gateway's session.interrupt leaves terminal(background=true) running.
|
||||
// reload / restore / edit truncate history, so processes from the discarded
|
||||
// turns get reaped.
|
||||
describe('usePromptActions background processes: interrupt vs rewind', () => {
|
||||
const seed = [
|
||||
{ id: 'u1', parts: [textPart('first')], role: 'user' as const, timestamp: 0 },
|
||||
{ id: 'a1', parts: [textPart('reply')], role: 'assistant' as const, timestamp: 1 }
|
||||
]
|
||||
|
||||
let gatewayCalls: Array<{ method: string; params?: Record<string, unknown> }>
|
||||
// resetSessionBackground remembers dismissed ids per session at module
|
||||
// level, so each test seeds a fresh process id.
|
||||
let procSeq = 0
|
||||
let procId = ''
|
||||
|
||||
beforeEach(() => {
|
||||
procId = `proc_live_${++procSeq}`
|
||||
$busy.set(false)
|
||||
$backgroundStatusBySession.set({})
|
||||
setMessages(seed as never)
|
||||
gatewayCalls = []
|
||||
$gateway.set({
|
||||
request: async (method: string, params?: Record<string, unknown>) => {
|
||||
gatewayCalls.push({ method, params })
|
||||
|
||||
return {} as never
|
||||
}
|
||||
} as never)
|
||||
reconcileBackgroundProcesses(RUNTIME_SESSION_ID, [
|
||||
{ command: 'npm run dev', session_id: procId, status: 'running' }
|
||||
] as never)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
clearNotifications()
|
||||
setMessages([])
|
||||
$busy.set(false)
|
||||
$backgroundStatusBySession.set({})
|
||||
$gateway.set(null)
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
const killed = () => gatewayCalls.some(call => call.method === 'process.kill' && call.params?.process_id === procId)
|
||||
|
||||
async function mount(requestGateway: ReturnType<typeof vi.fn>) {
|
||||
let handle: HarnessHandle | null = null
|
||||
await actRender(
|
||||
<Harness
|
||||
onReady={h => (handle = h)}
|
||||
refreshSessions={async () => undefined}
|
||||
requestGateway={requestGateway as never}
|
||||
seedMessages={seed}
|
||||
/>
|
||||
)
|
||||
|
||||
return handle!
|
||||
}
|
||||
|
||||
it('cancelRun interrupts the turn but leaves running background processes and subagents alone', async () => {
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
const handle = await mount(requestGateway)
|
||||
upsertSubagent(RUNTIME_SESSION_ID, { goal: 'review', status: 'running', subagent_id: 'bg-review', task_index: 0 })
|
||||
upsertSubagent(RUNTIME_SESSION_ID, { goal: 'old', status: 'completed', subagent_id: 'done-1', task_index: 1 })
|
||||
|
||||
await handle.cancelRun()
|
||||
|
||||
expect(($subagentsBySession.get()[RUNTIME_SESSION_ID] ?? []).map(row => [row.id, row.status])).toEqual([
|
||||
['bg-review', 'running']
|
||||
])
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledWith('session.interrupt', { session_id: RUNTIME_SESSION_ID })
|
||||
expect(requestGateway).not.toHaveBeenCalledWith('process.kill', expect.anything())
|
||||
expect(killed()).toBe(false)
|
||||
|
||||
const rows = $backgroundStatusBySession.get()[RUNTIME_SESSION_ID] ?? []
|
||||
|
||||
expect(rows.map(row => [row.id, row.state])).toEqual([[procId, 'running']])
|
||||
})
|
||||
|
||||
it('reloadFromMessage (regenerate) kills processes from the truncated turns', async () => {
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
const handle = await mount(requestGateway)
|
||||
|
||||
await handle.reloadFromMessage('u1')
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledWith(
|
||||
'prompt.submit',
|
||||
expect.objectContaining({ confirm_truncate: true, truncate_before_message_id: 'u1' }),
|
||||
expect.anything()
|
||||
)
|
||||
expect(killed()).toBe(true)
|
||||
expect($backgroundStatusBySession.get()[RUNTIME_SESSION_ID] ?? []).toEqual([])
|
||||
})
|
||||
|
||||
it('restoreToMessage still kills processes from the discarded turns', async () => {
|
||||
const requestGateway = vi.fn(async () => ({}) as never)
|
||||
const handle = await mount(requestGateway)
|
||||
|
||||
await handle.restoreToMessage('u1')
|
||||
|
||||
expect(killed()).toBe(true)
|
||||
expect($backgroundStatusBySession.get()[RUNTIME_SESSION_ID] ?? []).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('usePromptActions eager attachment upload (drop-time)', () => {
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
|
||||
Reference in new issue
Block a user