const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const { test } = require('node:test') const ts = require('typescript') function loadTypeScript(file, mocks = {}) { const source = fs.readFileSync(path.join(__dirname, '../src', file), 'utf8') const { outputText } = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, esModuleInterop: true }, }) const module = { exports: {} } new Function('require', 'module', 'exports', outputText)((id) => { if (Object.hasOwn(mocks, id)) return mocks[id] if (id.endsWith('.png')) return path.basename(id) return require(id) }, module, module.exports) return module.exports } const { getParticipantArtwork, getRoleArtwork } = loadTypeScript('utils/artwork.ts') const { formatConversationPreview } = loadTypeScript('utils/chat-preview.ts') const story = '好呀,我给你讲一个《小水滴的旅行》的故事。\r\n\r\n有一天,小水滴在云妈妈的怀抱里睡着了。\n小水滴落到了一片荷叶上。\u2028小水滴继续向前,来到了大河里。\n这个故事告诉我们,水是不断循环的。💧' const roles = ['Banban', 'Baikeboshi', 'Zhuli', 'Tuntunzai'] const { getConversations, getMessages } = loadTypeScript('services/chat.ts', { '@tarojs/taro': {}, './auth': { getCurrentUserId: () => 1 }, './binding': { loadCurrentChildBindingContext: async () => ({ currentChild: { child_id: 7, child_name: '拌拌' }, currentBinding: { device_id: 'fixture-device' }, }) }, './session': {}, './api': { request: async (url) => { if (url.endsWith('/roles')) return roles.map(role_key => ({ role_key, name: role_key })) if (url.includes('/ai-conversations')) return { items: roles.map((role_key, index) => ({ conversation_id: 10 + index, role_key, message_count: 1, last_message_preview: story, })) } if (url.includes('/conversations?')) return { items: [ { conversation_id: 1, conversation_type_name: 'parent_child', peer_type: 'parent', peer_id: '1' }, { conversation_id: 2, conversation_type_name: 'parent_child', peer_type: 'parent', peer_id: '2', peer_name: '右右' }, { conversation_id: 3, conversation_type_name: 'child_peer', peer_type: 'child', peer_id: '42', peer_name: '伴伴2号' }, ] } if (url.includes('/messages?')) return { items: [{ id: 100, conversation_id: 11, role_key: 'Baikeboshi', is_user: false, content: story, }] } throw new Error(`Unexpected fixture request: ${url}`) } }, }) test('real conversation mapping gives both parent entries the flower avatar and preserves specialist AI roles', async () => { const conversations = await getConversations() const artworkById = Object.fromEntries(conversations.map(item => [ item.id, getParticipantArtwork(item.peerKind, item.roleKey), ])) assert.equal(artworkById[1], 'avatar-parent.png') assert.equal(artworkById[2], 'avatar-parent.png') assert.equal(artworkById[3], 'role-tuntunzai.png') assert.equal(artworkById[10], 'role-tuntunzai.png') assert.equal(artworkById[11], 'role-baike.png') assert.equal(artworkById[12], 'role-zhuli.png') }) test('the role chooser and conversation avatars agree for each AI role', () => { for (const role of roles) { assert.equal(getParticipantArtwork('ai', role), getRoleArtwork(role)) assert.equal(getParticipantArtwork('ai', ` ${role.toUpperCase()} `), getRoleArtwork(role)) } }) test('a multiline story becomes a single-line list preview while message history keeps its paragraphs', async () => { const conversation = (await getConversations()).find(item => item.id === 11) const preview = formatConversationPreview(conversation.lastMessage) assert.doesNotMatch(preview, /[\r\n\u2028\u2029]/) assert.match(preview, /故事。 有一天/) assert.ok(preview.endsWith('💧')) assert.equal(conversation.lastMessage, story) const messages = await getMessages(11, 'ai') assert.equal(messages[0].content, story) })