import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import path from 'node:path' import { parse as parseSfc } from '@vue/compiler-sfc' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const sourceRoot = path.resolve(scriptDir, '../src') const [detailSource, listSource, routerSource, apiSource, typesSource] = await Promise.all([ readFile(path.join(sourceRoot, 'views/data-process/DataProcessDetailView.vue'), 'utf8'), readFile(path.join(sourceRoot, 'views/data-process/DataProcessListView.vue'), 'utf8'), readFile(path.join(sourceRoot, 'router/index.ts'), 'utf8'), readFile(path.join(sourceRoot, 'api/modules/dataProcess.ts'), 'utf8'), readFile(path.join(sourceRoot, 'types/dataProcess.ts'), 'utf8'), ]) const { descriptor, errors } = parseSfc(detailSource, { filename: 'DataProcessDetailView.vue' }) assert.equal(errors.length, 0, `数据处理详情页无法解析:${errors[0]}`) assert.ok(descriptor.template?.content.trim(), '数据处理详情页缺少可渲染模板') assert.match(routerSource, /path:\s*['"]data-process\/:id['"]/, '缺少数据处理详情动态路由') assert.match(routerSource, /name:\s*['"]data-process-detail['"]/, '数据处理详情路由缺少名称') assert.match(routerSource, /DataProcessDetailView\.vue/, '数据处理详情路由未加载详情页面') assert.match(routerSource, /title:\s*['"]数据处理详情['"]/, '数据处理详情路由标题不正确') assert.match(routerSource, /path:\s*['"]data-process\/:id\/regenerate['"][\s\S]*?name:\s*['"]data-process-regenerate['"][\s\S]*?DataProcessCreateView\.vue/, '重新生成路由未复用创建向导') assert.match(routerSource, /path:\s*['"]data-process\/:id\/workflow['"][\s\S]*?name:\s*['"]data-process-workflow['"][\s\S]*?DataProcessCreateView\.vue/, '未确认任务缺少工作流恢复路由') assert.match(listSource, /'data-process-detail'[\s\S]*?'data-process-workflow'/, '列表详情按钮未按确认状态分流') assert.match(listSource, /params:\s*\{\s*id:\s*task\.id\s*\}/, '列表详情按钮未传递任务 ID') assert.doesNotMatch(listSource, /查看详情功能开发中/, '详情按钮仍保留开发中提示') for (const requiredCopy of [ '处理耗时', '开始时间', '完成时间', '输入数据', '输出结果', '处理统计', '处理配置', '结果明细', ]) { assert.match(detailSource, new RegExp(requiredCopy), `详情页缺少必要信息:${requiredCopy}`) } assert.match(typesSource, /export type DataProcessStatus = 'pending' \| 'running' \| 'completed' \| 'failed' \| 'stopped'/, '任务状态契约不完整') assert.match(typesSource, /workflow_step\?: DataProcessWorkflowStep/, '任务详情契约缺少向导恢复步骤') assert.match(typesSource, /results_confirmed\?: boolean/, '任务详情契约缺少第六步确认标记') assert.match(detailSource, /getDataProcessTask\(taskId\.value\)/, '详情页没有通过真实 API 加载任务') assert.match(detailSource, /task\.status !== 'completed' \|\| task\.results_confirmed === false[\s\S]*?name: 'data-process-workflow'/, '未确认任务可绕过第六步直接进入最终详情') assert.match( detailSource, /if \(task\.status !== 'completed' \|\| task\.results_confirmed === false\) \{[\s\S]*?router\.replace\(\{ name: 'data-process-workflow'[\s\S]*?return[\s\S]*?\}[\s\S]*?detail\.value = task/, '未完成第六步确认时必须在赋值正式详情前终止加载', ) assert.match(detailSource, /getDataProcessResults\(taskId\.value,\s*\{[\s\S]*?page:[\s\S]*?page_size:[\s\S]*?keyword:[\s\S]*?status:/, '结果列表没有接入服务端分页、搜索和状态筛选') assert.match(detailSource, /getDataProcessProgress\(taskId\.value\)/, '运行中任务没有查询真实进度') assert.match(detailSource, /usePolling\(refreshRuntime,\s*3000/, '运行中任务没有启用进度轮询') assert.match(detailSource, /:data="results"/, '结果表格未绑定服务端结果数据') assert.match(detailSource, /v-model="keyword"/, '结果明细缺少搜索能力') assert.match(detailSource, /v-model="statusFilter"/, '结果明细缺少状态筛选') assert.match(detailSource, /:tooltip-options="resultCellTooltipOptions"/, '结果明细没有配置受控的长文本提示') assert.match(detailSource, /\.data-process-result-tooltip[\s\S]*?max-width:\s*min\(520px,\s*calc\(100vw - 32px\)\)/, '结果明细长文本提示没有限制最大宽度') assert.match(detailSource, /已发布数据请前往数据集详情编辑/, '结果明细没有说明已发布数据的编辑入口') assert.match(detailSource, / outputDatasets\.value\.length > 0\)/, '详情页没有独立识别仍然存在的已发布数据集') assert.match(detailSource, /const outputDatasets = computed\(\(\) => \{[\s\S]*?return detail\.value\?\.output_datasets \|\| \[\][\s\S]*?\}\)/, '详情页不应用输出指针伪造训练集') assert.doesNotMatch(detailSource, /name: detail\.value\.output_dataset_name[\s\S]*?type: 'train'/, '详情页仍在伪造训练集') assert.match(detailSource, /const hasCurrentPublishedDataset = computed\(\(\) => \([\s\S]*?outputDatasetId\.value[\s\S]*?outputDatasets\.value\.some[\s\S]*?String\(dataset\.id\) === String\(outputDatasetId\.value\)/, '当前轮发布状态没有校验指针对应的真实数据集') assert.match(detailSource, /dataProcessDisplayStatus\(\{[\s\S]*?output_dataset_id: hasCurrentPublishedDataset\.value \? outputDatasetId\.value : null/, '详情六态没有使用经过校验的当前发布指针') assert.match(detailSource, /v-if="hasPublishedOutputs && !hasCurrentPublishedDataset"[\s\S]*?当前轮尚未发布,以下为上次已发布数据集,仍可正常使用/, '重新生成或指针异常任务没有说明上次发布数据集仍然可用') assert.match(detailSource, /已发布数据集不可用,请重新发布/, '已发布指针失效时没有可恢复提示') assert.match(detailSource, /class="output-dataset-row"[\s\S]*?class="output-dataset-cell"[\s\S]*?class="output-dataset-links"/, '输出数据集没有独立的多行对齐布局') assert.match(detailSource, /\.output-dataset-links[\s\S]*?display:\s*inline-grid[\s\S]*?grid-template-columns:\s*max-content[\s\S]*?text-align:\s*left/, '输出数据集名称没有统一左边缘') assert.match(detailSource, /\.output-dataset-links[\s\S]*?\.el-button[\s\S]*?width:\s*100%[\s\S]*?margin-left:\s*0/, '输出数据集链接没有清除默认间距或统一宽度') assert.match(detailSource, /\.el-button\s*>\s*span[\s\S]*?width:\s*100%[\s\S]*?display:\s*flex/, '输出数据集按钮内容没有撑满统一宽度') assert.match(detailSource, /\.el-button i[\s\S]*?margin-left:\s*auto/, '输出数据集跳转图标没有统一右对齐') assert.match(detailSource, /将发布三个独立数据集/, '发布说明仍未明确生成三个独立数据集') assert.match(detailSource, /function startRegeneration\(\)[\s\S]*?name: 'data-process-regenerate'[\s\S]*?params: \{ id: taskId\.value \}/, '重新生成按钮没有携带原任务 ID 进入命名路由') assert.match(detailSource, /const canRepeatGeneration = computed[\s\S]*?status === 'completed'[\s\S]*?results_confirmed !== false[\s\S]*?previewCount\.value > 0/, '已完成任务缺少再次生成资格判断') assert.match(detailSource, /repeatDataProcessTask\(taskId\.value,[\s\S]*?expected_updated_at: detail\.value\.updated_at[\s\S]*?request_id: repeatRequestId\.value/, '再次生成没有携带源任务版本和幂等请求 ID') assert.match(detailSource, /name: 'data-process-workflow'[\s\S]*?params: \{ id: repeated\.task\.id \}/, '再次生成成功后没有进入新任务工作流') assert.match(detailSource, /原任务和原结果不会被修改/, '再次生成确认提示没有说明原任务保持不变') assert.match(detailSource, /v-if="canRepeatGeneration"[\s\S]*?@click="repeatGeneration"[\s\S]*?按原配置再生成一批/, '已完成任务详情缺少再次生成新批次入口') assert.match(detailSource, /const canRegenerate = computed\(\(\) => \{[\s\S]*?status === 'pending'[\s\S]*?status === 'failed'[\s\S]*?status === 'stopped'[\s\S]*?status === 'completed'[\s\S]*?outputDatasetId\.value[\s\S]*?hasPublishedOutputs\.value/, '详情页没有覆盖指针已清空但旧发布数据集仍存在的重新生成任务') assert.match(detailSource, /v-if="canRegenerate"[\s\S]*?@click="startRegeneration"[\s\S]*?重新生成/, '可恢复任务没有收敛为单一重新生成入口') assert.match(detailSource, /v-if="detail\.status === 'completed' && !hasCurrentPublishedDataset"[\s\S]*?@click="openPublishDialog"[\s\S]*?发布为三个数据集/, '未发布或发布指针失效的完成任务没有保留发布入口') assert.doesNotMatch(detailSource, /查看三个数据集|重新同步数据集|resyncPublishedDataset/, '已发布任务仍保留旧查看或同步按钮') assert.doesNotMatch(detailSource, /label="数据集类型"/, '发布三个数据集时不应再选择单一数据集类型') assert.match(detailSource, /无法加载数据处理任务/, '未知任务或加载失败缺少明确错误状态') assert.match(detailSource, /结果明细加载失败/, '结果加载失败缺少明确错误状态') assert.match(detailSource, /width:\s*100%/, '详情页没有铺满内容区域') assert.doesNotMatch(detailSource, /^\s*max-width:\s*\d+px/m, '详情页不应使用固定最大宽度') assert.match(detailSource, /!\/\(\?:password\|secret\|token\|api_key\)\/i\.test\(key\)/, '处理配置没有过滤敏感凭据字段') assert.match(detailSource, /key !== 'generation_model_snapshot'/, '处理配置仍直接展示内部模型快照') assert.match(detailSource, /preprocessOptionLabelMap/, '处理配置没有把预处理内部枚举转换为中文') assert.match(detailSource, /output_type:\s*'输出类型'/, '处理配置没有显示输出类型名称') assert.match(detailSource, /value === 'reasoning' \? '思维链回答' : value === 'dpo' \? 'DPO 偏好对' : '标准回答'/, '处理配置没有转换输出类型枚举') assert.match(detailSource, /prop="chosen"[\s\S]*?prop="rejected"/, 'DPO 结果没有展示 Chosen 与 Rejected') assert.match(detailSource, /publishForm\.format = isDpoOutput\.value \? 'dpo'/, 'DPO 发布没有锁定原生格式') assert.match(detailSource, /reasoning_detail:\s*'推理详细程度'/, '处理配置没有显示推理详细程度名称') assert.match(detailSource, /value === 'detailed' \? '详细推理' : '普通推理'/, '处理配置没有转换推理详细程度枚举') assert.match(detailSource, /key !== 'reasoning_detail' \|\| config\.output_type === 'reasoning'/, '标准回答任务不应展示无关的推理详细程度') assert.match(detailSource, /const parsed = typeof value === 'number' \? value : Number\(value\)/, '详情页不兼容 PostgreSQL 数字字符串') assert.match(detailSource, /new Date\(startTime\.value\)\.getTime\(\)/, '详情页没有在后端耗时缺失时按开始、完成时间回算') assert.match(detailSource, /outputCount \+ numeric\(detail\.value\?\.filtered_count\)/, '结果保留率没有使用输出和过滤结果的同口径分母') assert.doesNotMatch(detailSource, /numeric\(detail\.value\?\.output_count\) \/ inputCount/, '结果保留率仍在用输出结果除以源文件记录数') assert.match(detailSource, /:percentage="retentionPercentage"/, '结果保留率进度条仍绑定任务执行进度') assert.match(detailSource, /preview_count/, '非结构化任务没有展示实际输入切片数') assert.match(detailSource, /import \{ dataProcessDisplayStatus \} from '@\/utils\/dataProcessStatus\.js'/, '详情页没有复用数据处理六态映射') assert.doesNotMatch(detailSource, /ModelStatusTag/, '详情页仍在使用无法区分生成与发布的通用状态组件') assert.match(detailSource, /生成结果<\/span>\{\{ numeric\(detail\.output_count\)\.toLocaleString\(\) \}\} 条<\/strong>/, '生成结果数量缺少条数单位或仍误称成功输出') assert.match(detailSource, /const configExpanded = ref\(false\)/, '处理配置没有默认收起') assert.match(detailSource, /appendGroup\(\['clean_invalid', 'deduplicate'\], '数据清洗'\)/, '详情页没有将完整清洗配置合并为数据清洗') assert.match(detailSource, /appendGroup\(\['detect_structure', 'normalize_format'\], '结构标准化'\)/, '详情页没有将完整结构配置合并为结构标准化') assert.match(detailSource, /历史部分配置/, '详情页没有标识旧任务的半组选项') assert.match(detailSource, /异常数据过滤(历史规则)/, '详情页没有标识已停用的历史异常过滤规则') assert.match(detailSource, /new Set\(value\.map/, '详情页没有去除历史预处理配置中的重复值') assert.match(detailSource, /:aria-expanded="configExpanded"/, '处理配置折叠按钮缺少无障碍状态') assert.match(detailSource, /[\s\S]*?v-show="configExpanded"/, '处理配置没有折叠过渡或内容状态') assert.doesNotMatch(detailSource, /const (?:detailMap|completedResults)\b|TODO: 接入真实接口/, '详情页仍包含本地 Mock 数据') for (const apiName of [ 'getDataProcessTask', 'getDataProcessProgress', 'getDataProcessResults', 'updateDataProcessResult', 'restoreDataProcessResult', 'publishDataProcess', 'repeatDataProcessTask', ]) { assert.match( apiSource, new RegExp(`export (?:const|(?:async )?function) ${apiName}\\b`), `API 模块缺少 ${apiName}`, ) } assert.match(apiSource, /keyword\?: string; status\?: string; split\?: string/, '结果列表 API 缺少服务端筛选参数') assert.match(apiSource, /\/results\/\$\{encodeURIComponent\(resultId\)\}/, '结果资源路径没有安全编码结果 ID') assert.match(apiSource, /`\/data-process\/\$\{encodeURIComponent\(taskId\)\}\/publish`/, '发布 API 路径不正确') assert.match(apiSource, /`\/data-process\/\$\{encodeURIComponent\(taskId\)\}\/repeat`/, '再次生成 API 路径不正确') assert.match(typesSource, /interface DataProcessRepeatPayload[\s\S]*?expected_updated_at: string[\s\S]*?request_id: string/, '再次生成请求契约不完整') assert.match(typesSource, /interface DataProcessRepeatResult[\s\S]*?task: DataProcessTask[\s\S]*?source_task_id: string[\s\S]*?created: boolean/, '再次生成响应契约不完整') console.log('数据处理任务详情真实 API 回归检查通过')