feat(aihr): harden M0-M2 mobile training flows

This commit is contained in:
2026-07-23 21:18:13 +08:00
parent 06322c994b
commit 927458b9e0
44 changed files with 1674 additions and 119 deletions
+5
View File
@@ -92,6 +92,11 @@ test('消息咨询只以消息 ID 作为受控上下文,并让每轮文字或
test('消息咨询会新开会话,隐藏媒体和数据工具,并在撤回时清空对话', async () => {
const page = await read('../src/pages/user/sop/index.vue');
assert.match(page, /if \(!conversationStarted\.value\) \{\s*\/\/ Do not start project selection[\s\S]*await activateBroadcastConsultation\(pending, accountKey, pageGeneration\);/);
assert.match(page, /!broadcastContext\.value && !broadcastContextLoading\.value && !broadcastContextPrompting\.value/);
assert.match(page, /const cancelPendingBroadcastConsultation = \(\) => \{\s*broadcastContextGeneration \+= 1;\s*broadcastContextLoading\.value = false;\s*broadcastContextPrompting\.value = false;/);
assert.match(page, /onHide\(\(\) => \{\s*showGeneration \+= 1;\s*candidateRefreshGeneration \+= 1;\s*cancelPendingBroadcastConsultation\(\);/);
assert.match(page, /onUnload\(\(\) => \{\s*showGeneration \+= 1;\s*cancelPendingBroadcastConsultation\(\);/);
assert.match(page, /error\.statusCode === 404 \|\| error\.bizCode === 404/);
assert.match(page, /const pending = readBroadcastQuestionContext\(accountKey\);\s*if \(!pending\) \{\s*if \(broadcastContext\.value\) \{\s*clearBroadcastConsultation\(\);\s*resetConversation\(false\);/);
assert.match(page, /const startNewConversation = \(\) => \{\s*clearBroadcastConsultation\(\);\s*resetConversation\(true\);/);
+26
View File
@@ -4,6 +4,17 @@ import test from 'node:test';
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8');
const centeredButtonRule = (source, selector) => {
const rule = source.match(new RegExp(`\\.${selector}\\s*\\{([^}]*)\\}`))?.[1] || '';
assert.ok(rule, `missing .${selector} style rule`);
assert.match(rule, /display:\s*flex/);
assert.match(rule, /align-items:\s*center/);
assert.match(rule, /justify-content:\s*center/);
assert.match(rule, /padding:\s*0(?:;|\s)/);
assert.match(rule, /line-height:\s*1(?:;|\s)/);
};
test('银城大喇叭仅使用认证后的真实广播接口', async () => {
const service = await read('../src/services/broadcast.ts');
@@ -74,3 +85,18 @@ test('消息详情只交接短期且账号绑定的消息 ID,再进入问模
assert.match(service, /context\.accountKey !== accountKey \|\| context\.expiresAt <= now/);
assert.match(service, /clearBroadcastQuestionContext\(\)/);
});
test('M0/M1 的短文本操作控件始终在可点击区域内居中', async () => {
const [base, list, detail, ask] = await Promise.all([
read('../src/styles/base.css'),
read('../src/pages/user/broadcast/index.vue'),
read('../src/pages/user/broadcast/detail.vue'),
read('../src/pages/user/sop/index.vue')
]);
assert.match(base, /\.primary-button,\s*\.secondary-button,\s*\.ghost-button\s*\{[^}]*display:\s*flex[^}]*align-items:\s*center[^}]*justify-content:\s*center/);
centeredButtonRule(list, 'scope-tab');
centeredButtonRule(list, 'load-more');
centeredButtonRule(detail, 'ask-message-button');
centeredButtonRule(ask, 'broadcast-context-action');
});
@@ -42,6 +42,32 @@ test('训练页提交失败会回滚本次乐观追加的对话', async () => {
assert.match(source, /uni\.showToast\(\{ title: message\.value \|\| '提交失败', icon: 'none'/);
});
test('训练求助先留存会话事件,再以 practice_help 来源检索 SOP', async () => {
const [practicePage, practiceService, knowledgeService] = await Promise.all([
pageSource('../src/pages/user/practice/index.vue'),
pageSource('../src/services/practice.ts'),
pageSource('../src/services/knowledge.ts')
]);
assert.match(practiceService, /sessions\/\$\{encodeURIComponent\(sessionId\)\}\/help/);
assert.match(practicePage, /await recordPracticeHelp\(sessionId\.value, roundIndex\.value\)/);
assert.match(practicePage, /'practice_help'/);
assert.match(knowledgeService, /'practice_help'/);
});
test('语音 profile 透传方言枚举,并保持普通话默认值', async () => {
const [apiTypes, speechService] = await Promise.all([
pageSource('../src/types/api.ts'),
pageSource('../src/services/speech.ts')
]);
assert.match(apiTypes, /dialect\?: 'mandarin' \| 'cantonese' \| 'sichuanese'/);
assert.match(speechService, /dialect: 'mandarin'/);
assert.match(speechService, /customerVoiceProfile = \([\s\S]*?dialect:/);
assert.match(speechService, /dialect === 'cantonese'.*'zh-HK'/);
assert.match(speechService, /dialect === 'sichuanese'[\s\S]*?当前设备语音不支持四川话/);
});
test('训练结果回到顶部,SOP 答案生成后定位到回答区', async () => {
const [practiceSource, sopSource, navigationSource] = await Promise.all([
pageSource('../src/pages/user/practice/index.vue'),
@@ -99,6 +125,15 @@ test('登录页按高保真原型保留品牌结构和紧凑验证码行', async
assert.doesNotMatch(source, /grid-template-columns:\s*1fr;[\s\S]*?\.code-button/);
});
test('移动端短信登录不再让客户端选择租户', async () => {
const source = await pageSource('../src/pages/auth/login/index.vue');
const loginRequest = source.match(/url: '\/auth\/mobile\/sms-login',[\s\S]*?auth: false/)?.[0] || '';
assert.match(loginRequest, /phonenumber: value/);
assert.match(loginRequest, /smsCode: formData\.smsCode\.trim\(\)/);
assert.doesNotMatch(loginRequest, /tenantId:/);
});
test('共享视觉基座包含员工端主色和卡片圆角 token', async () => {
const source = await pageSource('../src/styles/base.css');