2026-07-10 16:46:23 +08:00
import assert from 'node:assert/strict'
import { existsSync } from 'node:fs'
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 viewPath = path . resolve ( scriptDir , '../src/views/data-process/DataProcessCreateView.vue' )
const createDir = path . resolve ( scriptDir , '../src/views/data-process/create' )
2026-07-11 14:49:10 +08:00
const confirmDialogPath = path . resolve ( scriptDir , '../src/components/AppConfirmDialog.vue' )
2026-07-10 16:46:23 +08:00
const layoutPath = path . resolve ( scriptDir , '../src/layouts/MainLayout.vue' )
2026-07-23 15:10:13 +08:00
const apiPath = path . resolve ( scriptDir , '../src/api/modules/dataProcess.ts' )
const contractTypesPath = path . resolve ( scriptDir , '../src/types/dataProcess.ts' )
2026-07-25 18:15:49 +08:00
const requestPath = path . resolve ( scriptDir , '../src/api/request.ts' )
const authStorePath = path . resolve ( scriptDir , '../src/stores/auth.ts' )
2026-07-25 22:41:06 +08:00
const modelsStorePath = path . resolve ( scriptDir , '../src/stores/models.ts' )
2026-07-25 18:15:49 +08:00
const routerPath = path . resolve ( scriptDir , '../src/router/index.ts' )
const sessionActivityPath = path . resolve ( scriptDir , '../src/utils/sessionActivity.ts' )
2026-07-24 11:28:02 +08:00
const sourceUploadWorkerPath = path . join ( createDir , 'useDataProcessSourceUpload.ts' )
2026-07-25 22:41:06 +08:00
const regenerationPath = path . join ( createDir , 'useDataProcessRegeneration.ts' )
2026-07-10 16:46:23 +08:00
const viewSource = await readFile ( viewPath , 'utf8' )
const layoutSource = await readFile ( layoutPath , 'utf8' )
2026-07-25 22:41:06 +08:00
const [ stateSource , generationSource , previewBuildSource , sourceUploadWorkerSource , regenerationSource , viewStyleSource , apiSource , contractTypesSource ] = await Promise . all ( [
2026-07-13 15:28:48 +08:00
readFile ( path . join ( createDir , 'dataProcessCreateState.ts' ) , 'utf8' ) ,
readFile ( path . join ( createDir , 'useDataProcessGeneration.ts' ) , 'utf8' ) ,
2026-07-24 11:28:02 +08:00
readFile ( path . join ( createDir , 'useDataProcessPreviewBuild.ts' ) , 'utf8' ) ,
readFile ( sourceUploadWorkerPath , 'utf8' ) ,
2026-07-25 22:41:06 +08:00
readFile ( regenerationPath , 'utf8' ) ,
2026-07-13 15:28:48 +08:00
readFile ( path . join ( createDir , 'data-process-create.scss' ) , 'utf8' ) ,
2026-07-23 15:10:13 +08:00
readFile ( apiPath , 'utf8' ) ,
readFile ( contractTypesPath , 'utf8' ) ,
2026-07-13 15:28:48 +08:00
] )
2026-07-25 22:41:06 +08:00
const implementationSource = [ viewSource , stateSource , generationSource , previewBuildSource , sourceUploadWorkerSource , regenerationSource ] . join ( '\n' )
const [ requestSource , authStoreSource , modelsStoreSource , routerSource , sessionActivitySource ] = await Promise . all ( [
2026-07-25 18:15:49 +08:00
readFile ( requestPath , 'utf8' ) ,
readFile ( authStorePath , 'utf8' ) ,
2026-07-25 22:41:06 +08:00
readFile ( modelsStorePath , 'utf8' ) ,
2026-07-25 18:15:49 +08:00
readFile ( routerPath , 'utf8' ) ,
readFile ( sessionActivityPath , 'utf8' ) ,
] )
assert . match ( requestSource , /if \(res\.code === 0\) \{[\s\S]*?touchSessionActivity\(\)/ , '成功 API 请求没有刷新会话活跃时间' )
assert . match ( authStoreSource , /const loginTime = sessionActivityTime/ , '认证状态没有共享请求层的会话活跃时间' )
assert . match ( authStoreSource , /if \(currentUser\.value\) touchSessionActivity\(\)/ , '会话续期仍可能在长任务结束后失效' )
assert . match ( routerSource , /const auth = useAuthStore\(\)[\s\S]*?auth\.syncSession\(\)[\s\S]*?if \(!auth\.isLoggedIn\)/ , '路由守卫没有在登录判断前同步 API 活跃时间' )
assert . match ( sessionActivitySource , /export function touchSessionActivity\(\)/ , '缺少统一会话活跃续期函数' )
2026-07-10 16:46:23 +08:00
2026-07-11 14:49:10 +08:00
assert . ok ( existsSync ( confirmDialogPath ) , '缺少公共确认弹窗组件 AppConfirmDialog' )
const confirmDialogSource = await readFile ( confirmDialogPath , 'utf8' )
for ( const marker of [ '<Teleport to="body">' , 'role="alertdialog"' , ':aria-modal="true"' , 'handleKeydown' , 'Escape' ] ) {
assert . ok ( confirmDialogSource . includes ( marker ) , ` 公共确认弹窗缺少可访问性能力: ${ marker } ` )
}
assert . match ( confirmDialogSource , /min-height:\s*44px/ , '公共确认弹窗按钮触控区域不足 44px' )
assert . match ( confirmDialogSource , /focus\(\)/ , '公共确认弹窗打开后没有管理键盘焦点' )
assert . match ( confirmDialogSource , /defineExpose\(\{ open \}\)/ , '公共确认弹窗没有暴露 Promise 式 open API' )
assert . match ( confirmDialogSource , /width:\s*min\(480px,\s*100%\)/ , '企业级确认弹窗宽度应保持紧凑的 480px' )
assert . match ( confirmDialogSource , /border-radius:\s*8px/ , '企业级确认弹窗应使用克制的 8px 圆角' )
assert . doesNotMatch ( confirmDialogSource , /backdrop-filter/ , '企业级确认弹窗不应使用装饰性背景模糊' )
assert . ok ( confirmDialogSource . includes ( 'app-confirm-header' ) , '企业级确认弹窗缺少独立标题栏' )
assert . match ( confirmDialogSource , /\.app-confirm-button\s*\{[\s\S]*?height:\s*34px/ , '桌面端操作按钮应使用紧凑的 34px 高度' )
assert . match ( confirmDialogSource , /@media \(max-width: 520px\)[\s\S]*?\.app-confirm-button\s*\{[\s\S]*?min-height:\s*44px/ , '移动端操作按钮仍需保留 44px 触控高度' )
assert . match ( viewSource , /import AppConfirmDialog from '@\/components\/AppConfirmDialog\.vue'/ , '创建页没有接入公共确认弹窗' )
assert . match ( viewSource , /<AppConfirmDialog/ , '创建页模板缺少公共确认弹窗实例' )
assert . match ( viewSource , /onBeforeRouteLeave\(async \(\) =>/ , '路由离开确认没有改为异步公共弹窗流程' )
assert . doesNotMatch ( viewSource , /window\.confirm|ElMessageBox/ , '创建页仍在使用系统或 Element Plus 确认框' )
2026-07-25 22:41:06 +08:00
assert . match ( routerSource , /path:\s*'data-process\/:id\/regenerate'[\s\S]*?name:\s*'data-process-regenerate'[\s\S]*?DataProcessCreateView\.vue/ , '重新生成路由没有复用创建向导' )
assert . match ( regenerationSource , /route\.name === 'data-process-regenerate'[\s\S]*?route\.params\.id/ , '创建向导没有从路由参数识别重新生成来源任务' )
assert . match ( viewSource , /const currentStep = ref\(0\)/ , '重新生成必须从向导第一步开始' )
2026-07-11 14:49:10 +08:00
2026-07-10 16:46:23 +08:00
assert . match ( viewSource , /const WIZARD_STEPS = \[/ , '向导步骤尚未改为固定常量' )
2026-07-13 17:12:49 +08:00
for ( const title of [ '创建任务' , '大模型选择' , '上传文件' , '数据预览' , '开始生成' , '结果编辑与保存' ] ) {
2026-07-10 16:46:23 +08:00
assert . ok ( viewSource . includes ( ` title: ' ${ title } ' ` ) , ` 缺少固定步骤: ${ title } ` )
}
2026-07-12 15:39:43 +08:00
assert . match (
viewSource ,
2026-07-13 17:12:49 +08:00
/\{ id: 'create',[\s\S]*?\{ id: 'model',[\s\S]*?\{ id: 'upload',[\s\S]*?\{ id: 'preview',[\s\S]*?\{ id: 'generate',[\s\S]*?\{ id: 'results'/ ,
'六步向导顺序必须为创建任务、大模型选择、上传文件、数据预览、开始生成、结果编辑与保存' ,
2026-07-12 15:39:43 +08:00
)
2026-07-10 16:46:23 +08:00
assert . doesNotMatch ( viewSource , /steps\s*=\s*computed|all\.filter/ , '步骤仍根据处理类型动态增减' )
2026-07-13 17:12:49 +08:00
assert . match (
viewStyleSource ,
/@media \(max-width: 1100px\)[\s\S]*?\.step-title\s*\{[\s\S]*?display:\s*none[\s\S]*?\.step-item\.is-active \.step-title\s*\{[\s\S]*?display:\s*block/ ,
'六步向导在中等宽度下没有收起非当前步骤标题' ,
)
2026-07-24 11:28:02 +08:00
assert . equal ( existsSync ( path . join ( createDir , 'useDataProcessDraft.ts' ) ) , false , '不应保留新建任务草稿模块' )
assert . doesNotMatch (
viewSource ,
/useDataProcessDraft|persistDraft|restoreDraft|restoringDraft|localStorage\.(?:setItem|getItem)/ ,
'新建任务不应保存或恢复草稿' ,
)
assert . match ( viewSource , /localStorage\.removeItem\('yg-data-process-create-draft'\)/ , '进入新建页时应清理遗留草稿' )
2026-07-23 15:10:13 +08:00
assert . ok ( viewSource . split ( '\n' ) . length < 1000 , 'DataProcessCreateView 拆分后仍超过 1000 行' )
assert . match ( viewSource , /useDataProcessGeneration\(\{/ , '生成流程没有拆分到独立 composable' )
2026-07-10 16:46:23 +08:00
const expectedComponents = [
'TaskSetupStep.vue' ,
2026-07-13 17:12:49 +08:00
'ModelSelectionStep.vue' ,
2026-07-12 15:39:43 +08:00
'SourceUploadStep.vue' ,
2026-07-10 16:46:23 +08:00
'PreviewCompareStep.vue' ,
'GenerationStep.vue' ,
'ResultEditorStep.vue' ,
]
for ( const component of expectedComponents ) {
assert . ok ( existsSync ( path . join ( createDir , component ) ) , ` 缺少步骤组件: ${ component } ` )
assert . ok ( viewSource . includes ( component . replace ( '.vue' , '' ) ) , ` 父页面未使用: ${ component } ` )
}
const typesPath = path . join ( createDir , 'types.ts' )
const modelPath = path . join ( createDir , 'previewModel.ts' )
2026-07-24 11:28:02 +08:00
const pdfViewerPath = path . join ( createDir , 'PdfSourceViewer.vue' )
2026-07-24 16:30:29 +08:00
const resultEditorPath = path . join ( createDir , 'ResultEditorStep.vue' )
2026-07-10 16:46:23 +08:00
assert . ok ( existsSync ( typesPath ) , '缺少向导类型定义' )
assert . ok ( existsSync ( modelPath ) , '缺少来源映射模型' )
2026-07-24 11:28:02 +08:00
assert . ok ( existsSync ( pdfViewerPath ) , '缺少 PDF 原文件预览组件' )
2026-07-10 16:46:23 +08:00
2026-07-24 16:30:29 +08:00
const [ typesSource , modelSource , previewSource , pdfViewerSource , resultEditorSource ] = await Promise . all ( [
2026-07-10 16:46:23 +08:00
readFile ( typesPath , 'utf8' ) ,
readFile ( modelPath , 'utf8' ) ,
readFile ( path . join ( createDir , 'PreviewCompareStep.vue' ) , 'utf8' ) ,
2026-07-24 11:28:02 +08:00
readFile ( pdfViewerPath , 'utf8' ) ,
2026-07-24 16:30:29 +08:00
readFile ( resultEditorPath , 'utf8' ) ,
2026-07-10 16:46:23 +08:00
] )
for ( const field of [ 'sourceStart' , 'sourceEnd' , 'originalContent' , 'editedContent' ] ) {
assert . ok ( typesSource . includes ( field ) , ` PreviewItem 缺少字段: ${ field } ` )
}
assert . match ( typesSource , /sourceFileId/ , 'PreviewItem 缺少来源文件标识' )
2026-07-13 17:12:49 +08:00
assert . match ( typesSource , /export type StepId = 'create' \| 'model' \| 'upload' \| 'preview' \| 'generate' \| 'results'/ , '步骤类型缺少独立大模型选择步骤' )
2026-07-10 16:46:23 +08:00
assert . match ( modelSource , /export function sourceLines/ , '缺少源文件行偏移生成函数' )
2026-07-23 15:10:13 +08:00
assert . doesNotMatch ( modelSource , /buildPreviewItems/ , '前端不应保留与后端重复的本地切片算法' )
2026-07-10 16:46:23 +08:00
assert . match ( viewSource , /selectedPreviewFileId/ , '父页面缺少当前预览文件状态' )
assert . match (
viewSource ,
2026-07-24 11:28:02 +08:00
/const \{ buildPreviewsByFile \} = useDataProcessPreviewBuild\(\)/ ,
'父页面没有通过独立 composable 构建逐文件预览' ,
2026-07-10 16:46:23 +08:00
)
2026-07-24 15:06:01 +08:00
assert . match ( viewSource , /backend-pipeline-v4/ , '切分管线版本未升级,旧预览缓存可能被误用' )
2026-07-23 15:10:13 +08:00
assert . match ( viewSource , /getDataProcessPreview\(taskId\.value,\s*\{ page:\s*1, page_size:\s*500 \}\)/ , '预览构建后没有分页读取后端数据' )
assert . doesNotMatch ( viewSource , /buildPreviewItems\(/ , '创建向导仍在本地构建集成预览数据' )
2026-07-10 16:46:23 +08:00
for ( const marker of [
'preview-workspace' ,
'source-viewer' ,
'source-line' ,
'is-highlighted' ,
'preview-item' ,
'preview-editor' ,
'scrollIntoView' ,
] ) {
2026-07-13 17:12:49 +08:00
assert . ok ( previewSource . includes ( marker ) , ` 第四步缺少结构或行为: ${ marker } ` )
2026-07-10 16:46:23 +08:00
}
2026-07-13 17:12:49 +08:00
assert . match ( previewSource , /sourceStart/ , '第四步未使用来源起始偏移' )
assert . match ( previewSource , /sourceEnd/ , '第四步未使用来源结束偏移' )
2026-07-10 16:46:23 +08:00
assert . match ( previewSource , /filterable/ , '文件选择器必须可搜索' )
assert . match ( previewSource , /当前文件/ , '预览缺少当前文件切换器' )
assert . doesNotMatch ( previewSource , /located-badge|sync-label|已定位到/ , '源文件栏不应显示冗余定位提示' )
2026-07-24 14:23:20 +08:00
assert . match ( previewSource , /const PREVIEW_PAGE_SIZE = 10/ , '切片列表每页应展示 10 条' )
2026-07-10 16:46:23 +08:00
assert . match ( previewSource , /const pagedItems = computed/ , '切片列表缺少分页数据' )
assert . match ( previewSource , /v-for="item in pagedItems"/ , '切片列表没有使用分页数据' )
assert . match ( previewSource , /<el-pagination[\s\S]*:page-size="PREVIEW_PAGE_SIZE"/ , '切片列表缺少分页控件' )
assert . match ( previewSource , /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/ , '预览工作区高度不足以展示切片正文' )
assert . match ( previewSource , /const editingItemId = ref<string \| null>\(null\)/ , '缺少切片编辑模式状态' )
assert . match ( previewSource , /const editorDraft = ref\(''\)/ , '缺少编辑临时草稿' )
assert . match ( previewSource , /function openEditor\(item: PreviewItem\)/ , '列表缺少打开切片编辑器的动作' )
assert . match ( previewSource , /function closeEditor\(\)/ , '编辑器缺少返回列表的动作' )
assert . match ( previewSource , /function saveEditor\(\)/ , '编辑器缺少保存动作' )
assert . match ( previewSource , /<template v-if="!editingItem">[\s\S]*?<template v-else>/ , '切片列表与编辑器必须互斥展示' )
assert . match ( previewSource , /fa-pencil/ , '切片列表缺少铅笔编辑按钮' )
assert . match ( previewSource , /fa-trash-o/ , '切片列表缺少垃圾桶删除按钮' )
assert . match ( previewSource , /class="preview-item"[\s\S]*?@click="selectItem\(item\.id\)"/ , '点击切片行必须更新当前选中切片' )
assert . match ( previewSource , /v-model="editorDraft"/ , '编辑器必须绑定临时草稿' )
assert . match ( previewSource , />取消<\/el-button>/ , '编辑器缺少取消按钮' )
assert . doesNotMatch ( previewSource , /返回列表/ , '编辑器不应同时显示返回列表和取消两个相同作用的按钮' )
assert . match ( previewSource , />保存修改<\/el-button>/ , '编辑器缺少保存修改按钮' )
assert . match ( previewSource , /\.editor-actions\s*\{[\s\S]*?justify-content:\s*flex-end/ , '取消和保存按钮必须在编辑器右侧对齐' )
assert . doesNotMatch ( previewSource , /item-token|item-status|modifiedOnly|仅看已修改/ , '切片列表不应再显示 Token 或修改状态' )
assert . match ( previewSource , /\.preview-editor\s*\{[\s\S]*?flex:\s*1 1 auto[\s\S]*?overflow-y:\s*auto/ , '编辑模式必须占据右侧剩余区域并可滚动' )
2026-07-13 17:12:49 +08:00
assert . match ( previewSource , /@media \(max-width: 900px\)/ , '第四步缺少窄屏上下布局' )
2026-07-24 11:28:02 +08:00
assert . match ( previewSource , /<PdfSourceViewer[\s\S]*v-if="isPdfSource"/ , 'PDF 文件没有切换到原文件查看组件' )
assert . match ( previewSource , /:selected-item="selectedItem \?\? null"/ , 'PDF 查看组件没有接收当前选中切片' )
assert . match ( previewSource , /:data-preview-id="item\.id"/ , '切片行缺少稳定的交互定位标识' )
assert . match ( previewSource , /<div v-else ref="sourceViewerRef" class="source-viewer"/ , '非 PDF 文件没有保留文本预览' )
assert . match ( pdfViewerSource , /getDataProcessSourceRawUrl/ , 'PDF 查看组件没有使用受控原文件地址' )
assert . match ( pdfViewerSource , /getDataProcessPdfPages/ , 'PDF 查看组件没有读取页码与全文偏移映射' )
assert . match ( pdfViewerSource , /pdfjs-dist/ , 'PDF 查看组件没有使用可控的 PDF.js 渲染器' )
assert . match ( pdfViewerSource , /TextLayer/ , 'PDF 查看组件没有渲染可定位的 PDF 文字层' )
2026-07-24 14:23:20 +08:00
assert . match ( pdfViewerSource , /page\.streamTextContent\(\)/ , 'PDF 文字层没有使用兼容 WebKit 的流式读取方式' )
assert . doesNotMatch ( pdfViewerSource , /page\.getTextContent\(\)/ , 'PDF 文字层仍依赖 WebKit 不完整支持的 ReadableStream 异步迭代' )
2026-07-24 11:28:02 +08:00
assert . match ( pdfViewerSource , /is-slice-highlighted/ , 'PDF 查看组件没有实现选中切片高亮' )
assert . match ( pdfViewerSource , /:data-page-number="currentPage"/ , 'PDF 查看组件缺少当前物理页标识' )
assert . doesNotMatch ( pdfViewerSource , /<iframe/ , 'PDF 查看组件不应继续使用无法控制高亮的浏览器 iframe' )
assert . match ( pdfViewerSource , /:aria-label="`PDF 预览:\$\{fileName\}`"/ , 'PDF 查看器缺少可访问标题' )
assert . match ( apiSource , /getDataProcessSourceRawUrl/ , '前端 API 缺少 PDF 原文件预览地址' )
assert . match ( apiSource , /getDataProcessPdfPages/ , '前端 API 缺少 PDF 页码映射接口' )
assert . match ( apiSource , /source-files\/\$\{encodeURIComponent\(fileId\)\}\/pdf-pages/ , 'PDF 页码映射接口地址不正确' )
2026-07-10 16:46:23 +08:00
const taskSetupPath = path . join ( createDir , 'TaskSetupStep.vue' )
2026-07-13 15:28:48 +08:00
const structuredOptionsPath = path . join ( createDir , 'StructuredOptionsPanel.vue' )
const unstructuredOptionsPath = path . join ( createDir , 'UnstructuredOptionsPanel.vue' )
const datasetSplitEditorPath = path . join ( createDir , 'DatasetSplitEditor.vue' )
2026-07-13 11:47:33 +08:00
const generationOptionsPath = path . join ( createDir , 'GenerationOptionsPanel.vue' )
2026-07-13 17:12:49 +08:00
const modelSelectionPath = path . join ( createDir , 'ModelSelectionStep.vue' )
2026-07-12 15:39:43 +08:00
const sourceUploadPath = path . join ( createDir , 'SourceUploadStep.vue' )
2026-07-13 15:28:48 +08:00
const [
taskSetupSource ,
structuredOptionsSource ,
unstructuredOptionsSource ,
datasetSplitEditorSource ,
generationControlSource ,
2026-07-13 17:12:49 +08:00
modelSelectionSource ,
2026-07-13 15:28:48 +08:00
sourceUploadSource ,
] = await Promise . all ( [
readFile ( taskSetupPath , 'utf8' ) ,
readFile ( structuredOptionsPath , 'utf8' ) ,
readFile ( unstructuredOptionsPath , 'utf8' ) ,
readFile ( datasetSplitEditorPath , 'utf8' ) ,
readFile ( generationOptionsPath , 'utf8' ) ,
2026-07-13 17:12:49 +08:00
readFile ( modelSelectionPath , 'utf8' ) ,
2026-07-13 15:28:48 +08:00
readFile ( sourceUploadPath , 'utf8' ) ,
] )
const taskSetupFeatureSource = [
taskSetupSource ,
structuredOptionsSource ,
unstructuredOptionsSource ,
datasetSplitEditorSource ,
] . join ( '\n' )
for ( const componentPath of [ structuredOptionsPath , unstructuredOptionsPath , datasetSplitEditorPath ] ) {
assert . ok ( existsSync ( componentPath ) , ` 缺少任务配置拆分组件: ${ path . basename ( componentPath ) } ` )
}
assert . ok ( taskSetupSource . split ( '\n' ) . length < 800 , 'TaskSetupStep 拆分后仍超过 800 行' )
assert . match ( taskSetupSource , /<StructuredOptionsPanel/ , '任务配置没有挂载结构化选项面板' )
assert . match ( taskSetupSource , /<UnstructuredOptionsPanel/ , '任务配置没有挂载非结构化选项面板' )
assert . match ( structuredOptionsSource , /<DatasetSplitEditor/ , '结构化选项没有复用数据集划分编辑器' )
assert . match ( unstructuredOptionsSource , /<DatasetSplitEditor/ , '非结构化选项没有复用数据集划分编辑器' )
2026-07-12 15:39:43 +08:00
for ( const marker of [ '<el-upload' , '源数据上传' , '数据源配置' , 'uploadedFiles' ] ) {
2026-07-13 15:28:48 +08:00
assert . ok ( ! taskSetupFeatureSource . includes ( marker ) , ` 第一步仍包含上传职责: ${ marker } ` )
2026-07-12 15:39:43 +08:00
}
2026-07-13 17:12:49 +08:00
assert . match ( viewSource , /<ModelSelectionStep\s+[\s\S]*?v-else-if="currentStepId === 'model'"/ , '第二步没有挂载独立大模型选择组件' )
assert . match ( viewSource , /<SourceUploadStep\s+[\s\S]*?v-else-if="currentStepId === 'upload'"/ , '第三步没有挂载独立上传组件' )
assert . match ( viewSource , /if \(currentStepId\.value === 'create'\) return '继续:选择大模型'/ , '第一步主按钮没有指向大模型选择' )
assert . match ( viewSource , /if \(currentStepId\.value === 'model'\) return '继续:上传文件'/ , '第二步主按钮没有指向上传文件' )
2026-07-24 11:28:02 +08:00
assert . match (
viewSource ,
/if \(currentStepId\.value === 'upload'\) \{[\s\S]*?sourceUploading\.value[\s\S]*?'正在上传'[\s\S]*?previewBuilding\.value \? '正在切分' : '继续:数据预览'/ ,
'第三步主按钮没有依次反映上传、切分状态并指向数据预览' ,
)
2026-07-12 15:39:43 +08:00
const nextFromCreateStart = viewSource . indexOf ( 'async function nextFromCreate()' )
2026-07-13 17:12:49 +08:00
const nextFromModelStart = viewSource . indexOf ( 'async function nextFromModel()' , nextFromCreateStart )
const nextFromUploadStart = viewSource . indexOf ( 'function nextFromUpload()' , nextFromModelStart )
2026-07-12 15:39:43 +08:00
const selectPreviewFileStart = viewSource . indexOf ( 'function selectPreviewFile(' , nextFromUploadStart )
2026-07-13 17:12:49 +08:00
assert . ok (
nextFromCreateStart >= 0 && nextFromModelStart > nextFromCreateStart && nextFromUploadStart > nextFromModelStart ,
'缺少创建、大模型选择与上传步骤的独立跳转函数' ,
)
const nextFromCreateSource = viewSource . slice ( nextFromCreateStart , nextFromModelStart )
const nextFromModelSource = viewSource . slice ( nextFromModelStart , nextFromUploadStart )
2026-07-12 15:39:43 +08:00
const nextFromUploadSource = viewSource . slice ( nextFromUploadStart , selectPreviewFileStart )
assert . match ( nextFromCreateSource , /taskSetupRef\.value\?\.validate\(\)/ , '创建步骤继续前没有校验任务配置' )
2026-07-13 17:12:49 +08:00
assert . match ( nextFromCreateSource , /goToStep\('model'\)/ , '创建步骤校验通过后没有进入大模型选择' )
2026-07-23 15:10:13 +08:00
assert . doesNotMatch ( nextFromCreateSource , /uploadedFiles|buildDataProcessPreview/ , '创建步骤仍在校验文件或提前生成预览' )
2026-07-13 17:12:49 +08:00
assert . match ( nextFromModelSource , /modelSelectionRef\.value\?\.validate\(\)/ , '大模型选择步骤继续前没有校验模型配置' )
2026-07-23 15:10:13 +08:00
assert . match ( nextFromModelSource , /createDataProcessTask\(taskPayload\(\)\)/ , '大模型选择完成后没有通过真实 API 创建任务' )
2026-07-13 17:12:49 +08:00
assert . match ( nextFromModelSource , /goToStep\('upload'\)/ , '大模型选择完成后没有进入上传文件' )
2026-07-25 22:41:06 +08:00
assert . match ( nextFromModelSource , /await modelsStore\.load\(true\)/ , '提交任务前没有等待最新模型列表' )
assert . ok ( nextFromModelSource . indexOf ( 'await modelsStore.load(true)' ) < nextFromModelSource . indexOf ( 'prepareRegeneration(taskPayload())' ) , '重新生成可能在模型列表校验前修改服务端' )
assert . ok ( nextFromModelSource . indexOf ( 'await modelsStore.load(true)' ) < nextFromModelSource . indexOf ( 'updateDataProcessTask' ) , '普通任务可能在模型列表校验前修改服务端' )
assert . ok ( nextFromModelSource . includes ( '模型列表加载失败,未提交任何修改' ) , '模型列表加载失败没有明确阻断提交' )
assert . ok ( nextFromModelSource . includes ( '原任务使用的模型已删除' ) , '原任务模型已删除没有要求重新选择' )
assert . match ( modelsStoreSource , /catch \{[\s\S]*?list\.value = \[\][\s\S]*?loaded\.value = false/ , '模型列表请求失败后 loaded 状态仍可能错误保留' )
2026-07-12 15:39:43 +08:00
assert . match ( nextFromUploadSource , /uploadedFiles\.value\.length === 0/ , '上传步骤继续前没有校验源数据' )
2026-07-24 11:28:02 +08:00
assert . match (
nextFromUploadSource ,
/const pendingFileIds = uploadedFiles\.value[\s\S]*?\.filter\(\(file\) => file\.previewStatus !== 'success' \|\| file\.previewConfigSignature !== configSignature\)[\s\S]*?\.map\(\(file\) => file\.sourceFileId\)[\s\S]*?\.filter\(\(fileId\): fileId is string => Boolean\(fileId\)\)/ ,
'上传步骤没有仅选择待处理或失败文件,无法跳过成功文件并重试失败文件' ,
)
assert . match (
nextFromUploadSource ,
/await buildPreviewsByFile\(taskId\.value, pendingFileIds, \(progress\) => \{/ ,
'上传步骤没有通过独立 composable 串行构建待处理文件' ,
)
assert . match (
nextFromUploadSource ,
/file\.previewStatus = progress\.status[\s\S]*?file\.previewProgress = progress\.progress[\s\S]*?file\.previewError = progress\.error/ ,
'上传步骤没有把单文件构建进度与错误回写到对应文件' ,
)
assert . doesNotMatch (
nextFromUploadSource ,
/source_file_ids:\s*uploadedFiles\.value\.map/ ,
'上传步骤仍一次性批量构建全部文件预览' ,
)
assert . match (
previewBuildSource ,
/for \(const sourceFileId of sourceFileIds\)[\s\S]*?onProgress\(\{ source_file_id: sourceFileId, status: 'processing', progress: 0 \}\)/ ,
'逐文件构建 composable 没有串行处理并先上报 processing 进度' ,
)
assert . match (
previewBuildSource ,
/buildDataProcessPreview\(taskId,\s*\{[\s\S]*?replace_existing:\s*true[\s\S]*?source_file_ids:\s*\[sourceFileId\]/ ,
'逐文件构建 composable 没有按单个源文件 ID 替换预览' ,
)
assert . match (
previewBuildSource ,
/status: 'success', progress: 100, preview_count: previewCount/ ,
'单文件构建成功后没有上报 100% 和预览数量' ,
)
assert . match (
previewBuildSource ,
/catch \(error\)[\s\S]*?status: 'failed'[\s\S]*?progress: 0[\s\S]*?error:/ ,
'单文件构建失败后没有保留可重试错误' ,
)
2026-07-23 15:10:13 +08:00
assert . match ( nextFromUploadSource , /getDataProcessPreview\(/ , '上传步骤没有读取后端预览结果' )
2026-07-24 11:28:02 +08:00
assert . match (
nextFromUploadSource ,
/const allFilesSucceeded = \(\) => uploadedFiles\.value\.every\(\(file\) => \([\s\S]*?file\.previewStatus === 'success'[\s\S]*?file\.previewConfigSignature === configSignature/ ,
'上传步骤缺少全部文件 success 且配置签名一致的完成判定' ,
)
const previewSuccessGateIndex = nextFromUploadSource . indexOf ( 'if (failedCount || !allFilesSucceeded())' )
const previewStepIndex = nextFromUploadSource . lastIndexOf ( "goToStep('preview')" )
assert . ok ( previewSuccessGateIndex >= 0 , '上传步骤没有在任一文件未成功时停留当前步骤' )
assert . match (
nextFromUploadSource ,
/if \(signature === previewSignature\.value && previewItems\.value\.length && allFilesSucceeded\(\)\) \{\s*goToStep\('preview'\)/ ,
'缓存预览快速路径没有要求全部文件 success' ,
)
assert . equal (
( nextFromUploadSource . match ( /goToStep\('preview'\)/g ) || [ ] ) . length ,
2 ,
'上传步骤只能通过全成功缓存路径或本轮全成功路径进入预览' ,
)
assert . ok (
previewStepIndex > previewSuccessGateIndex ,
'上传步骤必须在全部文件 success 后才能进入数据预览' ,
)
2026-07-12 15:39:43 +08:00
assert . match ( viewSource , /function goToStep\(stepId: StepId\)[\s\S]*?WIZARD_STEPS\.findIndex/ , '向导跳转没有使用稳定步骤标识' )
assert . match ( viewSource , /currentStepId\.value === 'preview'[\s\S]*?goToStep\('generate'\)/ , '数据预览步骤没有进入开始生成' )
assert . match ( viewSource , /generation\.status === 'success'[\s\S]*?goToStep\('results'\)/ , '生成成功后没有进入结果编辑与保存' )
assert . match ( viewSource , /<ResultEditorStep\s+[\s\S]*?v-else-if="currentStepId === 'results'"/ , '结果编辑器必须只在结果步骤渲染' )
2026-07-24 16:30:29 +08:00
assert . match ( viewSource , /<ResultEditorStep[\s\S]*?:preview-items="previewItems"/ , '结果编辑器没有接收已有预览原文' )
assert . match ( typesSource , /previewItemId: string \| null/ , '结果项缺少预览原文关联 ID' )
assert . match ( generationSource , /previewItemId:\s*item\.preview_item_id == null \? null : String\(item\.preview_item_id\)/ , '结果映射丢失 preview_item_id' )
assert . match ( resultEditorSource , /previewItems: PreviewItem\[\]/ , '结果编辑器缺少原文列表契约' )
assert . match ( resultEditorSource , /props\.previewItems\.find\(\(item\) => item\.id === previewItemId\)/ , '结果编辑器没有按 preview_item_id 关联原文' )
assert . match ( resultEditorSource , /editedContent\.trim\(\)[\s\S]*?originalContent\.trim\(\)/ , '原文参照没有优先展示实际用于生成的预处理内容' )
assert . match ( resultEditorSource , /原文参照/ , '结果编辑器缺少原文参照区域' )
assert . match ( resultEditorSource , /实际送入模型的预处理后原文/ , '结果编辑器没有说明原文参照口径' )
2026-07-12 15:39:43 +08:00
assert . match ( viewSource , /watch\(processType,[\s\S]*?resetSourceDataForProcessTypeChange\(\)/ , '切换处理类型后没有失效旧源数据' )
assert . match ( viewSource , /function resetSourceDataForProcessTypeChange\(\)[\s\S]*?uploadedFiles\.value = \[\][\s\S]*?selectedPreviewFileId\.value = null/ , '旧源数据失效没有同步清理文件与预览选择' )
2026-07-10 16:46:23 +08:00
2026-07-11 14:49:10 +08:00
assert . match ( taskSetupSource , /v-if="processType === 'structured'"/ , '结构化配置必须仅在结构化数据类型下显示' )
2026-07-24 11:28:02 +08:00
const expectedStructuredOptions = [
[ 'clean_invalid' , '清理无效数据' , '清理全空列,并剔除关键字段残缺的数据行' ] ,
[ 'detect_structure' , '识别表格结构' , '识别多级表头与合并单元格,并将嵌套字段展平' ] ,
[ 'deduplicate' , '重复数据去重' , '基于整行精确匹配和关键字段组合删除重复记录' ] ,
[ 'normalize_format' , '数据格式标准化' , '按所选规则统一编码、空白、字段名及 JSON 序列化格式' ] ,
[ 'filter_anomaly' , '异常数据过滤' , '使用 IQR 识别数值离群值,并过滤乱码等异常记录' ] ,
[ 'desensitize' , '敏感信息脱敏' , '识别并脱敏姓名、手机号、邮箱和身份证号' ] ,
]
for ( const [ value , label , description ] of expectedStructuredOptions ) {
assert . ok ( structuredOptionsSource . includes ( ` value: ' ${ value } ' ` ) , ` 结构化预处理缺少值: ${ value } ` )
assert . ok ( structuredOptionsSource . includes ( ` label: ' ${ label } ' ` ) , ` 结构化预处理缺少标签: ${ label } ` )
assert . ok ( structuredOptionsSource . includes ( ` description: ' ${ description } ' ` ) , ` 结构化预处理语义不准确: ${ value } ` )
}
const structuredOptionValues = [ ... structuredOptionsSource . matchAll ( /\{ value: '([^']+)', label:/g ) ]
. map ( ( match ) => match [ 1 ] )
assert . deepEqual ( structuredOptionValues , expectedStructuredOptions . map ( ( [ value ] ) => value ) , '结构化预处理值集合不准确' )
assert . equal ( new Set ( structuredOptionValues ) . size , structuredOptionValues . length , '结构化预处理 value 必须唯一' )
assert . match ( structuredOptionsSource , /Array\.from\(new Set\(value\.filter\(/ , '结构化预处理选中值没有去重' )
2026-07-13 15:28:48 +08:00
assert . ok ( structuredOptionsSource . includes ( '生成选项' ) , '结构化配置缺少生成选项分类' )
2026-07-11 14:49:10 +08:00
for ( const splitName of [ '训练集' , '验证集' , '测试集' ] ) {
2026-07-13 15:28:48 +08:00
assert . ok ( datasetSplitEditorSource . includes ( splitName ) , ` 生成选项缺少数据集划分: ${ splitName } ` )
2026-07-11 14:49:10 +08:00
}
2026-07-13 15:28:48 +08:00
assert . match ( datasetSplitEditorSource , /const splitTotal = computed/ , '数据集划分缺少比例总和计算' )
2026-07-11 14:49:10 +08:00
assert . match ( taskSetupSource , /splitTotal\.value !== 100/ , '数据集划分缺少总和 100% 校验' )
2026-07-13 15:28:48 +08:00
assert . ok ( datasetSplitEditorSource . includes ( '训练集、验证集和测试集比例总和必须为 100%' ) , '数据集划分缺少就地错误提示' )
2026-07-11 14:49:10 +08:00
for ( const splitField of [ 'train' , 'validation' , 'test' ] ) {
assert . match (
2026-07-13 15:28:48 +08:00
datasetSplitEditorSource ,
new RegExp ( ` modelValue \\ . ${ splitField } [ \\ s \\ S]*?:min="0"[ \\ s \\ S]*?:max="100"[ \\ s \\ S]*?:step="1"[ \\ s \\ S]*?:precision="0" ` ) ,
2026-07-11 14:49:10 +08:00
` 数据集划分字段 ${ splitField } 缺少 0~ 100 的整数限制 ` ,
)
}
2026-07-27 09:11:58 +08:00
assert . match ( typesSource , /QA_PAIRS_GENERATION_LIMITS\s*=\s*\{ min: 1, max: 50 \}/ , '问答生成数量统一范围必须为 1 到 50' )
assert . match ( typesSource , /function normalizeQaPairsGenerationCount[\s\S]*?Math\.trunc\(parsed\)[\s\S]*?QA_PAIRS_GENERATION_LIMITS\.max[\s\S]*?QA_PAIRS_GENERATION_LIMITS\.min/ , '问答生成数量缺少统一整数归一化和边界裁剪' )
assert . match ( structuredOptionsSource , /options\.qaPairsPerRow[\s\S]*?:min="QA_PAIRS_GENERATION_LIMITS\.min"[\s\S]*?:max="QA_PAIRS_GENERATION_LIMITS\.max"/ , '每行生成数量必须使用统一的 1 到 50 限制' )
assert . match ( structuredOptionsSource , /支持 1~ 50 条;数量越大,处理耗时和 Token 消耗越高/ , '结构化生成数量缺少耗时与 Token 消耗说明' )
2026-07-11 14:49:10 +08:00
assert . match ( viewSource , /const structuredOptions = ref<StructuredProcessOptions>/ , '父页面缺少结构化配置状态' )
2026-07-13 15:28:48 +08:00
assert . match ( stateSource , /datasetSplit:\s*\{ train: 80, validation: 10, test: 10 \}/ , '数据集划分默认值必须为 80/10/10' )
2026-07-11 14:49:10 +08:00
assert . match ( viewSource , /v-model:structured-options="structuredOptions"/ , '父页面没有双向绑定结构化配置' )
2026-07-23 15:10:13 +08:00
assert . match ( generationSource , /generateDataProcess\(taskId\)/ , '开始生成没有调用真实 API' )
assert . match ( generationSource , /getDataProcessProgress\(taskId\)/ , '生成状态没有通过真实 API 轮询' )
assert . match ( generationSource , /getDataProcessResults\(taskId,[\s\S]*?page:[\s\S]*?page_size:/ , '生成完成后没有分页加载真实结果' )
assert . match ( generationSource , /updateDataProcessResult\(taskId,\s*item\.id,[\s\S]*?expected_updated_at:/ , '结果保存没有调用真实 API 或缺少并发版本' )
assert . ok ( generationSource . includes ( 'item.quality_score?.overall' ) , '结果映射没有读取质量总分 overall' )
assert . ok ( generationSource . includes ( 'item.quality_score?.flags || []' ) , '结果映射没有读取质量标记 flags' )
assert . doesNotMatch ( generationSource , /createResults\(/ , '生成 composable 仍在本地伪造处理结果' )
for ( const apiName of [
'createDataProcessTask' ,
2026-07-25 22:41:06 +08:00
'regenerateDataProcessTask' ,
2026-07-23 15:10:13 +08:00
'uploadDataProcessSourceFiles' ,
'buildDataProcessPreview' ,
'getDataProcessPreview' ,
'generateDataProcess' ,
'getDataProcessProgress' ,
'getDataProcessResults' ,
'updateDataProcessResult' ,
'publishDataProcess' ,
] ) {
assert . match ( apiSource , new RegExp ( ` export (?:const|async function|function) ${ apiName } \\ b ` ) , ` API 模块缺少 ${ apiName } ` )
}
2026-07-24 11:28:02 +08:00
assert . match ( sourceUploadWorkerSource , /uploadDataProcessSourceFiles\(currentTaskId,\s*\[job\.file\]/ , '文件上传没有逐文件调用真实 API' )
assert . match ( sourceUploadWorkerSource , /STRUCTURED_FILE_EXTENSIONS = new Set\(\['json', 'jsonl', 'ndjson', 'csv', 'tsv', 'xlsx'\]\)/ , '结构化文件扩展名白名单不完整' )
for ( const extension of [ 'txt' , 'md' , 'markdown' , 'pdf' , 'docx' , 'pptx' , 'json' , 'jsonl' , 'ndjson' ] ) {
assert . ok ( sourceUploadWorkerSource . includes ( ` ' ${ extension } ' ` ) , ` 非结构化文件扩展名白名单缺少 ${ extension } ` )
}
assert . match ( sourceUploadWorkerSource , /LEGACY_OFFICE_EXTENSIONS = new Set\(\['doc', 'xls', 'ppt'\]\)/ , '缺少旧版 Office 格式识别' )
assert . ok ( sourceUploadWorkerSource . includes ( '请分别转换为 DOCX、XLSX、PPTX 后上传' ) , '旧版 Office 文件缺少转换提示' )
assert . match ( sourceUploadWorkerSource , /if \(!BINARY_FILE_EXTENSIONS\.has\(job\.extension\)\) \{[\s\S]*?TextDecoder/ , '文本格式没有执行 UTF-8 客户端校验' )
assert . match ( sourceUploadWorkerSource , /if \(BINARY_FILE_EXTENSIONS\.has\(job\.extension\)\) \{[\s\S]*?getDataProcessSourceContent\(currentTaskId, source\.id,[\s\S]*?start_line:\s*1,[\s\S]*?line_count:\s*10_000/ , '二进制文档上传后没有读取后端解析文本' )
2026-07-23 15:10:13 +08:00
assert . match ( apiSource , /formData\.append\('files', file\)/ , '上传 API 没有使用 files 多文件表单字段' )
2026-07-24 11:28:02 +08:00
assert . match ( apiSource , /onUploadProgress:[\s\S]*?event\.loaded \/ event\.total[\s\S]*?Math\.min\(99,/ , '上传 API 没有接入真实字节进度或响应前未限制在 99%' )
assert . match ( apiSource , /source-files`[\s\S]*?timeout: 5 \* 60 \* 1000/ , '源文件上传缺少 5 分钟超时' )
2026-07-23 15:10:13 +08:00
assert . match ( apiSource , /\/preview\/build/ , 'API 模块缺少后端预览构建路径' )
2026-07-24 11:28:02 +08:00
assert . match ( apiSource , /\/preview\/build`[\s\S]*?\{ timeout: 5 \* 60 \* 1000 \}/ , '单文件切分请求缺少 5 分钟超时' )
2026-07-23 15:10:13 +08:00
assert . match ( apiSource , /\/progress`/ , 'API 模块缺少生成进度路径' )
assert . match ( apiSource , /\/results`/ , 'API 模块缺少结果分页路径' )
assert . match ( apiSource , /\/publish`/ , 'API 模块缺少数据集发布路径' )
assert . match ( contractTypesSource , /source_file_ids\?: Array<string \| number>/ , '预览构建契约缺少源文件 ID 列表' )
2026-07-24 11:28:02 +08:00
assert . match (
contractTypesSource ,
/export type DataProcessPreviewFileStatus = 'waiting' \| 'processing' \| 'success' \| 'failed'/ ,
'文件预览状态契约不完整' ,
)
for ( const field of [ 'rawFile' , 'status' , 'uploadProgress' , 'uploadError' , 'previewStatus' , 'previewProgress' , 'previewError' , 'previewConfigSignature' ] ) {
assert . ok ( typesSource . includes ( field ) , ` 上传文件缺少逐文件预览字段: ${ field } ` )
}
assert . match ( typesSource , /status: 'queued' \| 'uploading' \| 'ready' \| 'failed'/ , '上传文件状态机不完整' )
assert . match ( viewSource , /uploadedFiles\.value\.push\([\s\S]*?status: 'queued'[\s\S]*?enqueueSourceUpload/ , '文件选择后没有先进入列表再加入上传队列' )
assert . match ( sourceUploadWorkerSource , /while \(queue\.length\) \{[\s\S]*?await uploadOne\(job\)/ , '多文件上传没有由单一队列逐个等待' )
assert . doesNotMatch ( sourceUploadWorkerSource , /Promise\.(?:all|allSettled)/ , '上传队列不得并发消费文件' )
assert . match ( sourceUploadWorkerSource , /pending\.status = 'ready'[\s\S]*?pending\.uploadProgress = 100/ , '服务端响应成功后没有将文件置为上传完成' )
assert . match ( viewSource , /failedUploads[\s\S]*?hasUnfinishedUploads[\s\S]*?buildPreviewsByFile/ , '上传失败或未完成时没有阻断切分' )
assert . doesNotMatch ( viewSource , /file\.sourceFileId \|\| file\.uid/ , '切分或删除仍可能把本地临时 UID 当成后端文件 ID' )
2026-07-23 15:10:13 +08:00
assert . match ( contractTypesSource , /expected_updated_at\?: string/ , '编辑契约缺少乐观并发版本字段' )
2026-07-25 22:41:06 +08:00
assert . match ( contractTypesSource , /export interface DataProcessRegeneratePayload extends DataProcessTaskCreatePayload[\s\S]*?expected_updated_at: string/ , '重新生成 payload 的原任务并发版本必须为必填' )
assert . match ( contractTypesSource , /export interface DataProcessRegenerateResult[\s\S]*?preview_invalidated: boolean[\s\S]*?published_outputs_preserved: boolean/ , '重新生成响应缺少预览失效或已发布数据保留状态' )
assert . match ( apiSource , /regenerateDataProcessTask[\s\S]*?\/regenerate`/ , '重新生成 API 路径未接入' )
2026-07-11 14:49:10 +08:00
2026-07-13 11:47:33 +08:00
for ( const field of [
'generationModelId' ,
'generationPrompt' ,
'qualityFilterEnabled' ,
'filterLowQuality' ,
'filterShortContent' ,
'minOutputLength' ,
] ) {
assert . ok ( typesSource . includes ( field ) , ` 生成控制配置缺少字段: ${ field } ` )
2026-07-24 11:28:02 +08:00
assert . ok ( implementationSource . includes ( field ) , ` 父页面默认值缺少字段: ${ field } ` )
2026-07-13 15:28:48 +08:00
}
2026-07-13 17:12:49 +08:00
assert . match ( structuredOptionsSource , /GenerationOptionsPanel/ , '结构化生成选项没有复用统一的质量筛选组件' )
assert . match ( unstructuredOptionsSource , /GenerationOptionsPanel/ , '非结构化生成选项没有复用统一的质量筛选组件' )
2026-07-13 15:28:48 +08:00
assert . match ( structuredOptionsSource , /:options="options"/ , '结构化生成选项未接入统一配置组件' )
assert . match ( unstructuredOptionsSource , /:options="options"/ , '非结构化生成选项未接入统一配置组件' )
2026-07-13 17:12:49 +08:00
assert . doesNotMatch ( taskSetupFeatureSource , /<h3>大模型<\/h3>|section="model"/ , '第一步不应继续承载大模型配置' )
assert . match ( modelSelectionSource , /<h3[^>]*>大模型选择<\/h3>/ , '独立步骤缺少大模型选择标题' )
assert . match ( modelSelectionSource , /section="model"/ , '独立步骤没有挂载模型配置' )
assert . match ( modelSelectionSource , /defineExpose\(\{ validate \}\)/ , '独立大模型选择步骤没有暴露继续前校验' )
assert . match ( modelSelectionSource , /class="form-section"/ , '大模型选择步骤没有沿用第一步的通栏表单分区' )
assert . doesNotMatch ( modelSelectionSource , /max-width:\s*980px/ , '大模型选择步骤不应使用比第一步更窄的固定内容宽度' )
2026-07-13 15:28:48 +08:00
assert . match ( taskSetupFeatureSource , /section="quality"/ , '质量筛选没有保留在生成选项分类中' )
2026-07-13 11:47:33 +08:00
assert . doesNotMatch ( generationControlSource , /<h4>大模型<\/h4>/ , '大模型不应继续作为生成选项内部子分类' )
for ( const label of [ '大模型' , '数据生成模型' , '默认提示语' , '质量筛选' , '过滤低质量内容' , '过滤过短内容' , '最少字数' ] ) {
assert . ok ( generationControlSource . includes ( label ) , ` 生成控制界面缺少: ${ label } ` )
}
assert . match ( generationControlSource , /filterable/ , '数据生成模型下拉必须支持搜索' )
assert . match ( generationControlSource , /maxlength="500"/ , '默认提示语缺少合理的长度限制' )
2026-07-13 15:28:48 +08:00
assert . match ( generationControlSource , /\.model-field\s*\{[\s\S]*?display:\s*flex[\s\S]*?flex-direction:\s*column/ , '大模型字段没有使用稳定的纵向表单布局' )
assert . match ( generationControlSource , /\.generation-config-group\s*\{[\s\S]*?border:\s*1px solid #e2e5ec/ , '大模型配置没有保留统一配置面板边框' )
2026-07-13 17:12:49 +08:00
assert . match ( generationControlSource , /\.model-config-group\s*\{[\s\S]*?padding:\s*0[\s\S]*?border:\s*0/ , '独立大模型步骤仍存在嵌套卡片挤压' )
assert . match (
generationControlSource ,
/\.model-config-group \.advanced-settings-grid\s*\{[\s\S]*?grid-template-columns:\s*1fr/ ,
'大模型高级参数没有改为与第一步一致的纵向布局' ,
)
2026-07-13 15:28:48 +08:00
assert . match ( stateSource , /const DEFAULT_GENERATION_PROMPT\s*=\s*['"][^'"]{40,}['"]/ , '大模型配置缺少可直接使用的默认提示语' )
assert . equal ( ( stateSource . match ( /generationPrompt:\s*DEFAULT_GENERATION_PROMPT/g ) || [ ] ) . length , 2 , '结构化与非结构化任务必须共用默认提示语' )
2026-07-13 11:47:33 +08:00
assert . match ( generationControlSource , /v-if="options\.qualityFilterEnabled"/ , '质量规则没有随总开关渐进显示' )
assert . match ( generationControlSource , /v-if="options\.filterShortContent"/ , '最少字数没有随短内容规则显示' )
assert . match ( generationControlSource , /:min="1"[\s\S]*:max="1000"/ , '最少字数缺少 1 到 1000 的边界限制' )
2026-07-13 17:12:49 +08:00
assert . match ( taskSetupSource , /qualityValidationMessage/ , '质量规则缺少继续前校验' )
2026-07-13 11:47:33 +08:00
assert . match ( viewSource , /useModelsStore/ , '创建页没有加载模型列表' )
2026-07-24 11:28:02 +08:00
const generationModelsStart = viewSource . indexOf ( 'const generationModels' )
const generationModelsEnd = viewSource . indexOf ( 'const taskSetupRef' , generationModelsStart )
const generationModelsSource = viewSource . slice ( generationModelsStart , generationModelsEnd )
assert . match ( generationModelsSource , /computed\(\(\) => modelList\.value\)/ , '数据生成模型候选没有直接使用模型管理完整列表' )
assert . doesNotMatch ( generationModelsSource , /\.filter\(|model\.type|model\.model_source|model\.status|model\.purpose/ , '数据生成模型候选仍按类型、来源、状态或用途静默过滤' )
assert . match ( viewSource , /modelsStore\.load\(true\)/ , '进入创建向导时没有强制刷新模型管理列表' )
2026-07-13 17:12:49 +08:00
assert . match ( viewSource , /<ModelSelectionStep[\s\S]*?:models="generationModels"/ , '创建页没有向独立大模型选择步骤传递模型列表' )
2026-07-24 11:28:02 +08:00
assert . match ( generationControlSource , /v-for="model in models \|\| \[\]"/ , '模型下拉没有遍历完整候选列表' )
assert . match ( generationControlSource , /:value="model\.id"/ , '模型下拉没有使用模型管理 ID 作为选中值' )
assert . match ( generationControlSource , /modelMeta\(model\)/ , '模型下拉缺少来源和类型说明' )
2026-07-13 11:47:33 +08:00
2026-07-11 14:49:10 +08:00
assert . match ( typesSource , /export interface UnstructuredProcessOptions/ , '缺少非结构化处理选项类型' )
2026-07-24 11:28:02 +08:00
assert . match (
typesSource ,
2026-07-25 18:00:44 +08:00
/export type ChunkMethod = 'layout_hybrid' \| 'semantic' \| 'fixed'/ ,
'非结构化切分方式类型必须只保留 layout_hybrid、semantic 和 fixed' ,
2026-07-24 11:28:02 +08:00
)
2026-07-11 14:49:10 +08:00
for ( const field of [
'preprocessOptions' ,
'chunkMethod' ,
'chunkSize' ,
'chunkOverlap' ,
'minChunkSize' ,
2026-07-25 18:00:44 +08:00
'semanticBreakpointPercentile' ,
2026-07-11 14:49:10 +08:00
'preserveTables' ,
'preserveCodeBlocks' ,
'preserveLists' ,
'semanticEnrichment' ,
'qaPairsPerChunk' ,
'datasetSplit' ,
] ) {
assert . ok ( typesSource . includes ( field ) , ` 非结构化处理选项缺少字段: ${ field } ` )
}
2026-07-12 15:39:43 +08:00
for ( const removedField of [ 'contextScope' , 'generationTypes' , 'skipUnanswerable' ] ) {
assert . ok ( ! typesSource . includes ( removedField ) , ` 简化后仍保留低频生成字段: ${ removedField } ` )
}
2026-07-11 14:49:10 +08:00
assert . match ( taskSetupSource , /v-if="processType === 'unstructured'"/ , '非结构化配置必须仅在非结构化数据类型下显示' )
2026-07-25 22:41:06 +08:00
assert . match ( taskSetupSource , /processTypeLocked\?: boolean/ , '任务配置缺少处理类型锁定状态' )
assert . match ( taskSetupSource , /:disabled="processTypeLocked"/ , '重新生成时处理类型选项没有禁用' )
assert . ok ( taskSetupSource . includes ( '重新生成沿用原任务处理类型,不可修改' ) , '处理类型锁定缺少明确说明' )
assert . match ( viewSource , /:process-type-locked="isRegeneration"/ , '创建向导没有向第一步传递重新生成锁定状态' )
2026-07-13 15:28:48 +08:00
assert . ok ( unstructuredOptionsSource . includes ( '智能预处理' ) , '简化后缺少智能预处理总开关' )
assert . ok ( unstructuredOptionsSource . includes ( '敏感信息脱敏' ) , '简化后缺少脱敏开关' )
2026-07-24 11:28:02 +08:00
const expectedSmartPreprocessOptions = [
'clean_invalid_content' ,
'detect_document_structure' ,
'merge_short_content' ,
'filter_low_quality' ,
'deduplicate_content' ,
'preserve_context' ,
]
const smartOptionsStart = unstructuredOptionsSource . indexOf ( 'const SMART_PREPROCESS_OPTIONS' )
const smartOptionsEnd = unstructuredOptionsSource . indexOf ( 'const CHUNK_METHODS' , smartOptionsStart )
const smartOptionsSource = unstructuredOptionsSource . slice ( smartOptionsStart , smartOptionsEnd )
const smartOptionValues = [ ... smartOptionsSource . matchAll ( /^\s*'([^']+)',?$/gm ) ] . map ( ( match ) => match [ 1 ] )
assert . deepEqual ( smartOptionValues , expectedSmartPreprocessOptions , '智能预处理内部值与后端语义不一致' )
assert . equal ( new Set ( smartOptionValues ) . size , smartOptionValues . length , '非结构化智能预处理 value 必须唯一' )
for ( const descriptionPart of [
2026-07-24 15:06:01 +08:00
'清理页眉页脚、页码、目录和无效内容' ,
2026-07-24 11:28:02 +08:00
'感知文档结构' ,
'合并短块' ,
'预过滤低质量内容' ,
'近重复去重' ,
'重叠保护上下文' ,
] ) {
assert . ok ( unstructuredOptionsSource . includes ( descriptionPart ) , ` 智能预处理说明缺少语义: ${ descriptionPart } ` )
}
assert . ok ( unstructuredOptionsSource . includes ( '姓名、手机号、邮箱和身份证号' ) , '非结构化脱敏说明缺少完整字段范围' )
assert . match ( unstructuredOptionsSource , /Array\.from\(new Set\(props\.options\.preprocessOptions\.filter\(/ , '非结构化预处理选中值没有去重' )
2026-07-13 15:28:48 +08:00
assert . match ( unstructuredOptionsSource , /const smartPreprocessEnabled = computed/ , '智能预处理没有映射到内部处理项' )
assert . match ( unstructuredOptionsSource , /function updateSmartPreprocess/ , '智能预处理开关缺少更新逻辑' )
assert . match ( unstructuredOptionsSource , /function updateDesensitize/ , '脱敏开关缺少更新逻辑' )
2026-07-11 14:49:10 +08:00
2026-07-13 15:28:48 +08:00
assert . ok ( unstructuredOptionsSource . includes ( '切分选项' ) , '非结构化配置缺少切分选项分类' )
2026-07-24 11:28:02 +08:00
const expectedChunkMethods = [
2026-07-25 18:00:44 +08:00
[ 'layout_hybrid' , '版面结构混合切分' ] ,
[ 'semantic' , '语义切分' ] ,
[ 'fixed' , '固定 Token 切分' ] ,
2026-07-24 11:28:02 +08:00
]
const chunkMethodsStart = unstructuredOptionsSource . indexOf ( 'const CHUNK_METHODS' )
const chunkMethodsEnd = unstructuredOptionsSource . indexOf ( 'const UNSTRUCTURED_NUMBER_LIMITS' , chunkMethodsStart )
const chunkMethodsSource = unstructuredOptionsSource . slice ( chunkMethodsStart , chunkMethodsEnd )
const chunkMethodValues = [ ... chunkMethodsSource . matchAll ( /value: '([^']+)'/g ) ] . map ( ( match ) => match [ 1 ] )
assert . deepEqual ( chunkMethodValues , expectedChunkMethods . map ( ( [ value ] ) => value ) , '切分方式值集合不准确' )
for ( const [ value , label ] of expectedChunkMethods ) {
assert . ok ( chunkMethodsSource . includes ( ` value: ' ${ value } ', label: ' ${ label } ' ` ) , ` 切分方式缺少选项: ${ label } ` )
}
2026-07-25 18:00:44 +08:00
for ( const removedMethod of [ 'structure' , 'custom' , 'heading' ] ) {
2026-07-24 11:28:02 +08:00
assert . ok ( ! chunkMethodsSource . includes ( ` value: ' ${ removedMethod } ' ` ) , ` 切分方式仍保留已移除值: ${ removedMethod } ` )
}
2026-07-25 18:00:44 +08:00
assert . ok ( unstructuredOptionsSource . includes ( '默认推荐,按标题、段落、列表和表格结构切分' ) , '版面结构混合切分缺少推荐说明' )
assert . ok ( unstructuredOptionsSource . includes ( '根据相邻内容的语义变化寻找主题边界' ) , '语义切分缺少适用场景说明' )
assert . ok ( unstructuredOptionsSource . includes ( '按句子边界控制固定 Token 长度' ) , '固定 Token 切分缺少稳定性说明' )
assert . doesNotMatch ( unstructuredOptionsSource , /自定义分隔符/ , '切分选项仍保留自定义分隔符' )
assert . doesNotMatch ( taskSetupSource , /customDelimiter|自定义分隔符/ , '任务配置仍校验已移除的自定义分隔符' )
2026-07-12 15:39:43 +08:00
for ( const label of [ '切片长度' , '重叠长度' , '最小切片长度' , '保护表格、代码和列表' ] ) {
2026-07-13 15:28:48 +08:00
assert . ok ( unstructuredOptionsSource . includes ( label ) , ` 切分选项缺少配置: ${ label } ` )
}
2026-07-13 17:12:49 +08:00
assert . doesNotMatch ( unstructuredOptionsSource , /advancedChunkSettingsOpen|>高级设置</ , '切分核心参数不应再隐藏在高级设置中' )
2026-07-13 15:28:48 +08:00
assert . match ( taskSetupSource , /if \(chunkValidationMessage\.value\) \{[\s\S]*?revealValidation\(\)[\s\S]*?return false/ , '高级切分配置校验失败时没有重新展开定位' )
assert . match ( unstructuredOptionsSource , /const preserveSpecialContentEnabled = computed/ , '特殊内容保护没有合并为单一开关' )
assert . match ( unstructuredOptionsSource , /function updateSpecialContentProtection/ , '特殊内容保护开关缺少更新逻辑' )
assert . match ( unstructuredOptionsSource , /options\.chunkSize[\s\S]*?:min="200"[\s\S]*?:max="2000"/ , '切片长度必须限制在 200 到 2000 Token' )
assert . match ( unstructuredOptionsSource , /options\.chunkOverlap[\s\S]*?:min="0"[\s\S]*?:max="500"/ , '重叠长度必须限制在 0 到 500 Token' )
assert . match ( unstructuredOptionsSource , /options\.minChunkSize[\s\S]*?:min="20"[\s\S]*?:max="500"/ , '最小切片长度必须限制在 20 到 500 Token' )
2026-07-25 18:00:44 +08:00
assert . match ( unstructuredOptionsSource , /options\.semanticBreakpointPercentile[\s\S]*?:min="1"[\s\S]*?:max="99"/ , '语义断点百分位必须限制在 1 到 99' )
assert . match ( unstructuredOptionsSource , /v-if="options\.chunkMethod === 'semantic'"[\s\S]*?options\.semanticBreakpointPercentile/ , '语义断点百分位必须仅在语义切分时显示' )
2026-07-11 14:49:10 +08:00
2026-07-13 17:12:49 +08:00
for ( const label of [ '每个切片生成数量' , '数据集划分' ] ) {
2026-07-13 15:28:48 +08:00
assert . ok ( taskSetupFeatureSource . includes ( label ) , ` 非结构化生成选项缺少: ${ label } ` )
2026-07-11 14:49:10 +08:00
}
2026-07-12 15:39:43 +08:00
for ( const removedLabel of [ '上下文范围' , '问题类型' , '跳过无法回答的内容' ] ) {
2026-07-13 15:28:48 +08:00
assert . ok ( ! taskSetupFeatureSource . includes ( removedLabel ) , ` 简化后仍显示低频选项: ${ removedLabel } ` )
2026-07-12 15:39:43 +08:00
}
2026-07-27 09:11:58 +08:00
assert . match ( unstructuredOptionsSource , /qaPairsPerChunk:\s*QA_PAIRS_GENERATION_LIMITS/ , '每切片生成数量裁剪必须使用统一的 1 到 50 限制' )
assert . match ( unstructuredOptionsSource , /options\.qaPairsPerChunk[\s\S]*?:min="QA_PAIRS_GENERATION_LIMITS\.min"[\s\S]*?:max="QA_PAIRS_GENERATION_LIMITS\.max"/ , '每个切片生成数量必须使用统一的 1 到 50 限制' )
assert . match ( unstructuredOptionsSource , /支持 1~ 50 条;数量越大,处理耗时和 Token 消耗越高/ , '非结构化生成数量缺少耗时与 Token 消耗说明' )
2026-07-11 14:49:10 +08:00
assert . match ( taskSetupSource , /unstructuredSplitTotal\.value !== 100/ , '非结构化数据集划分缺少总和 100% 校验' )
assert . match ( taskSetupSource , /chunkOverlap \+ props\.unstructuredOptions\.minChunkSize[\s\S]*?> props\.unstructuredOptions\.chunkSize/ , '切分配置未校验重叠长度与最小切片长度的组合边界' )
2026-07-13 15:28:48 +08:00
assert . ok ( unstructuredOptionsSource . includes ( 'Token 数为轻量估算值' ) , '切片长度缺少 Token 估算说明' )
assert . match ( unstructuredOptionsSource , /\.chunk-settings-grid\s*\{[\s\S]*?grid-template-columns:\s*repeat\(3,\s*minmax\(0,\s*1fr\)\)/ , '核心切分参数没有收紧为三列布局' )
2026-07-11 14:49:10 +08:00
assert . match ( viewSource , /const unstructuredOptions = ref<UnstructuredProcessOptions>/ , '父页面缺少非结构化配置状态' )
2026-07-25 18:00:44 +08:00
assert . match ( stateSource , /chunkMethod:\s*'layout_hybrid'/ , '非结构化默认切分方式必须为版面结构混合切分' )
2026-07-13 15:28:48 +08:00
assert . match ( stateSource , /chunkSize:\s*800/ , '默认切片长度必须为 800 Token' )
assert . match ( stateSource , /chunkOverlap:\s*100/ , '默认重叠长度必须为 100 Token' )
assert . match ( stateSource , /minChunkSize:\s*100/ , '默认最小切片长度必须为 100 Token' )
2026-07-25 18:00:44 +08:00
assert . match ( stateSource , /semanticBreakpointPercentile:\s*95/ , '默认语义断点百分位必须为 95' )
2026-07-13 15:28:48 +08:00
assert . match ( stateSource , /qaPairsPerChunk:\s*1/ , '默认每个切片必须生成 1 个问答对' )
2026-07-27 09:11:58 +08:00
assert . match ( stateSource , /qaPairsPerRow:\s*1/ , '默认每行必须生成 1 个问答对' )
2026-07-11 14:49:10 +08:00
assert . match ( viewSource , /v-model:unstructured-options="unstructuredOptions"/ , '父页面没有双向绑定非结构化配置' )
assert . match ( viewSource , /JSON\.stringify\(previewAffectingOptions\(\)\)/ , '影响切分的非结构化配置没有纳入预览失效判断' )
2026-07-24 11:28:02 +08:00
function defaultPreprocessValues ( functionName , nextFunctionName ) {
const start = stateSource . indexOf ( ` export function ${ functionName } ` )
const end = nextFunctionName ? stateSource . indexOf ( ` export function ${ nextFunctionName } ` , start ) : stateSource . length
const functionSource = stateSource . slice ( start , end )
const match = functionSource . match ( /preprocessOptions:\s*\[([\s\S]*?)\]/ )
assert . ok ( match , ` ${ functionName } 缺少 preprocessOptions 默认值 ` )
return [ ... match [ 1 ] . matchAll ( /'([^']+)'/g ) ] . map ( ( item ) => item [ 1 ] )
}
const defaultStructuredPreprocess = defaultPreprocessValues (
'createDefaultStructuredOptions' ,
'createDefaultUnstructuredOptions' ,
)
assert . deepEqual (
defaultStructuredPreprocess ,
[ 'clean_invalid' , 'detect_structure' , 'deduplicate' , 'normalize_format' ] ,
'结构化默认预处理配置不准确' ,
)
assert . equal ( new Set ( defaultStructuredPreprocess ) . size , defaultStructuredPreprocess . length , '结构化默认预处理值重复' )
const defaultUnstructuredPreprocess = defaultPreprocessValues ( 'createDefaultUnstructuredOptions' )
assert . deepEqual ( defaultUnstructuredPreprocess , expectedSmartPreprocessOptions , '智能预处理默认值不完整' )
assert . equal ( new Set ( defaultUnstructuredPreprocess ) . size , defaultUnstructuredPreprocess . length , '非结构化默认预处理值重复' )
const backendConfigStart = viewSource . indexOf ( 'function toBackendConfig()' )
const backendConfigEnd = viewSource . indexOf ( 'function taskPayload()' , backendConfigStart )
assert . ok ( backendConfigStart >= 0 && backendConfigEnd > backendConfigStart , '缺少任务后端配置映射' )
const backendConfigSource = viewSource . slice ( backendConfigStart , backendConfigEnd )
assert . ok ( backendConfigSource . includes ( 'preprocess_options: [...options.preprocessOptions]' ) , '预处理选项没有完整传入任务配置' )
assert . ok ( backendConfigSource . includes ( 'dataset_split: { ...options.datasetSplit }' ) , '数据集划分没有完整传入任务配置' )
for ( const [ backendField , frontendField ] of [
[ 'semantic_enrichment' , 'semanticEnrichment' ] ,
[ 'generation_model_id' , 'generationModelId' ] ,
[ 'generation_prompt' , 'generationPrompt' ] ,
[ 'temperature' , 'temperature' ] ,
[ 'max_tokens' , 'maxTokens' ] ,
[ 'json_mode' , 'jsonMode' ] ,
[ 'quality_filter_enabled' , 'qualityFilterEnabled' ] ,
[ 'filter_low_quality' , 'filterLowQuality' ] ,
[ 'filter_short_content' , 'filterShortContent' ] ,
[ 'min_output_length' , 'minOutputLength' ] ,
] ) {
assert . ok (
backendConfigSource . includes ( ` ${ backendField } : options. ${ frontendField } ` ) ,
` 公共配置 ${ frontendField } 没有传入 task payload ` ,
)
}
for ( const [ backendField , frontendField ] of [
[ 'chunk_method' , 'chunkMethod' ] ,
[ 'chunk_size' , 'chunkSize' ] ,
[ 'chunk_overlap' , 'chunkOverlap' ] ,
[ 'min_chunk_size' , 'minChunkSize' ] ,
2026-07-25 18:00:44 +08:00
[ 'semantic_breakpoint_percentile' , 'semanticBreakpointPercentile' ] ,
2026-07-24 11:28:02 +08:00
[ 'preserve_tables' , 'preserveTables' ] ,
[ 'preserve_code_blocks' , 'preserveCodeBlocks' ] ,
[ 'preserve_lists' , 'preserveLists' ] ,
[ 'qa_pairs_per_chunk' , 'qaPairsPerChunk' ] ,
] ) {
assert . ok (
backendConfigSource . includes ( ` ${ backendField } : unstructuredOptions.value. ${ frontendField } ` ) ,
` 非结构化配置 ${ frontendField } 没有传入 task payload ` ,
)
}
assert . ok (
backendConfigSource . includes ( 'qa_pairs_per_row: structuredOptions.value.qaPairsPerRow' ) ,
'结构化每行生成数量没有传入 task payload' ,
)
const taskPayloadStart = viewSource . indexOf ( 'function taskPayload()' )
const taskPayloadEnd = viewSource . indexOf ( 'function externalPayload()' , taskPayloadStart )
const taskPayloadSource = viewSource . slice ( taskPayloadStart , taskPayloadEnd )
2026-07-25 22:41:06 +08:00
for ( const marker of [ 'name: task.name.trim()' , 'description: task.description.trim()' , 'process_type: originalProcessType.value || processType.value' , 'config: toBackendConfig()' ] ) {
2026-07-24 11:28:02 +08:00
assert . ok ( taskPayloadSource . includes ( marker ) , ` 任务创建 payload 缺少: ${ marker } ` )
}
2026-07-25 22:41:06 +08:00
assert . match ( stateSource , /Object\.prototype\.hasOwnProperty\.call\(config, key\)/ , '配置反向映射没有区分缺失值与 false/0' )
assert . match ( stateSource , /Number\.isFinite\(value\) \? value : fallback/ , '配置反向映射没有保留合法数字 0' )
2026-07-27 09:11:58 +08:00
assert . match ( stateSource , /qaPairsPerRow:\s*normalizeQaPairsGenerationCount\([\s\S]*?qa_pairs_per_row[\s\S]*?defaults\.qaPairsPerRow/ , '结构化生成数量回填没有按 1 到 50 归一化' )
assert . match ( stateSource , /qaPairsPerChunk:\s*normalizeQaPairsGenerationCount\([\s\S]*?qa_pairs_per_chunk[\s\S]*?defaults\.qaPairsPerChunk/ , '非结构化生成数量回填没有按 1 到 50 归一化' )
2026-07-25 22:41:06 +08:00
assert . match ( stateSource , /createStructuredOptionsFromConfig/ , '结构化配置缺少后端到表单的反向映射' )
assert . match ( stateSource , /createUnstructuredOptionsFromConfig/ , '非结构化配置缺少后端到表单的反向映射' )
assert . match ( stateSource , /configValue<unknown>\(config, 'preprocess_options', \[\]\)/ , '历史任务缺少预处理配置时必须按后端空列表语义回填' )
for ( const [ field , fallback ] of [
[ 'preserve_tables' , 'preserveTables' ] ,
[ 'preserve_code_blocks' , 'preserveCodeBlocks' ] ,
[ 'preserve_lists' , 'preserveLists' ] ,
] ) {
assert . match (
stateSource ,
new RegExp ( ` configValue \\ ( \\ s*config, \\ s*' ${ field } ',[ \\ s \\ S]*?defaults \\ . ${ fallback } ` ) ,
` 历史任务缺少 ${ field } 时必须沿用表单默认值,同时保留显式 false ` ,
)
}
assert . match ( regenerationSource , /getDataProcessTask\(sourceTaskId\.value\)/ , '重新生成没有加载原任务' )
assert . match ( regenerationSource , /while \(true\)[\s\S]*?getDataProcessSourceContent[\s\S]*?has_more/ , '重新生成没有分页加载完整源正文' )
assert . match ( regenerationSource , /getDataProcessPreview\(taskId, \{ page: 1, page_size: 500 \}\)[\s\S]*?for \(let page = 2; page <= pages;/ , '重新生成没有分页加载全部现有切片' )
assert . match ( viewSource , /if \(hydrating\.value\) return/ , '任务水合期间仍可能触发重置副作用' )
assert . match ( regenerationSource , /currentSignature !== originalPreviewConfigSignature\.value[\s\S]*?currentSignature === confirmedPreviewConfigSignature\.value/ , '切分变更确认没有按原签名和已确认签名去重' )
assert . match ( regenerationSource , /if \(!bindings\.previewItems\.value\.length\) return true/ , '没有现有切片时仍会弹出删除切片警告' )
2026-07-27 10:43:48 +08:00
assert . ok ( regenerationSource . includes ( '点击“开始生成”前,原生成结果和已发布数据会继续保留' ) , '切分变更警告没有说明开始生成前原结果与发布数据仍保留' )
assert . doesNotMatch ( regenerationSource , /原切片、原生成结果/ , '切分变更警告不应承诺实际重切后仍保留原切片' )
assert . doesNotMatch ( regenerationSource , /修改预处理或切分配置将删除现有切片和生成结果/ , '准备配置阶段仍误称会立即删除原数据' )
2026-07-25 22:41:06 +08:00
assert . match ( regenerationSource , /regenerateDataProcessTask\(sourceTaskId\.value,[\s\S]*?expected_updated_at: originalTaskUpdatedAt\.value/ , '第二步没有携带最新乐观并发版本重新生成任务' )
assert . match ( regenerationSource , /originalTaskUpdatedAt\.value = regenerated\.task\.updated_at/ , '重新生成成功后没有更新下一次提交的并发版本' )
assert . match ( regenerationSource , /if \(regenerationPrepared\.value\) \{[\s\S]*?getDataProcessTask\(sourceTaskId\.value\)[\s\S]*?originalTaskUpdatedAt\.value = latestTask\.updated_at/ , '服务端中间操作后再次提交没有刷新任务并发版本' )
assert . match ( regenerationSource , /regenerationPrepared\.value = true/ , '重新生成提交成功后没有记录服务端已变更状态' )
assert . match ( regenerationSource , /hydrateWorkspace\(regeneratedTask, !regenerated\.preview_invalidated\)/ , '重新生成没有按 preview_invalidated 决定保留或清空切片' )
2026-07-27 10:43:48 +08:00
assert . match ( regenerationSource , /重新生成配置已保存,但工作区恢复失败/ , '重新生成配置已保存但水合失败时缺少可恢复错误状态' )
2026-07-25 22:41:06 +08:00
assert . match ( regenerationSource , /return chunks\.join\(''\)/ , '分页恢复源正文时不应额外插入换行' )
assert . doesNotMatch ( regenerationSource , /binaryDocument[\s\S]*?mapDataProcessSourceFile\(file, ''\)/ , '二进制源正文加载失败时不能静默降级为空内容' )
assert . match ( nextFromModelSource , /if \(isRegeneration\.value\) \{[\s\S]*?prepareRegeneration\(taskPayload\(\)\)/ , '重新生成每次从模型步骤继续时没有调用专用接口' )
assert . doesNotMatch ( nextFromModelSource , /isRegeneration\.value && !taskId\.value/ , '重新生成提交一次后可能错误转为普通任务更新' )
2026-07-27 10:43:48 +08:00
assert . ok ( regenerationSource . includes ( '重新生成配置已保存,但尚未开始生成' ) , '准备完成后离开提示没有区分尚未开始生成' )
assert . ok ( regenerationSource . includes ( '返回详情不会替换原生成结果或已发布数据' ) , '准备完成后离开提示未说明原详情仍保留' )
assert . match ( viewSource , /const leaveConfirmText = computed\(\(\) => isRegeneration\.value \? '返回详情' : '放弃修改'\)/ , '重新生成离开按钮仍使用误导性文案' )
assert . match ( regenerationSource , /async function returnToDetail\(\)[\s\S]*?router\.replace\(\{ name: 'data-process-detail', params: \{ id: sourceTaskId\.value \} \}\)/ , '重新生成退出没有显式返回原任务详情' )
assert . match ( viewSource , /async function returnToPreviousPage\(\)[\s\S]*?isRegeneration\.value && await returnToDetail\(\)/ , '退出按钮没有使用显式返回详情逻辑' )
assert . match ( viewSource , /\{\{ isRegeneration \? '返回详情' : '取消' \}\}/ , '重新生成第一步退出按钮没有明确标识返回详情' )
assert . match ( regenerationSource , /async function confirmStartGeneration\([\s\S]*?开始重新生成?[\s\S]*?当前生成结果将被替换[\s\S]*?confirmText: '开始生成'[\s\S]*?syncPreviewChanges\(\)/ , '开始生成前没有明确破坏性边界或同步预览修改' )
assert . match ( viewSource , /beforeGenerate: beforeStartGeneration[\s\S]*?async function beforeStartGeneration\(\)[\s\S]*?confirmStartGeneration\([\s\S]*?syncPreviewChanges/ , '生成流程没有在真实开始前调用确认边界' )
assert . match ( generationSource , /const canStart = await bindings\.beforeGenerate\?\.\(\)[\s\S]*?if \(canStart === false\) return[\s\S]*?generateDataProcess\(taskId\)/ , '用户取消开始生成时仍可能调用真实生成 API' )
2026-07-25 22:41:06 +08:00
assert . match ( viewSource , /v-if="initializationError"[\s\S]*?@click="loadRegenerationSource"[\s\S]*?重试加载原任务/ , '原任务初始化失败后缺少安全重试入口' )
assert . match ( regenerationSource , /previewStatus = preservePreviews && count > 0 \? 'success' : 'waiting'/ , '保留切片时没有跳过重切,或切片失效后未回到等待状态' )
2026-07-24 11:28:02 +08:00
2026-07-25 22:41:06 +08:00
const previewOptionsStart = stateSource . indexOf ( 'export function previewAffectingOptionsFor(' )
const previewOptionsEnd = stateSource . indexOf ( 'export function generationAffectingOptionsFor(' , previewOptionsStart )
2026-07-11 14:49:10 +08:00
assert . ok ( previewOptionsStart >= 0 && previewOptionsEnd > previewOptionsStart , '缺少预览影响配置签名函数' )
2026-07-25 22:41:06 +08:00
const previewOptionsSource = stateSource . slice ( previewOptionsStart , previewOptionsEnd )
2026-07-11 14:49:10 +08:00
for ( const field of [
'preprocessOptions' ,
'chunkMethod' ,
'chunkSize' ,
'chunkOverlap' ,
'minChunkSize' ,
2026-07-25 18:00:44 +08:00
'semanticBreakpointPercentile' ,
2026-07-11 14:49:10 +08:00
'preserveTables' ,
'preserveCodeBlocks' ,
'preserveLists' ,
] ) {
assert . ok ( previewOptionsSource . includes ( field ) , ` 预览签名缺少切分影响字段: ${ field } ` )
}
2026-07-12 15:39:43 +08:00
for ( const field of [ 'semanticEnrichment' , 'qaPairsPerChunk' , 'datasetSplit' ] ) {
2026-07-11 14:49:10 +08:00
assert . ok ( ! previewOptionsSource . includes ( field ) , ` 生成字段 ${ field } 不应导致预览重建并丢失编辑 ` )
}
2026-07-25 22:41:06 +08:00
const generationOptionsStart = stateSource . indexOf ( 'export function generationAffectingOptionsFor(' )
const generationOptionsEnd = stateSource . length
2026-07-11 14:49:10 +08:00
assert . ok ( generationOptionsStart >= 0 && generationOptionsEnd > generationOptionsStart , '缺少生成影响配置签名函数' )
2026-07-25 22:41:06 +08:00
const generationOptionsSource = stateSource . slice ( generationOptionsStart , generationOptionsEnd )
2026-07-13 11:47:33 +08:00
for ( const field of [
'semanticEnrichment' ,
'qaPairsPerChunk' ,
'datasetSplit' ,
'generationModelId' ,
'generationPrompt' ,
'qualityFilterEnabled' ,
'filterLowQuality' ,
'filterShortContent' ,
'minOutputLength' ,
] ) {
2026-07-11 14:49:10 +08:00
assert . ok ( generationOptionsSource . includes ( field ) , ` 生成签名缺少字段: ${ field } ` )
}
assert . match (
viewSource ,
/watch\(generationOptionsSignature,[\s\S]*?resetDownstream\(\)/ ,
'生成配置变化后没有仅失效下游结果' ,
)
for ( const mutationFunction of [
'updatePreviewContent' ,
'restorePreviewItem' ,
'addPreviewItem' ,
'removePreviewItem' ,
] ) {
const mutationStart = viewSource . indexOf ( ` function ${ mutationFunction } ` )
const mutationEnd = viewSource . indexOf ( '\nfunction ' , mutationStart + 1 )
const mutationSource = viewSource . slice ( mutationStart , mutationEnd === - 1 ? undefined : mutationEnd )
assert . ok ( mutationSource . includes ( 'resetDownstream()' ) , ` 预览变更 ${ mutationFunction } 后没有失效旧生成结果 ` )
}
2026-07-23 15:10:13 +08:00
assert . doesNotMatch ( modelSource , /createResults\(/ , '纯预览映射模块不应承担结果生成职责' )
2026-07-11 14:49:10 +08:00
2026-07-10 16:46:23 +08:00
function findNextStyleBlockStart ( source , startIndex ) {
let quote = null
for ( let index = startIndex ; index < source . length ; index += 1 ) {
const character = source [ index ]
const nextCharacter = source [ index + 1 ]
if ( quote ) {
if ( character === '\\' ) {
index += 1
} else if ( character === quote ) {
quote = null
}
continue
}
if ( character === '/' && nextCharacter === '*' ) {
const commentEnd = source . indexOf ( '*/' , index + 2 )
index = commentEnd === - 1 ? source . length : commentEnd + 1
continue
}
if ( character === '/' && nextCharacter === '/' ) {
const commentEnd = source . indexOf ( '\n' , index + 2 )
index = commentEnd === - 1 ? source . length : commentEnd
continue
}
if ( character === '\'' || character === '"' ) {
quote = character
continue
}
if ( character === '{' ) return index
}
return - 1
}
function findStyleBlockEnd ( source , blockStart ) {
let depth = 0
let quote = null
for ( let index = blockStart ; index < source . length ; index += 1 ) {
const character = source [ index ]
const nextCharacter = source [ index + 1 ]
if ( quote ) {
if ( character === '\\' ) {
index += 1
} else if ( character === quote ) {
quote = null
}
continue
}
if ( character === '/' && nextCharacter === '*' ) {
const commentEnd = source . indexOf ( '*/' , index + 2 )
index = commentEnd === - 1 ? source . length : commentEnd + 1
continue
}
if ( character === '/' && nextCharacter === '/' ) {
const commentEnd = source . indexOf ( '\n' , index + 2 )
index = commentEnd === - 1 ? source . length : commentEnd
continue
}
if ( character === '\'' || character === '"' ) {
quote = character
continue
}
if ( character === '{' ) {
depth += 1
} else if ( character === '}' && -- depth === 0 ) {
return index
}
}
return - 1
}
function resolveNestedSelector ( selector , parentSelector ) {
if ( ! parentSelector ) return selector
if ( selector . includes ( '&' ) ) return selector . replace ( /&/g , parentSelector )
return ` ${ parentSelector } ${ selector } `
}
function collectStyleRules ( source , parentSelector = '' ) {
const rules = [ ]
let ruleStart = 0
let cursor = 0
while ( cursor < source . length ) {
const blockStart = findNextStyleBlockStart ( source , cursor )
if ( blockStart === - 1 ) break
const blockEnd = findStyleBlockEnd ( source , blockStart )
if ( blockEnd === - 1 ) break
const rawSelector = source . slice ( ruleStart , blockStart ) . trim ( )
const declarations = source . slice ( blockStart + 1 , blockEnd )
if ( rawSelector ) {
const isAtRule = rawSelector . startsWith ( '@' )
const selector = isAtRule ? rawSelector : resolveNestedSelector ( rawSelector , parentSelector )
rules . push ( { selector , declarations } )
rules . push ( ... collectStyleRules ( declarations , isAtRule ? parentSelector : selector ) )
}
cursor = blockEnd + 1
ruleStart = cursor
}
return rules
}
function directStyleDeclarations ( source ) {
let result = ''
let nestedDepth = 0
let quote = null
for ( let index = 0 ; index < source . length ; index += 1 ) {
const character = source [ index ]
const nextCharacter = source [ index + 1 ]
if ( quote ) {
if ( character === '\\' ) {
index += 1
} else if ( character === quote ) {
quote = null
}
if ( nestedDepth === 0 ) result += ' '
continue
}
if ( character === '/' && nextCharacter === '*' ) {
const commentEnd = source . indexOf ( '*/' , index + 2 )
index = commentEnd === - 1 ? source . length : commentEnd + 1
if ( nestedDepth === 0 ) result += ' '
continue
}
if ( character === '/' && nextCharacter === '/' ) {
const commentEnd = source . indexOf ( '\n' , index + 2 )
index = commentEnd === - 1 ? source . length : commentEnd
if ( nestedDepth === 0 ) result += ' '
continue
}
if ( character === '\'' || character === '"' ) {
quote = character
if ( nestedDepth === 0 ) result += ' '
continue
}
if ( character === '{' ) {
nestedDepth += 1
if ( nestedDepth === 1 ) result += ' '
continue
}
if ( character === '}' ) {
nestedDepth = Math . max ( 0 , nestedDepth - 1 )
if ( nestedDepth === 0 ) result += ' '
continue
}
if ( nestedDepth === 0 ) result += character
}
return result
}
const nestedUploadedFileItemsStyles = collectStyleRules ( `
. upload - context {
. uploaded - file {
& - items {
max - height : 20 rem ;
}
@ media ( min - width : 1 px ) {
& - items {
overflow : auto ;
}
}
@ supports ( display : grid ) {
& - items {
overflow - x : hidden ;
overflow - y : auto ;
}
}
}
}
` ).filter(({ selector }) => /(?:^|[^ \w -]) \. uploaded-file-items(?![ \w -])/.test(selector))
assert . equal (
nestedUploadedFileItemsStyles . length ,
3 ,
'嵌套的 &-items 选择器必须展开为 .uploaded-file-items, 且不能被 at-rule 上下文遮蔽' ,
)
for ( const property of [ 'max-height' , 'overflow' , 'overflow-x' , 'overflow-y' ] ) {
assert . ok (
nestedUploadedFileItemsStyles . some ( ( { declarations } ) =>
new RegExp ( ` (?:^|;) \\ s* ${ property } \\ s*: ` , 'i' ) . test ( directStyleDeclarations ( declarations ) ) ,
) ,
` 嵌套 .uploaded-file-items 必须识别受限样式属性: ${ property } ` ,
)
}
for ( const marker of [
'uploaded-file-list-header' ,
'uploaded-file-items' ,
2026-07-24 11:28:02 +08:00
'已选择 {{ uploadedFiles.length }} 个文件' ,
2026-07-10 16:46:23 +08:00
':title="file.name"' ,
] ) {
2026-07-12 15:39:43 +08:00
assert . ok ( sourceUploadSource . includes ( marker ) , ` 源数据文件列表缺少: ${ marker } ` )
2026-07-10 16:46:23 +08:00
}
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/^const[ \t]+FILE_PAGE_SIZE[ \t]*=[ \t]*10[ \t]*;?[ \t]*$/m ,
'文件分页大小必须固定为整数 10' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/const pagedUploadedFiles\s*=\s*computed\(\(\)\s*=>\s*\{\s*const start = \(currentFilePage\.value - 1\) \* FILE_PAGE_SIZE\s*return props\.uploadedFiles\.slice\(start, start \+ FILE_PAGE_SIZE\)\s*\}\)/ ,
'文件分页必须按当前页偏移切片完整文件列表' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/watch\(\(\)\s*=>\s*props\.uploadedFiles\.length,\s*\(newLength, oldLength\)\s*=>\s*\{[\s\S]*?if \(newLength > oldLength\)\s*\{\s*currentFilePage\.value = totalPages[\s\S]*?\}[\s\S]*?currentFilePage\.value = Math\.min\(currentFilePage\.value, totalPages\)[\s\S]*?\}\)/ ,
'文件数变化时必须新增跳至末页、删除回退到有效页' ,
)
2026-07-12 15:39:43 +08:00
const filePaginationTags = [ ... sourceUploadSource . matchAll ( /<el-pagination\b[\s\S]*?\/>/g ) ]
2026-07-11 11:00:15 +08:00
assert . ok ( filePaginationTags . length >= 1 , '文件列表必须包含分页器' )
for ( const [ filePaginationTag ] of filePaginationTags ) {
for ( const attribute of [
'v-if="uploadedFiles.length > FILE_PAGE_SIZE"' ,
'v-model:current-page="currentFilePage"' ,
':page-size="FILE_PAGE_SIZE"' ,
':total="uploadedFiles.length"' ,
] ) {
assert . ok ( filePaginationTag . includes ( attribute ) , ` 文件分页器缺少属性: ${ attribute } ` )
}
2026-07-10 16:46:23 +08:00
}
2026-07-12 15:39:43 +08:00
const { descriptor : sourceUploadDescriptor } = parseSfc ( sourceUploadSource , { filename : sourceUploadPath } )
const uploadedFileItemsStyles = sourceUploadDescriptor . styles
2026-07-10 16:46:23 +08:00
. flatMap ( ( { content } ) => collectStyleRules ( content ) )
. filter ( ( { selector } ) => / ( ? : ^ | [ ^ \ w - ] ) \ . uploaded - file - items ( ? ! [ \ w - ] ) / . test ( selector ) )
for ( const { declarations } of uploadedFileItemsStyles ) {
assert . doesNotMatch (
directStyleDeclarations ( declarations ) ,
/(?:^|;)\s*(?:max-height|overflow|overflow-x|overflow-y)\s*:/i ,
'文件列表不能用内部滚动替代分页' ,
)
}
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/<el-upload\s+v-if="uploadedFiles\.length === 0"[\s\S]*?<\/el-upload>\s*<section\s+v-else\s+class="uploaded-file-list"\s+aria-label="已上传文件列表">/ ,
'有文件状态缺少带 aria-label="已上传文件列表" 的语义列表容器' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-24 11:28:02 +08:00
/<div\s+class="uploaded-file-list-header">[\s\S]*?已选择 \{\{ uploadedFiles\.length \}\} 个文件[\s\S]*?正在逐个上传/ ,
2026-07-10 16:46:23 +08:00
'文件列表标题结构或文件数量文案缺失' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/<div\s+class="uploaded-file-items">\s*<div\s+v-for="file in pagedUploadedFiles"[^>]*class="uploaded-file">/ ,
'文件列表缺少分页后的文件行容器' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-24 11:28:02 +08:00
/<el-upload\s+v-if="uploadedFiles\.length === 0"\s+drag\s+multiple\s+:accept="uploadAccept"\s+:disabled="previewBuilding"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>/ ,
2026-07-10 16:46:23 +08:00
'无文件时未保留原有大拖拽上传区或上传配置' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-24 11:28:02 +08:00
/<el-upload\s+v-if="uploadedFiles\.length === 0"[\s\S]*?<template\s+#tip>[\s\S]*?processType === 'unstructured'/ ,
'无文件时大拖拽上传区缺少按处理类型展示的格式提示' ,
2026-07-10 16:46:23 +08:00
)
2026-07-24 11:28:02 +08:00
assert . ok (
sourceUploadSource . includes ( "? '.txt,.md,.markdown,.pdf,.docx,.pptx,.json,.jsonl,.ndjson'" ) ,
'非结构化上传 accept 不完整' ,
)
assert . ok (
sourceUploadSource . includes ( ": '.json,.jsonl,.ndjson,.csv,.tsv,.xlsx'" ) ,
'结构化上传 accept 不完整' ,
)
assert . ok ( sourceUploadSource . includes ( '旧版 DOC/PPT 请先转换' ) , '非结构化格式提示没有说明旧版 DOC/PPT 需转换' )
assert . ok ( sourceUploadSource . includes ( '旧版 XLS 请先转换' ) , '结构化格式提示没有说明旧版 XLS 需转换' )
2026-07-10 16:46:23 +08:00
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-24 11:28:02 +08:00
/<div\s+class="uploaded-file-list-header">[\s\S]*?已选择 \{\{ uploadedFiles\.length \}\} 个文件[\s\S]*?<div\s+class="continue-upload">\s*<el-upload\s+multiple\s+:accept="uploadAccept"\s+:disabled="previewBuilding"\s+:auto-upload="false"\s+:show-file-list="false"\s+:on-change="\(file: UploadFile\) => emit\('file-change', file\)"[^>]*>\s*<el-button\s+size="small"\s+type="primary"\s+:disabled="previewBuilding">继续上传<\/el-button>\s*<\/el-upload>\s*<\/div>\s*<\/div>/ ,
2026-07-10 16:46:23 +08:00
'有文件时缺少标题右侧的继续上传触发器或上传配置' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/\.uploaded-file-list-header\s*\{[^}]*display:\s*flex[^}]*justify-content:\s*space-between/ ,
'文件列表标题未布局为右侧继续上传按钮' ,
)
assert . match (
2026-07-12 15:39:43 +08:00
sourceUploadSource ,
2026-07-10 16:46:23 +08:00
/\.continue-upload\s+:deep\(\.el-upload\)\s*\{[^}]*width:\s*auto;?[^}]*margin-top:\s*0;?/ ,
'继续上传未覆盖内层上传节点的宽度和顶部间距' ,
)
2026-07-12 15:39:43 +08:00
assert . match ( sourceUploadSource , /\.uploaded-file\s*\{[^}]*min-height:\s*48px/ , '文件行没有保持 48px 最小高度' )
2026-07-24 11:28:02 +08:00
assert . match ( sourceUploadSource , /success:\s*\{ label: '切分完成', icon: 'fa-check-circle' \}/ , '文件行缺少切分成功状态' )
assert . match ( sourceUploadSource , /previewBuilding/ , '上传组件没有接收逐文件预览构建状态' )
assert . match ( sourceUploadSource , /sourceUploading/ , '上传组件没有接收串行上传状态' )
assert . match (
sourceUploadSource ,
/<el-progress[\s\S]*?:percentage="getFileBarPercentage\(file\)"[\s\S]*?:indeterminate="isFileProcessing\(file\)"/ ,
'文件行缺少上传与切分阶段的独立进度' ,
)
for ( const label of [ '等待上传' , '正在上传' , '上传失败' , '等待切分' , '正在切分' , '切分完成' , '切分失败' ] ) {
assert . ok ( sourceUploadSource . includes ( ` label: ' ${ label } ' ` ) , ` 文件行缺少状态: ${ label } ` )
}
assert . match ( sourceUploadSource , /file\.uploadProgress/ , '文件行没有使用真实上传进度' )
assert . match ( sourceUploadSource , /file\.uploadError \|\| file\.previewError/ , '文件行没有按阶段展示上传或切分失败原因' )
assert . match ( sourceUploadSource , /file\.previewError/ , '文件行没有展示逐文件失败原因以支持重试' )
2026-07-12 15:39:43 +08:00
assert . match ( sourceUploadSource , /@click="emit\('remove-file', file\.uid\)"/ , '文件行缺少 remove-file 删除动作' )
2026-07-10 16:46:23 +08:00
const { descriptor } = parseSfc ( viewSource , { filename : viewPath } )
const template = descriptor . template ? . content || ''
assert . equal ( ( template . match ( /class="wizard-primary-action"/g ) || [ ] ) . length , 1 , '页面必须只有一个主操作入口' )
2026-07-23 15:10:13 +08:00
assert . match ( viewSource , /onBeforeUnmount\(\(\) => \{[\s\S]*?stopGenerationTimer\(\)[\s\S]*?\}\)/ , '生成轮询计时器没有在卸载时清理' )
2026-07-10 16:46:23 +08:00
assert . match ( viewSource , /function scrollToStepTop/ , '步骤切换后没有恢复页面顶部上下文' )
assert . match ( viewSource , /nextTick\(scrollToStepTop\)/ , '步骤切换没有触发页面滚动复位' )
2026-07-13 15:28:48 +08:00
assert . match ( viewStyleSource , /\.wizard-content\s*\{[\s\S]*min-height:\s*400px/ , '第一步内容区必须保留足够高度以显示底部操作栏' )
2026-07-10 16:46:23 +08:00
assert . match (
layoutSource ,
/&:has\(\.create-wizard-layout\)\s*\{[\s\S]*?overflow-y:\s*hidden[\s\S]*?\.page-canvas\s*\{[\s\S]*?flex:\s*1 1 auto[\s\S]*?min-height:\s*0/ ,
'创建任务页必须约束画布高度,避免底部操作栏被裁掉' ,
)
assert . match ( previewSource , /height:\s*clamp\(560px,\s*calc\(100vh - 370px\),\s*720px\)/ , '对照预览高度不足以展示切片正文' )
2026-07-13 17:12:49 +08:00
console . log ( '数据处理六步向导回归检查通过' )