import { flushPromises, mount } from '@vue/test-utils'; import AssistantPlayground from './AssistantPlayground.vue'; const mocked = vi.hoisted(() => ({ playground: vi.fn() })); vi.mock('dashboard/api/captain/assistant', () => ({ default: { playground: mocked.playground }, })); vi.mock('shared/composables/useMessageFormatter', () => ({ useMessageFormatter: () => ({ formatMessage: message => message }), })); const mountPlayground = () => mount(AssistantPlayground, { props: { assistantId: 7 }, global: { stubs: { Avatar: { template: '' }, Icon: { template: '' }, Spinner: { template: '' }, NextButton: false, }, }, }); const sendMessage = async (wrapper, content) => { await wrapper.get('input').setValue(content); await wrapper.findAll('button').at(-1).trigger('click'); }; describe('AssistantPlayground', () => { beforeEach(() => { mocked.playground.mockReset(); }); it('shows an accessible loading row and prevents duplicate sends', async () => { let resolveRequest; mocked.playground.mockReturnValue( new Promise(resolve => { resolveRequest = resolve; }) ); const wrapper = mountPlayground(); await sendMessage(wrapper, 'Hello'); await wrapper.get('input').setValue('Another message'); await wrapper.get('input').trigger('keydown.enter'); await wrapper.findAll('button').at(-1).trigger('click'); expect(mocked.playground).toHaveBeenCalledTimes(1); expect(wrapper.get('[role="status"]').attributes('aria-live')).toBe( 'polite' ); expect(wrapper.get('[role="status"]').text()).toContain( 'Generating response...' ); expect(wrapper.findAll('button').at(-1).attributes('disabled')).toBe(''); resolveRequest({ data: { response: 'Hi there' } }); await flushPromises(); expect(wrapper.find('[role="status"]').exists()).toBe(false); expect(wrapper.text()).toContain('Hi there'); }); it('maps fail-closed responses to a generic retryable error', async () => { let resolveRetry; mocked.playground .mockResolvedValueOnce({ data: { response: 'conversation_handoff', reasoning: 'raw provider timeout', provider: { base_url: 'https://private.example', api_key: 'secret', }, }, }) .mockImplementationOnce( () => new Promise(resolve => { resolveRetry = resolve; }) ); const wrapper = mountPlayground(); await sendMessage(wrapper, 'Original question'); await flushPromises(); const alert = wrapper.get('[role="alert"]'); const retryButton = alert.get('button'); expect(alert.text()).toContain( "The assistant couldn't respond. Try again or send a new message." ); expect(retryButton.element.tagName).toBe('BUTTON'); expect(retryButton.attributes('type')).toBe('button'); expect(wrapper.html()).not.toMatch( /conversation_handoff|reasoning|private\.example|secret/ ); await retryButton.trigger('click'); await wrapper.vm.$nextTick(); expect(wrapper.get('[role="alert"] button').attributes('disabled')).toBe( '' ); expect(wrapper.get('[role="alert"]').exists()).toBe(true); resolveRetry({ data: { response: 'Recovered answer' } }); await flushPromises(); expect(mocked.playground).toHaveBeenCalledTimes(2); expect(mocked.playground.mock.calls[1][0].messageContent).toBe( 'Original question' ); expect(wrapper.text().match(/Original question/g)).toHaveLength(1); expect(wrapper.find('[role="alert"]').exists()).toBe(false); expect(wrapper.text()).toContain('Recovered answer'); }); it('keeps the failed row while a new message succeeds', async () => { mocked.playground .mockRejectedValueOnce( new Error('raw provider error with api_key=secret') ) .mockResolvedValueOnce({ data: { response: 'New answer' } }); const wrapper = mountPlayground(); await sendMessage(wrapper, 'Failed question'); await flushPromises(); await sendMessage(wrapper, 'New question'); await flushPromises(); expect(mocked.playground).toHaveBeenCalledTimes(2); expect(mocked.playground.mock.calls[1][0].messageContent).toBe( 'New question' ); expect(wrapper.get('[role="alert"]').text()).toContain( "The assistant couldn't respond. Try again or send a new message." ); expect(wrapper.text()).toContain('New answer'); expect(wrapper.html()).not.toMatch(/api_key|secret|raw provider error/); }); });