Files
jiapuapp/scripts/check-audit-regressions.mjs
T

475 lines
30 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
const workspace = process.cwd()
const failures = []
const read = (filePath) => fs.readFileSync(path.join(workspace, filePath), 'utf8')
const expect = (condition, message) => {
if (!condition) failures.push(message)
}
const listPageSources = (directory = 'pages') => fs.readdirSync(path.join(workspace, directory), { withFileTypes: true })
.flatMap((entry) => {
const childPath = path.join(directory, entry.name)
return entry.isDirectory()
? listPageSources(childPath)
: entry.isFile() && entry.name.endsWith('.vue') ? [childPath] : []
})
const signIn = read('pages/auth/sign-in.vue')
const register = read('pages/auth/register.vue')
const routes = read('utils/navigation/routes.js')
const genealogyHome = read('pages/genealogy/my-genealogies.vue')
const invitationManager = read('components/genealogy/InvitationManager.vue')
const pedigree = read('pages/tree/pedigree.vue')
const treeOverview = read('pages/tree/overview.vue')
const moduleBackground = read('components/ModulePageBackground.vue')
const genealogyBackground = read('components/genealogy/PageBackground.vue')
const vipPage = read('pages/profile/vip.vue')
const promotionsPage = read('pages/profile/promotions.vue')
const helpPage = read('pages/profile/help.vue')
const editProfilePage = read('pages/profile/edit-profile.vue')
const genealogySettingsPage = read('pages/genealogy/settings.vue')
const genealogyCreatePage = read('pages/genealogy/create.vue')
const familyVideosPage = read('pages/family/videos.vue')
const articleEditorPage = read('pages/family/article-editor.vue')
const ceremonyEditorPage = read('pages/records/ceremony-editor.vue')
const ceremonyListPage = read('pages/records/ceremonies.vue')
const articleListPage = read('pages/family/articles.vue')
const personDocumentsPage = read('pages/records/person-documents.vue')
const personDocumentDialog = read('components/tree/PersonDocumentDialog.vue')
const earningsPage = read('pages/profile/earnings.vue')
const relativeRecordsPage = read('pages/records/relative-records.vue')
const growthJournalPage = read('pages/records/growth-journal.vue')
const messageCenterPage = read('pages/notification/message-center.vue')
const changePhonePage = read('pages/profile/change-phone.vue')
const changePasswordPage = read('pages/profile/change-password.vue')
const platformVideosPage = read('pages/family/platform-videos.vue')
const familyMediaContract = read('services/api/family-media-contract.js')
const dialogPages = [
'pages/tree/pedigree.vue',
'pages/family/album-detail.vue',
'pages/records/relative-records.vue',
'pages/genealogy/search.vue',
'pages/family/article-detail.vue',
'pages/notification/message-detail.vue',
'pages/notification/message-center.vue',
'pages/profile/earnings.vue',
]
const vipService = read('services/api/vip-service.js')
const siteContentService = read('services/api/site-content-service.js')
const requestClient = read('services/api/request-client.js')
const requestErrorMessage = read('services/api/request-error-message.js')
const memberDirectoryPage = read('pages/tree/member-directory.vue')
const peoplePage = read('pages/records/people.vue')
const idValidatedPages = [
'pages/tree/add-relative.vue',
'pages/tree/member-rank.vue',
'pages/tree/member-profile.vue',
'pages/tree/member-states.vue',
'pages/genealogy/generation-poems.vue',
'pages/genealogy/overview.vue',
'pages/records/person-detail.vue',
]
const memberRankPage = read('pages/tree/member-rank.vue')
const meritRecordsPage = read('pages/records/merit-records.vue')
const memoPage = read('pages/records/memos.vue')
const lifeRecordContract = read('services/api/life-record-contract.js')
const ceremonyDetailPage = read('pages/records/ceremony-detail.vue')
const relativeRecordEditorPage = read('pages/records/relative-record-editor.vue')
const requestNormalizers = read('services/api/request-normalizers.js')
const profileContract = read('services/api/profile-contract.js')
const genealogyContext = read('utils/genealogy/context.js')
const editMemberPage = read('pages/tree/edit-member.vue')
const addRelativePage = read('pages/tree/add-relative.vue')
const securityPage = read('pages/profile/security.vue')
const lineageService = read('services/api/lineage-service.js')
const lineageWriteContract = read('services/api/lineage-write-contract.js')
const lineagePersonContract = read('services/api/lineage-person-contract.js')
const feedDetailPage = read('pages/family/feed-detail.vue')
const articleDetailPage = read('pages/family/article-detail.vue')
const familyVideosPageSource = read('pages/family/videos.vue')
const genealogyContract = read('services/api/genealogy-contract.js')
const genealogySearchPage = read('pages/genealogy/search.vue')
const generationPoemService = read('services/api/generation-poem-service.js')
const genealogyMemberService = read('services/api/genealogy-member-service.js')
const lifeRecordService = read('services/api/life-record-service.js')
const personDocumentService = read('services/api/person-document-service.js')
const mediaUpload = read('utils/media-upload.js')
const businessFileContract = read('services/api/business-file-contract.js')
const authContract = read('services/api/auth-contract.js')
const authService = read('services/api/auth-service.js')
const contentRecoveryContract = read('services/api/content-password-recovery-contract.js')
const contentRecoveryService = read('services/api/content-password-recovery-service.js')
const referralContract = read('services/api/referral-contract.js')
const permissionContract = read('services/api/genealogy-permission-contract.js')
const genealogyCapabilityService = read('services/api/genealogy-capability-service.js')
const businessDictionaryContract = read('services/api/business-dictionary-contract.js')
const vipContract = read('services/api/vip-contract.js')
const tacLibrary = read('static/tac/js/tac.min.js')
const openApi = read('genealogy-app-openapi.yaml')
const manifest = JSON.parse(read('manifest.json'))
for (const pagePath of listPageSources()) {
expect(!/暂不支持|开发中|敬请期待|准备中/.test(read(pagePath)), `${pagePath} 仍展示占位功能提示`)
}
expect(manifest.name === '代代相传家谱', 'manifest 应用名称与正式产品名称不一致')
expect(!signIn.includes('协议页面准备中'), '登录页仍使用协议占位提示')
expect(!register.includes('协议页面准备中'), '注册页仍使用协议占位提示')
expect(signIn.includes('openComplianceDocument'), '登录页没有接入协议正文导航')
expect(register.includes('openComplianceDocument'), '注册页没有接入协议正文导航')
expect(
/getComplianceDocument[\s\S]*?authenticated:\s*false/.test(siteContentService),
'协议正文请求仍依赖登录状态',
)
const complianceRoute = routes.match(/M13: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
expect(complianceRoute.includes('"A01"'), '协议正文路由不允许从登录页进入')
expect(complianceRoute.includes('"A04"'), '协议正文路由不允许从注册页进入')
const membersRoute = routes.match(/G13: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
expect(membersRoute.includes('"G01"'), '成员页路由不允许从家谱首页进入')
expect(
/members:\s*\(\)\s*=>\s*openPage\("G13"/.test(genealogyHome),
'家谱首页“成员”没有直达成员列表',
)
const platformVideosRoute = routes.match(/F11: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
expect(platformVideosRoute.includes('"G01"'), '宣传视频路由不允许从家谱首页进入')
expect(platformVideosRoute.includes('"videoId"'), '宣传视频路由不能携带指定视频 ID')
expect(
familyMediaContract.includes("HOME_FEATURED: 'home_featured'") &&
familyMediaContract.includes("VIDEO_CENTER: 'video_center'") &&
familyMediaContract.includes("PROFILE_FEATURED: 'profile_featured'"),
'平台视频契约没有完整覆盖后端三个投放位',
)
expect(genealogyHome.includes('featured-media-grid'), '家谱首页没有宣传视频封面预览')
expect(
genealogyHome.includes('PLATFORM_VIDEO_PLACEMENT.HOME_FEATURED'),
'家谱首页没有读取首页推荐视频投放位',
)
expect(
platformVideosPage.includes('video-card__cover-button') &&
platformVideosPage.includes('requestedVideoId'),
'宣传视频列表没有按封面进入指定视频播放',
)
expect(
genealogyHome.includes('genealogyListError') &&
genealogyHome.includes('getRequestErrorMessage'),
'家谱首页仍会隐藏家谱列表的真实失败类型',
)
expect(
articleListPage.includes('articleCategoryError') &&
articleListPage.includes('getRequestErrorMessage'),
'谱文分类读取失败仍被静默伪装为空分类',
)
expect(
!/loadArticleCategories[\s\S]*?catch\s*\([^)]*\)[\s\S]*?return\s+\[\]/.test(articleListPage),
'谱文分类读取失败仍直接返回空数组',
)
expect(
invitationManager.includes('.invitation-manager__row .app-button'),
'邀请记录按钮没有独立收缩规则',
)
expect(
!/invitation-manager__row text:first-child\s*\{[^}]*overflow-wrap:\s*anywhere/.test(invitationManager),
'邀请记录名称仍允许逐字断行',
)
expect(pedigree.includes('isVerticalPedigreeText'), '世系表格没有区分中英文排版')
expect(pedigree.includes('member-node__name--horizontal'), '世系表格缺少英文姓名横排样式')
expect(!pedigree.includes('\\p{Script=Han}'), '世系表格仍使用 Android WebView 不兼容的正则')
expect(!treeOverview.includes('邀请绑定暂未开放'), '世系操作仍展示没有实现的占位入口')
expect(moduleBackground.includes('mode="aspectFill"'), '通用长背景仍按原图高度绘制')
expect(genealogyBackground.includes('mode="aspectFill"'), '家谱长背景仍按原图高度绘制')
expect(vipService.includes('async createVipOrder'), 'VIP service 缺少创建订单接口')
expect(vipService.includes('async getVipPaymentStatus'), 'VIP service 缺少支付状态查询接口')
expect(vipService.includes('async closeVipPayment'), 'VIP service 缺少关闭支付接口')
expect(vipPage.includes('uni.requestPayment'), 'VIP 页面没有接入 App 支付')
expect(vipPage.includes('createVipOrder'), 'VIP 页面没有调用创建订单接口')
expect(vipPage.includes('createNonIdempotentWriteGuard'), 'VIP 创建订单缺少重复下单保护')
expect(vipPage.includes('orderCreationGuard.recordFailure'), 'VIP 下单结果未知时仍允许重复购买')
expect(Boolean(manifest['app-plus']?.modules?.Payment), 'manifest 未启用 Payment 模块')
expect(Boolean(manifest['app-plus']?.modules?.Share), 'manifest 未启用推广页所需的 Share 模块')
expect(
/saveGenerationPoemBatch[\s\S]*?requireData:\s*false/.test(generationPoemService),
'字辈批量保存仍把 VoidResult 当作必须包含 data 的响应',
)
expect(
/transferGenealogyOwner[\s\S]*?requireData:\s*false/.test(genealogyMemberService),
'谱主转让仍把 VoidResult 当作必须包含 data 的响应',
)
expect(
/setGrowthRecordPassword[\s\S]*?requireData:\s*false/.test(lifeRecordService),
'成长记录密码设置仍把无 data 的成功响应当作失败',
)
expect(
/setPersonDocumentPassword[\s\S]*?requireData:\s*false/.test(personDocumentService),
'证件密码设置仍把 VoidResult 当作必须包含 data 的响应',
)
expect(mediaUpload.includes('IMAGE_TYPE_INVALID'), '图片上传缺少选择结果类型复核')
expect(mediaUpload.includes('readNativeFile(await pickNativeVideo(), "视频")'), '视频读取错误仍误称为图片错误')
expect(
businessFileContract.includes("!/^https:\\/\\/[^\\s]+$/.test(accessUrl)"),
'业务文件访问地址没有限制为 HTTPS',
)
expect(
authContract.includes("typeof token !== 'string' || !token || token.trim() !== token"),
'登录响应没有严格校验 access_token',
)
expect(!tacLibrary.includes('res.code'), '行为验证码 SDK 异常分支仍引用未定义的 res.code')
expect(!tacLibrary.includes('validFail(res'), '行为验证码 SDK 异常分支仍传递未定义的 res')
expect(
manifest['app-plus']?.distribute?.android?.permissions?.some((permission) =>
permission.includes('android.permission.READ_MEDIA_VIDEO')),
'Android 视频相册选择缺少 READ_MEDIA_VIDEO 权限',
)
expect(
manifest['app-plus']?.distribute?.android?.permissions?.some((permission) =>
permission.includes('android.permission.READ_EXTERNAL_STORAGE') &&
permission.includes('android:maxSdkVersion="32"')),
'Android 12L 及以下相册选择缺少受限的存储读取权限',
)
expect(
manifest['app-plus']?.distribute?.ios?.privacyDescription?.NSPhotoLibraryUsageDescription,
'iOS 相册选择缺少 NSPhotoLibraryUsageDescription 用途说明',
)
expect(
/promotion-card__actions[^>]*@click\.stop/.test(promotionsPage),
'推广操作按钮会继续触发卡片跳转',
)
expect(
/\.help-article-list\s*\{[^}]*margin-top:\s*18rpx/.test(helpPage),
'帮助中心分类栏与首条问答之间缺少间距',
)
expect(
/\.help-feedback-card text\s*\{[^}]*display:\s*block/.test(helpPage),
'帮助中心反馈标题和说明仍挤在同一行',
)
expect(editProfilePage.includes('createDiscardConfirmation'), '编辑资料页缺少未保存修改确认')
expect(editProfilePage.includes('onBackPress'), '编辑资料页系统返回未接入返回守卫')
expect(editProfilePage.includes('dirty: isDirty.value'), '编辑资料页返回守卫未检查完整资料快照')
expect(genealogySettingsPage.includes('createDiscardConfirmation'), '家谱设置页缺少未保存修改确认')
expect(genealogySettingsPage.includes('onBackPress'), '家谱设置页系统返回未接入返回守卫')
expect(genealogySettingsPage.includes('if (isDirty.value'), '家谱设置页返回守卫未检查设置快照')
expect(familyVideosPage.includes('createDiscardConfirmation'), '家族视频表单缺少未保存修改确认')
expect(familyVideosPage.includes('onBackPress'), '家族视频页系统返回未接入返回守卫')
expect(familyVideosPage.includes('formSnapshot.value !== formBaseline.value'), '家族视频表单返回未检查视频草稿')
expect(!familyVideosPage.includes('video-card__player'), '家族视频列表仍直接铺设播放器')
expect(familyVideosPage.includes('video-card__cover-action'), '家族视频列表没有封面点击播放入口')
expect(ceremonyListPage.includes('item.coverFile?.accessUrl'), '礼仪列表没有消费封面字段')
expect(ceremonyDetailPage.includes('detail.coverFile?.accessUrl'), '礼仪详情没有显示封面')
expect(articleListPage.includes('item.coverFile?.accessUrl'), '谱文列表没有消费封面字段')
expect(articleDetailPage.includes('article.coverFile?.accessUrl'), '谱文详情没有显示封面')
expect(routes.includes('path: "/pages/records/person-documents"'), '缺少家谱级重要证件路由')
expect(personDocumentsPage.includes('<PersonDocumentDialog'), '家谱级重要证件页没有复用证件工作流')
expect(personDocumentDialog.includes('const documentQuery = props.personId'), '重要证件弹窗不支持家谱级聚合查询')
expect(vipContract.includes("orderNo: normalizeVipText(item.orderNo, 'orderNo')"), 'VIP 订单契约没有保留订单号')
expect(vipPage.includes('订单号:{{ item.orderNo }}'), 'VIP 订单页没有显示订单号')
expect(vipPage.includes('支付时间:{{ item.paidAt }}') && vipPage.includes('到期时间:{{ item.expiresAt }}'), 'VIP 订单页没有区分支付和到期时间')
for (const field of ['withdrawalNo', 'auditRemark', 'payoutReference', 'paidAt']) {
expect(earningsPage.includes(`withdrawal.${field}`), `提现记录没有显示 ${field}`)
}
expect(relativeRecordsPage.includes('item.mediaFiles?.[0]?.accessUrl'), '贺礼簿列表没有显示首图')
expect(growthJournalPage.includes('field !== "lineagePersonId"'), '成长记录仍把自动带入人物直接判为用户修改')
const joinApplicationRoute = routes.match(/G08: defineRoute\(\{[\s\S]*?^\s{2}\}\),/m)?.[0] || ''
expect(joinApplicationRoute.includes('"genealogyName"'), '公开家谱申请页路由仍拒绝谱名参数')
expect(messageCenterPage.includes(':active="sourceTab"') && messageCenterPage.includes('returnToSource'), '消息中心没有按真实来源恢复导航')
expect(changePhonePage.includes('请先获取新手机号的验证码'), '换绑手机号仍错误提示当前手机号验证码')
expect(!changePhonePage.includes('请先获取当前手机号的验证码'), '换绑手机号保留了错误的验证码归属文案')
expect(changePhonePage.includes('role="alert"') && changePhonePage.includes(':aria-describedby'), '换绑手机号字段错误缺少无障碍关联')
expect(changePasswordPage.includes('<button') && changePasswordPage.includes(':aria-pressed'), '密码显示开关缺少按钮和状态语义')
expect(changePasswordPage.includes('role="alert"') && changePasswordPage.includes(':aria-describedby'), '修改密码字段错误缺少无障碍关联')
expect(
genealogySettingsPage.indexOf('pageState.value = "form"') < genealogySettingsPage.indexOf('getPermanentDeletionCapability'),
'家谱设置仍被永久注销资格读取阻断',
)
for (const dialogPagePath of dialogPages) {
expect(read(dialogPagePath).includes('onBackPress'), `${dialogPagePath} 的弹窗未接入系统返回`)
}
const iconPaths = []
const collectStrings = (value) => {
if (typeof value === 'string') iconPaths.push(value)
else if (value && typeof value === 'object') Object.values(value).forEach(collectStrings)
}
collectStrings(manifest['app-plus']?.distribute?.icons)
const uniqueIconPaths = [...new Set(iconPaths)]
expect(uniqueIconPaths.length === 17, `App 图标路径数量异常:${uniqueIconPaths.length}`)
for (const iconPath of uniqueIconPaths) {
expect(fs.existsSync(path.join(workspace, iconPath)), `App 图标不存在:${iconPath}`)
}
const privacyPath = path.join(workspace, 'androidPrivacy.json')
expect(fs.existsSync(privacyPath), '缺少 androidPrivacy.json')
if (fs.existsSync(privacyPath)) {
const privacy = JSON.parse(fs.readFileSync(privacyPath, 'utf8'))
expect(privacy.prompt === 'template', 'Android 原生隐私提示未使用 template 模式')
expect(/https:\/\//.test(privacy.message || ''), 'Android 原生隐私提示没有 HTTPS 协议链接')
expect(typeof privacy.backToExit === 'boolean', 'Android 原生隐私提示 backToExit 必须是布尔值')
}
expect(!read('App.vue').includes("console.log('家谱 App 已启动')"), 'App.vue 仍保留启动调试日志')
expect(
/HTTP_ERROR'[\s\S]*?httpStatus\) === 401/.test(requestClient),
'请求层没有把 HTTP 401 识别为会话失效',
)
expect(
/statusCode < 200[\s\S]*?recoverExpiredAuthenticatedSession/.test(requestClient),
'普通请求收到 HTTP 401 时没有恢复登录状态',
)
expect(
/expectedStatus !== null[\s\S]*?recoverExpiredAuthenticatedSession/.test(requestClient),
'严格请求收到 HTTP 401 时没有恢复登录状态',
)
expect(requestClient.includes('const FILE_UPLOAD_TIMEOUT_MS = 120000'), '文件上传没有独立的超时配置')
expect(
(requestClient.match(/timeout: FILE_UPLOAD_TIMEOUT_MS|}, FILE_UPLOAD_TIMEOUT_MS\)/g) || []).length === 2,
'原生和浏览器文件上传没有统一使用独立超时配置',
)
expect(
requestClient.includes('rejectAuthenticatedUploadStatus'),
'文件上传收到 HTTP 401 时没有恢复登录状态',
)
expect(
/unwrapAuthenticatedUploadResponse[\s\S]*?isAuthenticatedSessionRejected[\s\S]*?recoverExpiredAuthenticatedSession/.test(requestClient),
'文件上传收到业务 401 时没有恢复登录状态',
)
expect(
/HTTP_ERROR'[\s\S]*?httpStatus\) === 403/.test(requestErrorMessage),
'HTTP 403 没有使用无权限提示',
)
for (const [pageName, pageSource] of [
['成员目录', memberDirectoryPage],
['人物列表', peoplePage],
]) {
expect(pageSource.includes('const requestedPage = append ? pageNum.value + 1 : 1'), `${pageName} 没有延迟提交分页页码`)
expect(!pageSource.includes('pageNum.value += 1'), `${pageName} 仍会在请求成功前递增页码`)
expect(pageSource.includes('if (append) loadMoreError.value = true'), `${pageName} 加载更多失败会破坏现有列表`)
expect(pageSource.includes('/^[1-9]\\d*$/.test(genealogyId.value)'), `${pageName} 没有校验家谱参数`)
}
for (const pagePath of idValidatedPages) {
expect(read(pagePath).includes('/^[1-9]\\d*$/.test(genealogyId.value)'), `${pagePath} 没有校验家谱 ID`)
}
expect(memberRankPage.includes('createDiscardConfirmation'), '成员排行页缺少未保存修改确认')
expect(memberRankPage.includes('dirty: isDirty.value'), '成员排行页返回守卫未检查排序修改')
expect(memberRankPage.includes('sortOrderBaseline.value = normalizedSortOrder'), '成员排行保存成功后没有更新草稿基准')
expect(editProfilePage.includes('original[field] = payload[field]'), '编辑资料保存成功后没有更新草稿基准')
expect(meritRecordsPage.includes('/^(?:0|[1-9]\\d{0,9})(?:\\.\\d{1,2})?$/'), '功德记录页面没有限制金额精度和范围')
expect(lifeRecordContract.includes("normalizeOptionalCurrencyNumber(payload.amount, '功德金额')"), '功德记录 API 契约没有限制金额精度和范围')
expect(requestNormalizers.includes('normalizeOptionalCurrencyNumber'), '请求契约缺少统一金额校验')
expect(ceremonyDetailPage.includes('献礼金额应为 0 至 9999999999.99'), '礼仪献礼页面没有限制金额精度和范围')
expect(relativeRecordEditorPage.includes('礼金金额应为 0 至 9999999999.99'), '亲友往来页面没有限制金额精度和范围')
expect(profileContract.includes('const isCalendarDate'), '个人资料契约没有验证真实日期')
expect(profileContract.includes("birthday && !isCalendarDate(birthday.slice(0, 10))"), '个人资料响应没有验证生日')
expect(genealogyContext.includes("/^[1-9]\\d*$/.test(genealogyId)"), '家谱上下文允许缓存无效家谱 ID')
expect(
editMemberPage.match(/\/\^\[1-9\]\\d\*\$\/.test/g)?.length >= 4,
'修改成员页没有完整校验家谱和人物 ID',
)
expect(
/v-if="feed\.canDelete"[\s\S]*?label="删除动态"/.test(feedDetailPage),
'动态详情向无删除权限用户显示删除按钮',
)
expect(
articleDetailPage.includes('article.canEdit || article.canDelete || article.canManageProtection'),
'谱文仅有内容密码管理权限时不显示密码操作',
)
expect(
/v-if="video\.canDelete"[\s\S]*?:label="deletingVideoId/.test(familyVideosPageSource),
'视频列表向无删除权限用户显示删除按钮',
)
expect(genealogyContract.includes('hasMembership: item.canManage === true || Boolean(memberStatus)'), '公开家谱契约用管理权限代替成员关系')
expect(genealogySearchPage.includes(':disabled="item.hasMembership"'), '公开家谱对普通成员仍显示申请加入')
expect(treeOverview.includes('/^[1-9]\\d*$/.test(genealogyId.value)'), '树状图没有校验家谱 ID')
expect(pedigree.includes('/^[1-9]\\d*$/.test(genealogyId.value)'), '世系谱没有校验家谱 ID')
const articleCategoryContract = openApi.slice(
openApi.indexOf(' /genealogy/app/genealogies/{genealogyId}/article-categories:'),
openApi.indexOf(' /genealogy/app/genealogies/{genealogyId}/articles:'),
)
expect(articleCategoryContract.includes('AppArticleCategoryResult'), '谱文分类写接口没有返回明确的分类对象')
expect(!articleCategoryContract.includes('PaymentOrderVo'), '谱文分类接口仍错误引用支付下单模型')
expect(/loginWithWechat[\s\S]*?data:\s*\{\s*code:/.test(authService), '微信登录没有按最新后端契约只提交一次性 code')
expect(authService.includes('ACCOUNT_BINDING_REQUIRED'), '微信登录没有处理后端要求的账号绑定状态')
expect(securityPage.includes('authApi.bindWechat'), '账号与安全页没有提供微信绑定入口')
expect(contentRecoveryService.includes('/capability`'), '内容密码找回能力仍调用旧路径')
expect(/resetPassword[\s\S]*?method:\s*'POST'/.test(contentRecoveryService), '内容密码重置仍使用旧 HTTP 方法')
expect(/sendCode[\s\S]*?requireData:\s*false/.test(contentRecoveryService), '内容密码验证码发送仍要求后端返回旧版 delivery 对象')
expect(contentRecoveryContract.includes('value.available'), '内容密码找回能力仍读取旧版 enabled 字段')
expect(contentRecoveryContract.includes('value.mobileMasked'), '内容密码找回能力仍读取旧版 maskedPhone 字段')
expect(referralContract.includes('referredUserCount'), '推广资料没有接入后端返回的推荐人数')
expect(referralContract.includes('value.shareUrl'), '推广资料没有读取后端提供的 HTTPS 分享链接')
expect(promotionsPage.includes('profile.shareUrl'), '推广中心分享内容没有使用后端提供的分享链接')
expect(profileContract.includes('currentPassword: payload.currentPasswordHash.toLowerCase()'), '手机号换绑没有提交当前密码摘要')
expect(changePhonePage.includes('calcMD5(currentPassword.value)'), '手机号换绑没有按现有认证契约处理当前密码')
expect(changePhonePage.includes('session.clear()') && changePhonePage.includes('goRoot("A01")'), '手机号换绑成功后没有清理登录态并返回登录页')
expect(changePasswordPage.includes('session.clear()') && changePasswordPage.includes('goRoot("A01")'), '密码修改成功后没有清理已失效登录态')
expect(genealogyContract.includes("'requestId'") && genealogyContract.includes("'ownerIsFirstAncestor'"), '建谱契约缺少幂等请求号或谱主始迁祖标记')
expect(genealogyCreatePage.includes('genealogyCreateRequestId') && genealogyCreatePage.includes('ownerIsFirstAncestor'), '建谱页面没有提交稳定请求号或谱主始迁祖选择')
expect(genealogyContract.includes('normalizedPayload.coverOssId = null'), '家谱封面契约不能发送 null 清空')
for (const [pageSource, pageName] of [[genealogySettingsPage, '家谱'], [articleEditorPage, '谱文'], [ceremonyEditorPage, '礼仪'], [familyVideosPage, '视频']]) {
expect(pageSource.includes('移除封面'), `${pageName}编辑页没有提供移除封面操作`)
}
expect(lifeRecordContract.includes("BENEFACTOR: 'benefactor'"), '备忘契约缺少家族恩人类型')
expect(memoPage.includes('memoType: memoType.value'), '家族恩人页面没有提交 benefactor 类型')
expect(memoPage.includes('memo.memoType === memoType.value'), '家族恩人列表没有按类型过滤')
for (const contractSource of [lifeRecordContract, familyMediaContract, read('services/api/ceremony-contract.js')]) {
expect(contractSource.includes('createTime:'), '资源响应契约没有保留后端 createTime')
}
expect(permissionContract.includes('item.code'), '权限目录仍读取旧版 permissionCode 字段')
expect(permissionContract.includes('item.groupName'), '权限目录没有接入后端权限分组')
expect(genealogyCapabilityService.includes('/comments/page`'), '家族视频根评论仍调用旧列表路径')
expect(genealogyCapabilityService.includes('/replies/page`'), '家族视频回复仍调用旧列表路径')
expect(/getVideoComments[\s\S]*?assertPage/.test(genealogyCapabilityService), '家族视频评论没有读取后端分页响应')
expect(genealogyContract.includes('genealogyName: payload.confirmationName.trim()'), '家谱永久删除仍提交旧版 confirmationName 字段')
expect(/getPermanentDeletionCapability[\s\S]*?permanent-deletion\/capability/.test(vipService + genealogyCapabilityService + read('services/api/genealogy-service.js')), '家谱设置没有接入永久删除能力接口')
expect(vipContract.includes('item.method'), 'VIP 支付能力没有读取后端 VO 的 method 字段')
expect(!vipContract.includes('item.paymentMethod'), 'VIP 支付能力仍读取与后端 VO 不一致的 paymentMethod 字段')
expect(vipContract.includes('value.orderString'), '支付宝下单仍读取旧版 alipayOrderInfo 字段')
expect(vipContract.includes('value.completed !== true'), '余额支付没有校验后端 completed 字段')
expect(/createVipOrder[\s\S]*?requestId/.test(vipService), 'VIP 下单没有提交后端必填幂等 requestId')
for (const dictionaryType of [
'gen_parent_relationship_variant',
'gen_education_type',
'gen_death_expression',
'gen_spouse_relationship_variant',
'gen_ceremony_type',
'gen_person_document_type',
'gen_growth_record_type',
'gen_merit_type',
'gen_feedback_type',
'gen_zodiac',
]) {
expect(businessDictionaryContract.includes(`'${dictionaryType}'`), `前端业务字典白名单缺少 ${dictionaryType}`)
}
for (const latestLineageField of ['zodiacCode', 'educationCode', 'deathExpressionCode', 'relationVariantCode']) {
expect(lineageWriteContract.includes(`'${latestLineageField}'`), `成员写契约缺少最新字段 ${latestLineageField}`)
}
expect(!lineageWriteContract.includes("'hereditaryMedicalHistory'"), '成员主档仍混入遗传病史敏感字段')
expect(lineageService.includes('/sensitive-profile`'), '成员敏感健康资料没有接入独立接口')
expect(lineagePersonContract.includes('value.zodiacCode'), '成员详情仍读取旧版生肖字段')
expect(lineagePersonContract.includes('value.educationCode'), '成员详情仍读取旧版学历字段')
expect(lineagePersonContract.includes('value.deathExpressionCode'), '成员详情仍读取旧版逝世表述字段')
for (const pageSource of [addRelativePage, editMemberPage]) {
expect(pageSource.includes('gen_zodiac'), '成员表单没有读取生肖动态字典')
expect(pageSource.includes('gen_education_type'), '成员表单没有读取学历动态字典')
expect(pageSource.includes('gen_death_expression'), '成员表单没有读取逝世表述动态字典')
expect(pageSource.includes('dictionaryRequestControllers'), '成员表单的并发字典请求仍共用一个取消控制器')
}
expect(
genealogySettingsPage.includes('deletionCapabilityReadRequestController'),
'家谱设置和永久删除能力的并发读取仍共用一个取消控制器',
)
if (failures.length) {
console.error(`AUDIT REGRESSION CHECK FAILED (${failures.length})`)
failures.forEach((failure) => console.error(`- ${failure}`))
process.exit(1)
}
console.log(`AUDIT REGRESSION CHECK PASS icons=${uniqueIconPaths.length}`)