import { beforeEach, describe, expect, it, vi } from 'vitest'; const hostApi = vi.hoisted(() => ({ fetch: vi.fn(), ensureToken: vi.fn(), createEvents: vi.fn(), })); vi.mock('@/lib/host-api', () => ({ hostApiFetch: hostApi.fetch, ensureHostApiToken: hostApi.ensureToken, createHostEventSource: hostApi.createEvents, })); describe('coding Conversations Host facade', () => { beforeEach(() => { vi.clearAllMocks(); }); it('loads the target snapshot through the encoded Host route', async () => { const snapshot = { conversation: { id: 'conversation/a' } }; hostApi.fetch.mockResolvedValueOnce({ snapshot }); const { getCodingConversationSnapshot } = await import('@/lib/coding-conversations'); await expect(getCodingConversationSnapshot('conversation/a')).resolves.toBe(snapshot); expect(hostApi.fetch).toHaveBeenCalledWith( '/api/coding/conversations/conversation%2Fa/snapshot', ); }); it('submits only the product prompt DTO and returns its acceptance', async () => { const acceptance = { accepted: true, conversationId: 'conversation/a', clientRequestId: 'request-1', runId: 'run-1', mode: 'prompt', }; hostApi.fetch.mockResolvedValueOnce({ acceptance }); const { submitCodingConversationPrompt } = await import('@/lib/coding-conversations'); await expect(submitCodingConversationPrompt({ conversationId: 'conversation/a', clientRequestId: 'request-1', mode: 'prompt', text: 'Build it', attachments: [{ attachmentId: 'attachment-1' }], })).resolves.toBe(acceptance); expect(hostApi.fetch).toHaveBeenCalledWith( '/api/coding/conversations/conversation%2Fa/prompt', { method: 'POST', body: JSON.stringify({ clientRequestId: 'request-1', mode: 'prompt', text: 'Build it', attachments: [{ attachmentId: 'attachment-1' }], }), }, ); }); it('hydrates the Host token before constructing the EventSource URL', async () => { const order: string[] = []; const source = { close: vi.fn() } as unknown as EventSource; hostApi.ensureToken.mockImplementationOnce(async () => { order.push('token'); return 'host-token'; }); hostApi.createEvents.mockImplementationOnce((path: string) => { order.push(`events:${path}`); return source; }); const { openCodingConversationEvents } = await import('@/lib/coding-conversations'); await expect(openCodingConversationEvents('conversation/a')).resolves.toBe(source); expect(order).toEqual([ 'token', 'events:/api/coding/events?conversationId=conversation%2Fa', ]); }); it('routes every PI-130 Conversation control through the product Host surface', async () => { const model = { model: { accountId: 'account/1', modelId: 'model/1', thinkingLevel: 'medium' }, modelResolution: 'resolved', }; const forked = { id: 'forked-1' }; const diagnostics = { revision: { provider: 1, resources: 2 }, workers: [] }; hostApi.fetch .mockResolvedValueOnce({}) .mockResolvedValueOnce({ model }) .mockResolvedValueOnce({ model }) .mockResolvedValueOnce({}) .mockResolvedValueOnce({}) .mockResolvedValueOnce({ conversation: forked }) .mockResolvedValueOnce({ interactions: [{ id: 'interaction-1' }] }) .mockResolvedValueOnce({}) .mockResolvedValueOnce({ runtime: diagnostics }); const facade = await import('@/lib/coding-conversations'); await facade.abortCodingConversation('conversation/a'); await expect(facade.setCodingConversationModel('conversation/a', model.model)).resolves.toBe(model); await expect(facade.setCodingConversationThinking('conversation/a', 'high')).resolves.toBe(model); await facade.compactCodingConversation('conversation/a'); await facade.recoverCodingConversation('conversation/a'); await expect(facade.forkCodingConversation('conversation/a', 'entry/1')).resolves.toBe(forked); await facade.listCodingConversationInteractions('conversation/a'); await facade.respondCodingConversationInteraction('conversation/a', { interactionId: 'interaction/1', optionId: 'option-1', }); await expect(facade.getCodingRuntimeDiagnostics()).resolves.toBe(diagnostics); expect(hostApi.fetch.mock.calls).toEqual([ ['/api/coding/conversations/conversation%2Fa/abort', { method: 'POST', body: '{}' }], ['/api/coding/conversations/conversation%2Fa/model', { method: 'POST', body: JSON.stringify({ model: model.model }), }], ['/api/coding/conversations/conversation%2Fa/thinking', { method: 'POST', body: JSON.stringify({ thinkingLevel: 'high' }), }], ['/api/coding/conversations/conversation%2Fa/compact', { method: 'POST', body: '{}' }], ['/api/coding/conversations/conversation%2Fa/recover', { method: 'POST', body: '{}' }], ['/api/coding/conversations/conversation%2Fa/fork', { method: 'POST', body: JSON.stringify({ sourceEntryId: 'entry/1' }), }], ['/api/coding/interactions?conversationId=conversation%2Fa'], ['/api/coding/interactions/interaction%2F1/respond', { method: 'POST', body: JSON.stringify({ conversationId: 'conversation/a', optionId: 'option-1' }), }], ['/api/coding/runtime/diagnostics'], ]); expect(JSON.stringify(hostApi.fetch.mock.calls)).not.toContain('/api/opencode'); }); });