test: strengthen launch smoke and capacity guardrails

This commit is contained in:
Codex
2026-07-01 04:10:55 +08:00
parent 3d33c783cd
commit 6bdb2a175a
11 changed files with 494 additions and 39 deletions

View File

@@ -22,6 +22,19 @@ const ids = {
session: '00000000-0000-4000-8000-000000000601',
plan: '00000000-0000-4000-8000-000000000701',
order: '00000000-0000-4000-8000-000000000801',
vocabularyUnit: '00000000-0000-4000-8000-000000000901',
vocabularyWord: '00000000-0000-4000-8000-000000000902',
handbookSubject: '00000000-0000-4000-8000-000000001001',
handbookChapter: '00000000-0000-4000-8000-000000001002',
handbookEntry: '00000000-0000-4000-8000-000000001003',
asset: '00000000-0000-4000-8000-000000001101',
video: '00000000-0000-4000-8000-000000001201',
school: '00000000-0000-4000-8000-000000001301',
major: '00000000-0000-4000-8000-000000001302',
scorelineField: '00000000-0000-4000-8000-000000001303',
scorelineRecord: '00000000-0000-4000-8000-000000001304',
notification: '00000000-0000-4000-8000-000000001401',
aiReport: '00000000-0000-4000-8000-000000001501',
};
const portals = [
@@ -210,7 +223,8 @@ function mockApiPayload(pathname, method, query, body) {
phone: '13800000000',
avatarPreset: 'male',
score: 120,
target: { regionId: ids.region, regionName: '天津' },
target: { regionId: ids.region, regionName: '天津', schoolId: ids.school, schoolName: '天津职业大学', majorId: ids.major, majorName: '软件工程' },
membership: { isSvip: true },
stats: { answers: { totalAnswered: 12 } },
},
};
@@ -218,7 +232,23 @@ function mockApiPayload(pathname, method, query, body) {
if (pathname === '/api/profile/score-events') return { items: [] };
if (pathname === '/api/profile/activity-tasks') return { items: [] };
if (pathname === '/api/profile/exchange-items') return { items: [] };
if (pathname === '/api/profile/notifications') return { items: [], summary: { unread: 0, read: 0, archived: 0 } };
if (pathname === '/api/profile/notifications') {
return {
items: [{
id: ids.notification,
notificationType: 'badge_granted',
status: query.get('status') || 'unread',
severity: 'success',
title: '入门勋章已发放',
message: '完成首次练习后,系统已自动发放坚持练习勋章。',
actionLabel: '查看个人中心',
actionPath: '/pages/student/profile/index',
createdAt: new Date().toISOString(),
}],
summary: { unread: 1, read: 0, archived: 0, dismissed: 0 },
};
}
if (pathname === '/api/profile/notifications/status') return { item: { updatedCount: body.notificationIds?.length || 1, status: body.status || 'read' } };
if (pathname === '/api/profile/badges') return { items: [{ id: 'badge-smoke', name: '坚持练习' }] };
if (pathname === '/api/profile/exam-countdowns') return { items: [] };
if (pathname === '/api/commerce/entitlements') return { items: [{ type: 'svip', status: 'active' }], summary: { svip: true } };
@@ -270,8 +300,21 @@ function mockApiPayload(pathname, method, query, body) {
}
if (pathname === '/api/learning/favorites/questions' && method === 'POST') return { ok: true, favorite: body.favorite !== false };
if (pathname === '/api/learning/favorites/questions') return { items: [question] };
if (pathname === '/api/learning/wrong-questions/review-plan') return { items: [], nextAction: {} };
if (pathname === '/api/learning/wrong-questions') return { items: [] };
if (pathname === '/api/learning/wrong-questions/review-plan') {
return {
items: [{
questionId: ids.question,
content: question.content,
type: question.type,
typeLabel: question.typeLabel,
wrongCount: 2,
lastWrongAt: new Date().toISOString(),
}],
nextAction: { mode: 'wrong_review', questionLimit: 30 },
};
}
if (pathname === '/api/learning/wrong-questions/resolve') return { ok: true };
if (pathname === '/api/learning/wrong-questions') return { items: [question] };
if (pathname === '/api/learning/stats') {
return {
item: {
@@ -288,6 +331,196 @@ function mockApiPayload(pathname, method, query, body) {
if (pathname === '/api/learning/trend') return { items: [{ date: '2026-07-01', answeredCount: 3, correctCount: 2, wrongCount: 1, accuracy: 0.66, sessionCount: 1, reportCount: 1, score: 66, totalScore: 100 }] };
if (pathname === '/api/learning/practice-sessions/history') return { items: [] };
if (pathname === '/api/catalog/vocabulary-units') {
return { items: [{ id: ids.vocabularyUnit, name: '英语核心词汇', description: '专升本高频词', wordCount: 1, regionId: ids.region }] };
}
if (pathname === '/api/catalog/vocabulary-words') {
return {
items: [{
id: ids.vocabularyWord,
wordId: ids.vocabularyWord,
unitId: ids.vocabularyUnit,
word: 'analysis',
phonetic: "ə'næləsɪs",
meaning: '分析',
example: 'Careful analysis helps students improve.',
exampleTranslation: '细致分析能帮助学生提升。',
status: 'new',
}],
};
}
if (pathname === '/api/learning/vocabulary/review-plan') {
return {
item: {
words: [{
id: ids.vocabularyWord,
wordId: ids.vocabularyWord,
unitId: ids.vocabularyUnit,
word: 'analysis',
phonetic: "ə'næləsɪs",
meaning: '分析',
example: 'Careful analysis helps students improve.',
exampleTranslation: '细致分析能帮助学生提升。',
dueLevel: 'new',
}],
dueCount: 1,
newCount: 1,
totalPlanned: 1,
},
};
}
if (pathname === '/api/learning/vocabulary/stats') {
return { item: { totalWords: 1, progressedWords: 0, masteredWords: 0, learningWords: 0, todayReviewed: 0, favoriteWords: 0 } };
}
if (pathname === '/api/learning/vocabulary/favorites' && method === 'POST') return { ok: true, favorite: body.favorite !== false };
if (pathname === '/api/learning/vocabulary/favorites') return { items: [] };
if (pathname === '/api/learning/vocabulary/review') return { item: { wordId: body.wordId || ids.vocabularyWord, result: body.result || 'known' } };
if (pathname === '/api/catalog/handbook-subjects') {
return { items: [{ id: ids.handbookSubject, name: '高等数学手册', type: 'handbook', description: '章节化知识点' }] };
}
if (pathname === '/api/catalog/handbook-chapters') {
return { items: [{ id: ids.handbookChapter, subjectId: ids.handbookSubject, name: '函数与极限', description: '基础章节' }] };
}
if (pathname === '/api/catalog/handbook-entries') {
return {
items: [{
id: ids.handbookEntry,
chapterId: ids.handbookChapter,
title: '导数定义',
summary: '导数描述函数在某一点附近的变化率。',
content: '设函数 y=f(x),当自变量增量趋近于 0 时,差商的极限称为导数。$f(x)=x^2$ 的导数是 $2x$。',
}],
};
}
if (pathname === '/api/catalog/assets') {
return {
items: [{
id: ids.asset,
title: '天津专升本英语真题 PDF',
assetType: 'pdf',
categoryLabel: '真题资料',
description: '用于验证私有资料短签名、水印和预览入口。',
fileName: 'tj-english-paper.pdf',
mimeType: 'application/pdf',
visibility: 'members',
previewStatus: 'ready',
}],
};
}
if (pathname === '/api/catalog/assets/preview') {
return {
item: { id: ids.asset, title: '天津专升本英语真题 PDF', assetType: 'pdf', fileName: 'tj-english-paper.pdf', mimeType: 'application/pdf', visibility: 'members' },
preview: { url: 'https://assets.example.test/tj-english-paper-preview.pdf?token=smoke', expiresInSec: 300, expiresAt: new Date(Date.now() + 300_000).toISOString(), signatureMode: 'signed' },
watermark: { mode: 'visible_overlay', required: true, text: '工学题库 SMOKE', traceId: 'wm-preview-smoke', position: 'diagonal', opacity: 0.16, repeat: true, renderHint: 'overlay' },
};
}
if (pathname === '/api/catalog/assets/download') {
return {
item: { id: ids.asset, title: '天津专升本英语真题 PDF', assetType: 'pdf', fileName: 'tj-english-paper.pdf', mimeType: 'application/pdf', visibility: 'members' },
download: { url: 'https://assets.example.test/tj-english-paper.pdf?token=smoke', expiresInSec: 300, expiresAt: new Date(Date.now() + 300_000).toISOString(), signatureMode: 'signed' },
watermark: { mode: 'visible_overlay', required: true, text: '工学题库 SMOKE', traceId: 'wm-download-smoke', position: 'diagonal', opacity: 0.16, repeat: true, renderHint: 'overlay' },
};
}
if (pathname === '/api/questions/videos') {
return {
videos: [{
id: ids.video,
questionId: query.get('questionId') || ids.question,
title: '本题视频解析',
description: '后端签名播放烟测视频',
duration: 180,
accessMode: 'svip',
playable: true,
}],
total: 1,
};
}
if (pathname === '/api/videos/play') {
return {
item: { id: body.videoId || ids.video, questionId: body.questionId || ids.question, title: '本题视频解析', duration: 180, accessMode: 'svip' },
playToken: 'video-play-token-smoke',
playback: { url: 'https://assets.example.test/video-smoke.mp4?token=smoke', expiresInSec: 300, expiresAt: new Date(Date.now() + 300_000).toISOString(), signatureMode: 'signed' },
watermark: { mode: 'visible_overlay', required: true, text: '工学题库 SMOKE', traceId: 'wm-video-smoke', position: 'diagonal', opacity: 0.16, repeat: true, renderHint: 'overlay' },
access: { accessMode: 'svip', consumedQuota: 0 },
};
}
if (pathname === '/api/scoreline/fields') {
return { items: [{ id: ids.scorelineField, regionId: ids.region, fieldKey: 'min_score', fieldName: '最低分', fieldType: 'number', unit: '分', isFilter: true, isVisible: true, isTrend: true, sortOrder: 1 }] };
}
if (pathname === '/api/scoreline/schools') {
return { items: [{ id: ids.school, regionId: ids.region, name: '天津职业大学', shortName: '天职大', isHot: true, order: 1 }] };
}
if (pathname === '/api/scoreline/majors') {
return { items: [{ id: ids.major, regionId: ids.region, schoolId: ids.school, name: '软件工程', order: 1, hasRestriction: false }] };
}
if (pathname === '/api/scoreline/years') return { items: [2026, 2025, 2024] };
if (pathname === '/api/scoreline/trend' || pathname === '/api/scoreline/records') {
return {
items: [{
id: ids.scorelineRecord,
regionId: ids.region,
schoolId: ids.school,
majorId: ids.major,
year: 2025,
schoolName: '天津职业大学',
majorName: '软件工程',
fieldValues: { min_score: 218 },
}],
total: 1,
};
}
if (pathname === '/api/ai/school-recommendations') {
return {
items: [{
id: ids.aiReport,
tenantId: ids.tenant,
userId: ids.user,
regionId: ids.region,
status: 'generated',
provider: 'local_rules',
promptVersion: 'smoke-v1',
inputPayload: { estimatedScore: 210, riskPreference: 'balanced' },
contextPayload: {},
resultPayload: {
schemaVersion: 'school-recommendation-report-v1',
summary: '建议优先关注天津职业大学软件工程方向。',
riskLevel: 'balanced',
recommendedSchools: [{
schoolId: ids.school,
schoolName: '天津职业大学',
majorId: ids.major,
majorName: '软件工程',
latestYear: 2025,
latestScore: 218,
averageScore: 214,
scoreGap: -8,
riskLevel: 'balanced',
confidence: 0.76,
reason: '与预估分接近,适合作为稳中带冲选择。',
tags: ['天津', '工科'],
}],
actionPlan: ['复盘公共课错题', '补齐英语词汇'],
disclaimers: ['烟测报告仅用于页面联调。'],
dataCoverage: { scorelines: 1 },
},
generatedAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}],
};
}
if (pathname === '/api/ai/school-recommendations/generate') {
return mockApiPayload('/api/ai/school-recommendations', 'GET', query, body);
}
if (pathname === '/api/ai/school-recommendations/export') {
return { item: { reportId: ids.aiReport, format: query.get('format') || 'markdown', fileName: 'school-report-smoke.md', mimeType: 'text/markdown', contentBase64: 'IyBTbW9rZQ==', contentText: '# Smoke', sha256: 'smoke', sizeBytes: 7 } };
}
if (pathname === '/api/tenant-admin/dashboard') return { item: tenantDashboard };
if (pathname === '/api/tenant-admin/overview') return { item: { slug: 'master', brandName: '工学题库商户后台' } };
if (pathname === '/api/tenant-admin/permissions') {
@@ -740,6 +973,12 @@ async function waitForApiRequest(api, path, method) {
}, 10_000);
}
async function navigateAndExpect(page, baseUrl, path, text, timeoutMs = 10_000) {
await page.navigate(`${baseUrl}${path}`);
await waitForPath(page, path.split('?')[0], timeoutMs);
await waitForText(page, text, timeoutMs);
}
async function runStudentJourney(browser, portal, api) {
const checks = [];
const page = await newPage(browser, `${portal.baseUrl}${portal.landingPath}`);
@@ -775,6 +1014,61 @@ async function runStudentJourney(browser, portal, api) {
await waitForPath(page, '/pages/student/checkout/index');
await waitForText(page, '会员收银台');
checks.push({ id: 'student.profile.to_checkout', status: 'pass', detail: await currentPath(page) });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/review/index?type=wrong', '错题本');
await waitForText(page, '开始复习');
await waitForText(page, '这是 H5 交互烟测题目');
await clickText(page, '收藏');
await waitForText(page, '收藏夹');
await clickText(page, '开始复习');
await waitForPath(page, '/pages/student/practice/index');
checks.push({ id: 'student.review.wrong_favorite', status: 'pass', detail: '错题本、收藏夹和后端组卷入口可打开' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/vocabulary/index', '背单词');
await waitForText(page, '学习概览');
await waitForText(page, 'analysis');
await clickText(page, '认识');
await waitForApiRequest(api, '/api/learning/vocabulary/review', 'POST');
checks.push({ id: 'student.vocabulary.review', status: 'pass', detail: '背单词计划和复习提交 API 已触发' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/handbook/index', '知识手册');
await waitForText(page, '高等数学手册');
await waitForText(page, '导数定义');
await waitForText(page, '变化率');
checks.push({ id: 'student.handbook.rendered', status: 'pass', detail: '手册科目、章节和知识点渲染正常' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/assets/index', '资料下载');
await waitForText(page, '天津专升本英语真题 PDF');
await clickText(page, '预览');
await waitForText(page, '追踪码');
await waitForApiRequest(api, '/api/catalog/assets/preview', 'GET');
await clickText(page, '下载');
await waitForText(page, '确认下载');
await waitForApiRequest(api, '/api/catalog/assets/download', 'GET');
checks.push({ id: 'student.assets.signed_watermark', status: 'pass', detail: '资料预览/下载短签名和水印面板可用' });
await navigateAndExpect(page, portal.baseUrl, `/pages/student/video/index?questionId=${ids.question}`, '视频解析');
await waitForText(page, '本题视频解析');
await clickText(page, '本题视频解析');
await waitForApiRequest(api, '/api/videos/play', 'POST');
checks.push({ id: 'student.video.play_auth', status: 'pass', detail: '视频列表和播放授权 API 已触发' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/scoreline/index', '历年分数线');
await waitForText(page, '天津职业大学');
await waitForText(page, '最低分');
checks.push({ id: 'student.scoreline.rendered', status: 'pass', detail: '分数线筛选、趋势和结果渲染正常' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/ai-school/index', 'AI择校推荐');
await waitForText(page, '推荐结果');
await waitForText(page, '天津职业大学');
checks.push({ id: 'student.ai_school.rendered', status: 'pass', detail: 'AI 择校报告列表和推荐结果可渲染' });
await navigateAndExpect(page, portal.baseUrl, '/pages/student/notifications/index', '消息中心');
await waitForText(page, '入门勋章已发放');
await clickText(page, '全部已读');
await waitForApiRequest(api, '/api/profile/notifications/status', 'POST');
checks.push({ id: 'student.notifications.status', status: 'pass', detail: '消息筛选和批量已读 API 已触发' });
return checks;
} finally {
page.close();
@@ -800,6 +1094,21 @@ async function runTenantJourney(browser, portal) {
await waitForPath(page, '/pages/tenant-admin/finance/index');
await waitForText(page, '财务运营');
checks.push({ id: 'tenant.workbench.to_finance', status: 'pass', detail: await currentPath(page) });
const moduleChecks = [
{ label: '数据看板', path: '/pages/tenant-admin/dashboard/index' },
{ label: '学生运营', path: '/pages/tenant-admin/students/index' },
{ label: '营销中心', path: '/pages/tenant-admin/marketing/index' },
{ label: '租户设置', path: '/pages/tenant-admin/settings/index' },
];
for (const item of moduleChecks) {
await page.navigate(`${portal.baseUrl}${portal.landingPath}`);
await waitForText(page, item.label);
await clickText(page, item.label);
await waitForPath(page, item.path);
await waitForText(page, item.label);
checks.push({ id: `tenant.workbench.to_${item.path.split('/').at(-2)}`, status: 'pass', detail: await currentPath(page) });
}
return checks;
} finally {
page.close();
@@ -825,6 +1134,19 @@ async function runPlatformJourney(browser, portal) {
await waitForPath(page, '/pages/platform-admin/billing/index');
await waitForText(page, '账务中心');
checks.push({ id: 'platform.workbench.to_billing', status: 'pass', detail: await currentPath(page) });
const moduleChecks = [
{ label: '公共题库', path: '/pages/platform-admin/question-banks/index', expected: '公共题库授权' },
{ label: '平台员工', path: '/pages/platform-admin/staff/index', expected: '平台员工' },
];
for (const item of moduleChecks) {
await page.navigate(`${portal.baseUrl}${portal.landingPath}`);
await waitForText(page, item.label);
await clickText(page, item.label);
await waitForPath(page, item.path);
await waitForText(page, item.expected);
checks.push({ id: `platform.workbench.to_${item.path.split('/').at(-2)}`, status: 'pass', detail: await currentPath(page) });
}
return checks;
} finally {
page.close();