/* * Copyright 2016-present the IoT DC3 original author or authors. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import {mount} from '@vue/test-utils'; import {createPinia, setActivePinia} from 'pinia'; import {defineComponent, h} from 'vue'; import {beforeEach, describe, expect, it, vi} from 'vitest'; import AgenticAssistant from '@/components/agentic/AgenticAssistant.vue'; import i18n from '@/config/i18n'; import type {AgenticMessage} from '@/config/types'; import {useAgenticStore} from '@/store'; import {createElButtonStub, layoutStubs} from '../setup/stubs/element-plus'; vi.mock('vue-element-plus-x/styles/index.css', () => ({})); vi.mock('vue-element-plus-x', () => ({ Prompts: {template: '
'}, Welcome: {template: '
'}, })); const PassthroughStub = defineComponent({ name: 'PassthroughStub', setup(_, {slots}) { return () => h('span', slots.default?.()); }, }); // Direct state injection on the agentic store. Going through the public // `bootstrap()` action would force four mocked API calls per test for // values that are completely irrelevant to the UI assertions below; the // tradeoff favours seeded state with a one-line escape hatch over // scaffolding API mocks. If you find yourself growing this helper with // production-like derivation logic, that's the moment to switch back to // public actions. function seedAssistant(messages: AgenticMessage[]) { const store = useAgenticStore(); store.visible = true; store.loading = false; store.streaming = messages.some((message) => message.streaming); store.activeConversationId = 'conversation-ui-test'; store.sessions = [{conversationId: 'conversation-ui-test', title: 'Reasoning UI'}]; store.models = [ { model: 'reasoning-model', label: 'Reasoning Model', stream: true, toolCall: true, vision: false, reasoning: true, }, ]; store.selectedModel = 'reasoning-model'; store.reasoningEnabled = true; store.messagesByConversation = {'conversation-ui-test': messages}; store.attachmentsByConversation = {'conversation-ui-test': []}; store.pendingAttachmentIdsByConversation = {'conversation-ui-test': []}; store.pendingActionsByConversation = {'conversation-ui-test': []}; store.traceEventsByConversation = {'conversation-ui-test': []}; } function mountAssistant() { return mount(AgenticAssistant, { global: { plugins: [i18n], directives: { loading: () => undefined, }, stubs: { ...layoutStubs, ElButton: createElButtonStub(), ElDropdown: {template: ''}, ElDropdownItem: {template: ''}, ElDropdownMenu: {template: ''}, ElIcon: PassthroughStub, ElPopover: {template: ''}, ElSlider: {template: ''}, // Element Plus icons — stub to a passthrough so the component tree mounts. ChatDotRound: PassthroughStub, ChatLineSquare: PassthroughStub, Check: PassthroughStub, CircleClose: PassthroughStub, Clock: PassthroughStub, Close: PassthroughStub, Cpu: PassthroughStub, Delete: PassthroughStub, Document: PassthroughStub, DocumentCopy: PassthroughStub, EditPen: PassthroughStub, Lightning: PassthroughStub, Paperclip: PassthroughStub, Plus: PassthroughStub, Promotion: PassthroughStub, Setting: PassthroughStub, VideoPause: PassthroughStub, Warning: PassthroughStub, RenderedAssistantMessage: { props: ['content'], template: '
{{ content }}
', }, }, }, }); } describe('AgenticAssistant', () => { beforeEach(() => { localStorage.clear(); setActivePinia(createPinia()); }); it('opens the reasoning panel while an assistant answer is streaming', () => { seedAssistant([ { id: 'assistant-1', role: 'assistant', content: '', reasoning: '正在确认当前租户上下文,并准备查询设备列表。', streaming: true, }, ]); const wrapper = mountAssistant(); const panel = wrapper.find('.agentic-reasoning-panel'); expect(panel.exists()).toBe(true); //
renders with `open=""` — the attribute exists with an // empty string value. Be explicit so an accidental `open="false"` // wouldn't pass. expect(Object.keys(panel.attributes())).toContain('open'); expect(wrapper.find('.agentic-reasoning-panel__text').text()).toContain('正在确认当前租户上下文'); expect(wrapper.find('.agentic-live-thinking').exists()).toBe(false); }); it('collapses completed reasoning by default and keeps technical details separate', () => { seedAssistant([ { id: 'assistant-1', role: 'assistant', content: '当前共有 3 台设备在线。', reasoning: '先读取租户上下文,再调用设备查询工具。', streaming: false, contentExt: { reasoning: true, tools: ['searchDevices'], tokens: {input: 40, output: 20}, }, }, ]); const wrapper = mountAssistant(); const panel = wrapper.find('.agentic-reasoning-panel'); expect(panel.exists()).toBe(true); expect(panel.attributes('open')).toBeUndefined(); expect(wrapper.find('.agentic-reasoning-panel__text').text()).toContain('先读取租户上下文'); expect(wrapper.find('.agentic-details').text()).not.toContain('先读取租户上下文'); expect(wrapper.find('.agentic-details').text()).toContain('searchDevices'); }); it('shows a compact removable preview instead of inserting raw markdown into the composer', async () => { seedAssistant([ { id: 'assistant-quote', role: 'assistant', content: '### 分析结果\n这是一段需要引用的 **AI 分析结果**。\n它包含多行详细信息。', streaming: false, }, ]); const wrapper = mountAssistant(); const actions = wrapper.findAll('.agentic-message__action'); await actions[1].trigger('click'); expect(wrapper.find('.agentic-quote-preview').exists()).toBe(true); expect(wrapper.find('.agentic-quote-preview').text()).toContain('AI response'); expect(wrapper.find('.agentic-quote-preview').text()).toContain('需要引用的 AI 分析结果'); expect(wrapper.find('.agentic-quote-preview').text()).not.toContain('> '); expect(wrapper.find('.agentic-quote-preview').text()).not.toContain('###'); await wrapper.find('.agentic-quote-preview__close').trigger('click'); expect(wrapper.find('.agentic-quote-preview').exists()).toBe(false); }); it('renders quoted user content as a styled card instead of raw markdown markers', () => { seedAssistant([ { id: 'user-quote', role: 'user', content: '> **AI response**\n> FAN-03 温度趋势存在异常。\n\n请继续分析原因。', streaming: false, }, ]); const wrapper = mountAssistant(); expect(wrapper.find('.agentic-user-quote').text()).toContain('FAN-03 温度趋势存在异常'); expect(wrapper.find('.agentic-text').text()).toBe('请继续分析原因。'); expect(wrapper.find('.agentic-message__content').text()).not.toContain('> **'); }); });