146 lines
4.5 KiB
JavaScript
146 lines
4.5 KiB
JavaScript
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: '<span />' },
|
|
Icon: { template: '<span />' },
|
|
Spinner: { template: '<span />' },
|
|
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/);
|
|
});
|
|
});
|