Files
2026-08-21 13:01:13 +08:00

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/);
});
});