181 lines
7.9 KiB
JavaScript
181 lines
7.9 KiB
JavaScript
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<DisplayMessage>/, '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, />分身微调</, 'avatar edit page should use the requested title')
|
|
assert.match(editView, /uploadAvatarPhoto/, 'avatar edit page should upload a clicked replacement photo')
|
|
assert.doesNotMatch(editView, />头像链接</, 'avatar edit page should not expose a photo URL input')
|
|
for (const field of ['profession', 'position', 'organization', 'organizationAddress']) {
|
|
assert.match(editView, new RegExp(`formData\\.${field}`), `avatar edit page should expose ${field}`)
|
|
}
|
|
|
|
const manageView = fs.readFileSync(path.resolve('src/views/AvatarManage.vue'), 'utf8')
|
|
assert.match(manageView, /shareAvatar/, 'avatar management should offer a share action')
|
|
assert.match(manageView, /createAvatarShareLink/, 'share action should create a public share link')
|
|
|
|
const router = fs.readFileSync(path.resolve('src/router/index.ts'), 'utf8')
|
|
assert.match(router, /path: '\/share\/:shareToken'/, 'router should expose a public chat route')
|
|
|
|
console.log('avatar-page-data tests passed')
|