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(' { 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}`)