test(desktop): cover /btw prompt.btw dispatch and btw.complete

Pin the RPC route (not slash.exec), bare-/btw usage, older-gateway
fallback, and originating-session answer rendering.

Co-authored-by: SsSs <w-kwan@hotmail.com>
Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
This commit is contained in:
Brooklyn Nicholson
2026-08-31 19:24:32 -05:00
committed by brooklyn!
parent 5994b833b0
commit 38e4de48d8
3 changed files with 167 additions and 1 deletions
@@ -0,0 +1,66 @@
import { act, cleanup } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RpcEvent } from '@/types/hermes'
import { type MessageStreamHarness, renderMessageStream } from './test-harness'
const SID = 'session-1'
const OTHER_SID = 'session-2'
let stream: MessageStreamHarness
function mountStream() {
stream = renderMessageStream(SID)
}
function emit(type: RpcEvent['type'], payload: RpcEvent['payload'] = {}, sessionId = SID) {
act(() => stream.handleEvent({ payload, session_id: sessionId, type }))
}
function lastMessage(id = SID) {
return stream.state(id).messages.at(-1)
}
describe('btw.complete event', () => {
beforeEach(() => {
mountStream()
})
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
// #99065: prompt.btw delivers the answer here. The slash-worker route
// printed it after the stdout capture window closed, so only the ack rendered.
it('appends the answer to the originating session as a system message', () => {
emit('btw.complete', { task_id: 'btw_ab12cd', question: 'which file was that error in?', text: 'src/main.ts' })
const message = lastMessage()
expect(message?.role).toBe('system')
expect(message?.id).toBe('btw-complete-btw_ab12cd')
expect(stream.text()).toBe('[btw "which file was that error in?" (btw_ab12cd)]\nsrc/main.ts')
})
it('keeps another session untouched when the event targets this one', () => {
emit('btw.complete', { task_id: 'btw_1', text: 'for the other chat' }, OTHER_SID)
expect(lastMessage()).toBeUndefined()
expect(stream.text(OTHER_SID)).toBe('[btw (btw_1)]\nfor the other chat')
})
it('omits the question and task-id header bits when the backend omits them', () => {
emit('btw.complete', { text: 'the answer' })
expect(stream.text()).toBe('[btw]\nthe answer')
})
it('drops an empty completion instead of appending a blank line', () => {
emit('btw.complete', { task_id: 'btw_1', question: 'q', text: ' ' })
expect(lastMessage()).toBeUndefined()
})
})
)
@@ -1002,6 +1002,99 @@ describe('usePromptActions /compress', () => {
})
})
describe('usePromptActions /btw', () => {
afterEach(() => {
cleanup()
vi.restoreAllMocks()
})
// #99065: slash.exec only captured the ack; the answer prints after the
// worker's stdout window. prompt.btw + btw.complete is the TUI path.
it('routes through prompt.btw (not slash.exec) and renders the start notice', async () => {
const seeds: Record<string, unknown>[] = []
const requestGateway = vi.fn(async (method: string) => {
if (method === 'prompt.btw') {
return { task_id: 'btw_ab12cd' } as never
}
return {} as never
})
let handle: HarnessHandle | null = null
await actRender(
<Harness
onReady={h => (handle = h)}
onSeedState={s => seeds.push(s)}
refreshSessions={async () => undefined}
requestGateway={requestGateway}
/>
)
await handle!.submitText('/btw which file was that error in?')
expect(requestGateway).toHaveBeenCalledWith('prompt.btw', {
session_id: RUNTIME_SESSION_ID,
text: 'which file was that error in?'
})
expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything())
expect(requestGateway).not.toHaveBeenCalledWith('command.dispatch', expect.anything())
expect(renderedSeedTexts(seeds).some(text => text.includes('btw_ab12cd'))).toBe(true)
})
it('shows usage when no question is typed', async () => {
const seeds: Record<string, unknown>[] = []
const requestGateway = vi.fn(async () => ({} as never))
let handle: HarnessHandle | null = null
await actRender(
<Harness
onReady={h => (handle = h)}
onSeedState={s => seeds.push(s)}
refreshSessions={async () => undefined}
requestGateway={requestGateway}
/>
)
await handle!.submitText('/btw')
expect(requestGateway).not.toHaveBeenCalled()
expect(renderedSeedTexts(seeds).some(text => text.includes('Usage: /btw'))).toBe(true)
})
it('falls back to the slash worker when an older gateway lacks prompt.btw', async () => {
const seeds: Record<string, unknown>[] = []
const requestGateway = vi.fn(async (method: string) => {
if (method === 'prompt.btw') {
throw new Error('method not found: prompt.btw')
}
if (method === 'slash.exec') {
return { output: 'Side question started by legacy gateway' } as never
}
throw new Error(`unexpected method: ${method}`)
})
let handle: HarnessHandle | null = null
await actRender(
<Harness
onReady={h => (handle = h)}
onSeedState={s => seeds.push(s)}
refreshSessions={async () => undefined}
requestGateway={requestGateway}
/>
)
await handle!.submitText('/btw anything')
expect(requestGateway).toHaveBeenCalledWith('slash.exec', expect.objectContaining({ command: 'btw anything' }))
expect(renderedSeedTexts(seeds).some(text => text.includes('legacy gateway'))).toBe(true)
})
})
describe('usePromptActions exec fallback error reporting', () => {
beforeEach(() => {
setSessions(() => [sessionInfo()])
@@ -232,9 +232,9 @@ describe('desktop slash command curation', () => {
})
it('still routes commands without dedicated RPCs through exec()', () => {
// /btw is an action (prompt.btw) — the slash-worker print never reached Desktop.
const execNames = [
'/bg',
'/btw',
'/debug',
'/goal',
'/personality',
@@ -251,6 +251,13 @@ describe('desktop slash command curation', () => {
}
})
it('routes /btw to the prompt.btw side-question action', () => {
expect(resolveDesktopCommand('/btw')?.surface).toEqual({ kind: 'action', action: 'btw' })
expect(isDesktopSlashCommand('/btw')).toBe(true)
expect(isDesktopSlashSuggestion('/btw')).toBe(true)
expect(desktopSlashUnavailableMessage('/btw')).toBeNull()
})
it('distinguishes free prose from finite slash option lists', () => {
expect(desktopSlashCommandArgumentMode('/goal')).toBe('mixed')
expect(desktopSlashCommandArgumentMode('/steer')).toBe('text')