[codex] integrate goal command workflow (#1025)
* feat: integrate goal command workflow * fix: keep goal done visible * fix: add goal done slash command * fix: promote queued message on run start
This commit is contained in:
@@ -157,4 +157,36 @@ describe('chat-run socket reconnect handling', () => {
|
||||
expect(socket.__listenerCount('disconnect')).toBe(1)
|
||||
expect(socket.emit).toHaveBeenCalledWith('run', body)
|
||||
})
|
||||
|
||||
it('fans session.command events to run-local and global handlers', async () => {
|
||||
const { onSessionCommand, startRunViaSocket } = await import('../../packages/client/src/api/hermes/chat')
|
||||
const onEvent = vi.fn()
|
||||
const onGlobalCommand = vi.fn()
|
||||
const offGlobalCommand = onSessionCommand(onGlobalCommand)
|
||||
|
||||
startRunViaSocket(
|
||||
{ session_id: 'session-1', input: '/goal status', profile: 'default', source: 'cli' },
|
||||
onEvent,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
)
|
||||
|
||||
const socket = socketState.sockets[0]
|
||||
const event = {
|
||||
event: 'session.command',
|
||||
session_id: 'session-1',
|
||||
command: 'goal',
|
||||
action: 'status',
|
||||
message: 'Goal (active, 0/20 turns): write site',
|
||||
}
|
||||
|
||||
socket.__trigger('session.command', event)
|
||||
|
||||
expect(onEvent).toHaveBeenCalledWith(event)
|
||||
expect(onGlobalCommand).toHaveBeenCalledWith(event)
|
||||
|
||||
offGlobalCommand()
|
||||
socket.__trigger('session.command', { ...event, message: 'next status' })
|
||||
expect(onGlobalCommand).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// @vitest-environment jsdom
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
const chatApi = vi.hoisted(() => ({
|
||||
registerSessionHandlers: vi.fn(),
|
||||
unregisterSessionHandlers: vi.fn(),
|
||||
getChatRunSocket: vi.fn(() => ({ emit: vi.fn() })),
|
||||
sessionCommandHandlers: [] as Array<(event: any) => void>,
|
||||
peerUserMessageHandlers: [] as Array<(event: any) => void>,
|
||||
}))
|
||||
|
||||
vi.mock('@/api/hermes/chat', () => ({
|
||||
startRunViaSocket: vi.fn(),
|
||||
resumeSession: vi.fn(),
|
||||
registerSessionHandlers: chatApi.registerSessionHandlers,
|
||||
unregisterSessionHandlers: chatApi.unregisterSessionHandlers,
|
||||
getChatRunSocket: chatApi.getChatRunSocket,
|
||||
respondToolApproval: vi.fn(),
|
||||
respondClarify: vi.fn(),
|
||||
onPeerUserMessage: vi.fn((handler: (event: any) => void) => {
|
||||
chatApi.peerUserMessageHandlers.push(handler)
|
||||
return vi.fn()
|
||||
}),
|
||||
onSessionCommand: vi.fn((handler: (event: any) => void) => {
|
||||
chatApi.sessionCommandHandlers.push(handler)
|
||||
return vi.fn()
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/client', () => ({
|
||||
getActiveProfileName: () => 'default',
|
||||
}))
|
||||
|
||||
vi.mock('@/api/hermes/sessions', () => ({
|
||||
deleteSession: vi.fn(),
|
||||
fetchSession: vi.fn(),
|
||||
fetchSessions: vi.fn(),
|
||||
setSessionModel: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/hermes/download', () => ({
|
||||
getDownloadUrl: (_path: string, name: string) => `/download/${name}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/completion-sound', () => ({
|
||||
primeCompletionSound: vi.fn(),
|
||||
playCompletionSound: vi.fn(),
|
||||
}))
|
||||
|
||||
import { useChatStore, type Session } from '@/stores/hermes/chat'
|
||||
|
||||
function makeSession(): Session {
|
||||
return {
|
||||
id: 'session-1',
|
||||
title: 'session',
|
||||
messages: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('chat store session.command fanout', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
chatApi.sessionCommandHandlers = []
|
||||
chatApi.peerUserMessageHandlers = []
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('attaches to a goal resume run started from another window', () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
expect(chatApi.sessionCommandHandlers).toHaveLength(1)
|
||||
|
||||
chatApi.sessionCommandHandlers[0]({
|
||||
event: 'session.command',
|
||||
session_id: 'session-1',
|
||||
command: 'goal',
|
||||
action: 'resume',
|
||||
message: 'Goal resumed',
|
||||
started: true,
|
||||
terminal: false,
|
||||
})
|
||||
|
||||
expect(store.isStreaming).toBe(true)
|
||||
expect(chatApi.registerSessionHandlers).toHaveBeenCalledWith('session-1', expect.objectContaining({
|
||||
onRunStarted: expect.any(Function),
|
||||
onSessionCommand: expect.any(Function),
|
||||
}))
|
||||
expect(store.messages).toEqual([
|
||||
expect.objectContaining({
|
||||
role: 'command',
|
||||
content: 'Goal resumed',
|
||||
commandAction: 'resume',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it('does not clear the transcript for goal done commands', () => {
|
||||
const store = useChatStore()
|
||||
const session = makeSession()
|
||||
session.messages = [
|
||||
{ id: 'user-1', role: 'user', content: 'keep me', timestamp: 1 },
|
||||
]
|
||||
store.sessions = [session]
|
||||
store.activeSessionId = 'session-1'
|
||||
store.activeSession = session
|
||||
|
||||
chatApi.sessionCommandHandlers[0]({
|
||||
event: 'session.command',
|
||||
session_id: 'session-1',
|
||||
command: 'goal',
|
||||
action: 'clear',
|
||||
message: 'Goal cleared.',
|
||||
terminal: true,
|
||||
})
|
||||
|
||||
expect(store.messages).toEqual([
|
||||
expect.objectContaining({ id: 'user-1', content: 'keep me' }),
|
||||
expect.objectContaining({
|
||||
role: 'command',
|
||||
content: 'Goal cleared.',
|
||||
commandAction: 'clear',
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user