hermes-agent/apps/desktop/src/store/active-work.test.ts
Brooklyn Nicholson 9ae3bd73c9 feat(desktop): confirm before quitting with a turn in flight
Cmd-Q went straight through to teardown, killing the backend mid-tool-call
— the turn is gone and whatever the agent was part-way through writing
stays part-way written, with nothing on screen to warn about it.

Renderers now report which chats are mid-turn; before-quit merges the
reports and asks, naming them, defaulting to Keep Running. Update, swap,
and uninstall relaunches skip the prompt: those are the app replacing
itself, and a modal there would strand the detached script waiting on a
PID that never exits.
2026-07-27 15:47:57 -05:00

60 lines
2.1 KiB
TypeScript

import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ClientSessionState } from '@/app/types'
import { $sessions } from './session'
import { clearAllSessionStates, publishSessionState } from './session-states'
const desktopWindow = window as unknown as { hermesDesktop?: Window['hermesDesktop'] }
const setActiveWork = vi.fn()
const busy = (storedSessionId: string, isBusy: boolean) =>
({ busy: isBusy, needsInput: false, storedSessionId }) as ClientSessionState
const session = (id: string, title: null | string) => ({ id, title }) as (typeof $sessions.value)[number]
beforeAll(async () => {
desktopWindow.hermesDesktop = { setActiveWork } as unknown as Window['hermesDesktop']
// Subscribes at import time, so the bridge has to exist first.
await import('./active-work')
})
beforeEach(() => {
clearAllSessionStates()
$sessions.set([])
setActiveWork.mockClear()
})
describe('active work bridge', () => {
it('reports a busy session by title', () => {
$sessions.set([session('s1', 'Fix login'), session('s2', 'Idle chat')])
publishSessionState('runtime-1', busy('s1', true))
expect(setActiveWork).toHaveBeenLastCalledWith({ count: 1, titles: ['Fix login'] })
})
it('counts an untitled busy session without inventing a title', () => {
$sessions.set([session('s1', null)])
publishSessionState('runtime-1', busy('s1', true))
expect(setActiveWork).toHaveBeenLastCalledWith({ count: 1, titles: [] })
})
it('drops back to nothing when the turn ends', () => {
$sessions.set([session('s1', 'Fix login')])
publishSessionState('runtime-1', busy('s1', true))
publishSessionState('runtime-1', busy('s1', false))
expect(setActiveWork).toHaveBeenLastCalledWith({ count: 0, titles: [] })
})
it('does not re-send an unchanged summary', () => {
$sessions.set([session('s1', 'Fix login')])
publishSessionState('runtime-1', busy('s1', true))
setActiveWork.mockClear()
$sessions.set([session('s1', 'Fix login'), session('s2', 'Something else')])
expect(setActiveWork).not.toHaveBeenCalled()
})
})