import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import test, { after } from 'node:test'; import { fileURLToPath } from 'node:url'; import { createServer } from 'vite'; const read = (path) => readFile(new URL(path, import.meta.url), 'utf8'); const vite = await createServer({ root: fileURLToPath(new URL('..', import.meta.url)), configFile: false, optimizeDeps: { noDiscovery: true }, server: { middlewareMode: true, hmr: false }, appType: 'custom' }); after(async () => vite.close()); const { mergeKnowledgeSnippets } = await vite.ssrLoadModule('/src/services/knowledge.ts'); test('个人助理客户端使用确认式记忆接口且确认请求携带稳定幂等键', async () => { const service = await read('../src/services/personal-assistant.ts'); const card = await read('../src/components/chat/MemoryCandidateCard.vue'); assert.match(service, /\/api\/aihr\/personal-assistant\/memory-candidates/); assert.match(service, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/confirm/); assert.match(service, /\$\{candidateBase\}\/\$\{candidateRef\}\/confirm/); assert.match(service, /\/api\/aihr\/agent\/actions\/\$\{encodeURIComponent\(String\(candidateRef\)\)\}\/dismiss/); assert.match(service, /\$\{candidateBase\}\/\$\{candidateRef\}\/dismiss/); assert.match(service, /\/api\/aihr\/service-memories/); assert.match(service, /\/api\/aihr\/personal-assistant\/assistant-captures/); assert.match(card, /const idempotencyKey = ref\(createMemoryIdempotencyKey\(props\.candidate\.id\)\)/); assert.match(card, /if \(submitting\.value \|\| completed\.value\) return/); assert.match(card, /expectedVersion: props\.candidate\.version/); assert.match(card, /enableReminder: reminderEnabled\.value/); assert.match(card, /saveScope: saveScope \|\| undefined/); assert.match(card, /\.\.\.value/); for (const field of ['projectCode', 'title', 'detail', 'status']) assert.match(card, new RegExp(field)); }); test('统一采集确认卡把缺失字段作为可选提示并让用户选择保存范围', async () => { const card = await read('../src/components/chat/MemoryCandidateCard.vue'); assert.match(card, /candidate\.status === 'NEEDS_INPUT'/); assert.match(card, /candidate\.status === 'DRAFT'/); assert.match(card, /const reminderEnabled = ref\(false\)/); assert.match(card, />修改暂不保存确认保存仅自己保存提交公司处理还需要补充信息 !item\.value\?\.trim\(\)\)\.map/); assert.match(card, /missingProjectLocationLabels\.value\.length/); assert.match(card, /还需补充:\$\{missingProjectLocationLabels\.value\.join\('、'\)\}/); }); test('工作助手页面展示任务过期后不会消费待执行查询或数据工具', async () => { const page = await read('../src/pages/user/sop/index.vue'); assert.match(page, /const isCurrentShow = \(\) => generation === showGeneration && accountKey === currentAccountKey\(\)/); assert.match(page, /const pending = uni\.getStorageSync\(pendingSopQueryKey\);\s*if \(!isCurrentShow\(\)\) return;/); assert.match(page, /const pendingTool = uni\.getStorageSync\(pendingToolKey\);[\s\S]*?if \(!isCurrentShow\(\)\) return;/); assert.match(page, /if \(!isCurrentShow\(\)\) return;\s*uni\.removeStorageSync\(pendingToolKey\)/); assert.match(page, /if \(!isCurrentShow\(\)\) return;\s*uni\.removeStorageSync\(pendingSopQueryKey\)/); assert.match(page, /const requestShowGeneration = showGeneration/); assert.match(page, /const requestAccountKey = currentAccountKey\(\)/); assert.match(page, /ensureEmployeePosition\('\/pages\/user\/sop\/index'\)\) \|\| !isCurrentQuestion\(\)/); }); test('我的资料与记忆展示个人记录和公司流转状态', async () => { const [page, service] = await Promise.all([ read('../src/pages/user/assistant/memories.vue'), read('../src/services/personal-assistant.ts') ]); assert.match(page, /getAssistantCaptures/); assert.match(page, />我的记录 { const [page, header, knowledge] = await Promise.all([ read('../src/pages/user/sop/index.vue'), read('../src/components/chat/AskSourceHeader.vue'), read('../src/services/knowledge.ts') ]); assert.match(header, />\s*工作助手\s*<\/button>/); assert.match(header, />\s*查全网\s*<\/button>/); assert.match(page, /@select-web="openWebAi"/); assert.match(page, /去查全网/); assert.match(knowledge, /url: '\/api\/knowledge\/query'/); assert.match(knowledge, /memoryCandidate: response\.memoryCandidate \|\| legacy\.memoryCandidate/); assert.match(page, /visibleAccountKey !== accountKey/); assert.match(page, /accountKey !== currentAccountKey\(\)/); assert.match(page, /const generation = \+\+showGeneration/); assert.match(page, /generation === showGeneration && accountKey === currentAccountKey\(\)/); assert.match(page, /onHide\(\(\) => \{\s*showGeneration \+= 1/); assert.match(page, /onUnload\(\(\) => \{\s*showGeneration \+= 1/); assert.match(page, /项目记录/); assert.match(page, /serviceMemoryStatusLabel/); assert.match(page, /!nextResult\.answer && !nextResult\.snippets\?\.length && !nextResult\.memoryCandidate/); assert.doesNotMatch(page, /setStorageSync\([^\n]*web[^\n]*query/i); }); test('企业与项目引用合并去重并保留项目状态和时间', () => { const snippets = mergeKnowledgeSnippets([ { docTitle: '住户服务规范', content: '先确认需求。', fragmentId: 7, sourceType: 'DOCUMENT', domain: 'ENTERPRISE' }, { docTitle: '3栋2单元1201跟进记录', content: '等待回访。', docId: '901', sourceType: 'PROJECT_SERVICE', domain: 'PROJECT_SERVICE', status: 'IN_PROGRESS', occurredAt: '2026-07-18T14:00:00+08:00', updatedAt: '2026-07-19T09:20:00+08:00' } ], [ { docTitle: '住户服务规范', content: '先确认需求。', fragmentId: 7, spaceCode: 'enterprise-sop' } ]); assert.equal(snippets.length, 2); assert.equal(snippets[0].domain, 'ENTERPRISE'); assert.equal(snippets[0].spaceCode, 'enterprise-sop'); assert.equal(snippets[1].domain, 'PROJECT_SERVICE'); assert.equal(snippets[1].status, 'IN_PROGRESS'); assert.equal(snippets[1].updatedAt, '2026-07-19T09:20:00+08:00'); }); test('我的页面和路由提供首版资料与记忆入口', async () => { const [profile, memories, pages] = await Promise.all([ read('../src/pages/user/profile/index.vue'), read('../src/pages/user/assistant/memories.vue'), read('../src/pages.json') ]); assert.match(profile, /我的资料与记忆/); assert.match(profile, /openMemories/); assert.match(memories, /待确认/); assert.match(memories, /项目跟进/); assert.match(memories, /仅展示当前账号有权限查看的项目服务记录/); assert.match(memories, /项目:\{\{ item\.projectCode \}\}/); assert.match(pages, /pages\/user\/assistant\/memories/); });