import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import { buildAvatarUpdatePayload, normalizeAvatarEditForm, pickAvatarId, unwrapListData, } from '../src/utils/avatar-page-data.js' import { renderChatMarkdownCharacters } from '../src/utils/chat-markdown.js' assert.deepEqual(unwrapListData([{ id: 'a1' }]), [{ id: 'a1' }], 'unwrapListData should return raw arrays') assert.deepEqual( unwrapListData({ data: [{ id: 'a2' }], total: 1 }), [{ id: 'a2' }], 'unwrapListData should unwrap wrapped collection payloads' ) assert.deepEqual(unwrapListData(null), [], 'unwrapListData should fall back to an empty list') assert.equal( pickAvatarId('current-id', [{ id: 'first-id' }]), 'current-id', 'pickAvatarId should prefer current avatar id' ) assert.equal( pickAvatarId('', [{ id: 'first-id' }]), 'first-id', 'pickAvatarId should fall back to the first avatar id' ) assert.equal(pickAvatarId('', []), null, 'pickAvatarId should return null when no avatar exists') const boldReply = renderChatMarkdownCharacters('请注意:**不能自行诊断或随意用药**。') assert.equal( boldReply.map((character) => character.text).join(''), '请注意:不能自行诊断或随意用药。', 'chat markdown should hide bold markers' ) assert.equal( boldReply.filter((character) => character.bold).map((character) => character.text).join(''), '不能自行诊断或随意用药', 'chat markdown should style bold text' ) assert.equal( renderChatMarkdownCharacters('****重点****').map((character) => character.text).join(''), '重点', 'chat markdown should tolerate repeated bold markers' ) assert.deepEqual( normalizeAvatarEditForm({ name: '我的分身', displayName: '会会助手', description: '描述', status: 'inactive', photoUrl: 'https://img.example/avatar.png', config: { replyStyle: 'friendly', creativity: 72, rigor: 88, humor: 16, responseLength: 'short', systemPrompt: '不要编造', profession: '医生', position: '主任医师', organization: '测试医院', organizationAddress: '测试路 1 号', autoReply: false }, }), { name: '我的分身', displayName: '会会助手', description: '描述', status: 'inactive', photoUrl: 'https://img.example/avatar.png', replyStyle: 'friendly', creativity: 72, rigor: 88, humor: 16, responseLength: 'short', systemPrompt: '不要编造', profession: '医生', position: '主任医师', organization: '测试医院', organizationAddress: '测试路 1 号', autoReply: false, }, 'normalizeAvatarEditForm should map API avatars into edit form state' ) assert.deepEqual( buildAvatarUpdatePayload({ name: '更新后的分身', displayName: '更新后的助手', description: '新描述', status: 'active', photoUrl: 'https://img.example/new-avatar.png', replyStyle: 'casual', creativity: 65, rigor: 70, humor: 25, responseLength: 'medium', systemPrompt: '回答简洁', profession: '医生', position: '主任医师', organization: '测试医院', organizationAddress: '测试路 1 号', autoReply: true, }), { name: '更新后的分身', displayName: '更新后的助手', description: '新描述', status: 'active', photoUrl: 'https://img.example/new-avatar.png', config: { replyStyle: 'casual', creativity: 65, rigor: 70, humor: 25, responseLength: 'medium', systemPrompt: '回答简洁', profession: '医生', position: '主任医师', organization: '测试医院', organizationAddress: '测试路 1 号', autoReply: true, }, }, 'buildAvatarUpdatePayload should emit API-ready update payloads' ) const knowledgeView = fs.readFileSync(path.resolve('src/views/KnowledgeManage.vue'), 'utf8') assert.match(knowledgeView, /文档知识库/, 'knowledge page should expose the document tab') assert.match(knowledgeView, /标准问答对/, 'knowledge page should expose the QA tab') assert.match(knowledgeView, /activeTab/, 'knowledge page should switch active tabs') assert.match(knowledgeView, /accept="\.md,\.txt,\.pdf,\.doc,\.docx,\.xlsx"/, 'knowledge page should accept md and txt') assert.match(knowledgeView, /mobile-card-list/, 'knowledge page should render mobile-first card lists') assert.match(knowledgeView, /knowledge-card/, 'knowledge page should expose document and QA cards') const chatView = fs.readFileSync(path.resolve('src/views/AvatarChat.vue'), 'utf8') assert.match(chatView, /avatar\?\.photoUrl/, 'chat should render the active avatar photo when available') assert.match(chatView, /userAvatarUrl/, 'chat should render the logged-in user photo when available') assert.match(chatView, /avatarStatus/, 'chat should synchronize the visible status indicator with avatar status') assert.match(chatView, /document\.title = avatar\.value/, 'chat should use the avatar name as the page title') assert.match(chatView, /position: sticky/, 'chat header should remain visible while the message list scrolls') assert.match(chatView, /typing-character/, 'chat replies should animate one character at a time') assert.match(chatView, /renderChatMarkdownCharacters/, 'chat replies should render markdown as safe web text') assert.match(chatView, /markdown-bold/, 'chat replies should style markdown emphasis without showing markers') assert.match(chatView, /streamAvatarChat/, 'private chat should consume SSE response chunks') assert.match(chatView, /streamPublicAvatarChat/, 'public chat should consume SSE response chunks') assert.match(chatView, /scrollDuringStream/, 'streaming replies should throttle scrolling to animation frames') assert.match(chatView, /typing-character\.newline/, 'streaming replies should render sentence line breaks') assert.match(chatView, /let attached = false/, 'assistant bubble should wait for the first streamed text chunk') assert.match(chatView, /reactive/, 'every streamed character should update through a reactive reply object') assert.doesNotMatch(chatView, /你好,我是\{\{/, 'chat welcome card should not introduce the avatar by name') assert.doesNotMatch(chatView, /\/\[。!?;\]\/\.test\(character\)/, 'chat should not force a line break after every sentence') assert.match(chatView, /previous === '\\n'/, 'streaming text should collapse whitespace at line boundaries') assert.match(chatView, /welcome-avatar/, 'chat welcome should use the active avatar image instead of a generic icon') assert.doesNotMatch(chatView, /我会优先参考标准问答和知识库/, 'chat welcome should not expose internal answer sources') assert.match(chatView, /welcome-description/, 'chat welcome should render the avatar description') assert.doesNotMatch(chatView, /介绍一下你自己/, 'chat welcome should not contain fixed starter questions') const editView = fs.readFileSync(path.resolve('src/views/AvatarEdit.vue'), 'utf8') assert.match(editView, />分身微调头像链接