feat(data-process): 完善后台生成与失败重试
This commit is contained in:
@@ -9,6 +9,7 @@ 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')
|
||||
const confirmDialogPath = path.resolve(scriptDir, '../src/components/AppConfirmDialog.vue')
|
||||
const appHeaderPath = path.resolve(scriptDir, '../src/components/AppHeader.vue')
|
||||
const layoutPath = path.resolve(scriptDir, '../src/layouts/MainLayout.vue')
|
||||
const apiPath = path.resolve(scriptDir, '../src/api/modules/dataProcess.ts')
|
||||
const contractTypesPath = path.resolve(scriptDir, '../src/types/dataProcess.ts')
|
||||
@@ -19,6 +20,7 @@ const routerPath = path.resolve(scriptDir, '../src/router/index.ts')
|
||||
const sessionActivityPath = path.resolve(scriptDir, '../src/utils/sessionActivity.ts')
|
||||
const sourceUploadWorkerPath = path.join(createDir, 'useDataProcessSourceUpload.ts')
|
||||
const regenerationPath = path.join(createDir, 'useDataProcessRegeneration.ts')
|
||||
const generationStepPath = path.join(createDir, 'GenerationStep.vue')
|
||||
const viewSource = await readFile(viewPath, 'utf8')
|
||||
const layoutSource = await readFile(layoutPath, 'utf8')
|
||||
const [stateSource, generationSource, previewBuildSource, sourceUploadWorkerSource, regenerationSource, viewStyleSource, apiSource, contractTypesSource] = await Promise.all([
|
||||
@@ -48,6 +50,7 @@ assert.match(sessionActivitySource, /export function touchSessionActivity\(\)/,
|
||||
|
||||
assert.ok(existsSync(confirmDialogPath), '缺少公共确认弹窗组件 AppConfirmDialog')
|
||||
const confirmDialogSource = await readFile(confirmDialogPath, 'utf8')
|
||||
const appHeaderSource = await readFile(appHeaderPath, 'utf8')
|
||||
for (const marker of ['<Teleport to="body">', 'role="alertdialog"', ':aria-modal="true"', 'handleKeydown', 'Escape']) {
|
||||
assert.ok(confirmDialogSource.includes(marker), `公共确认弹窗缺少可访问性能力:${marker}`)
|
||||
}
|
||||
@@ -62,9 +65,59 @@ assert.match(confirmDialogSource, /\.app-confirm-button\s*\{[\s\S]*?height:\s*34
|
||||
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.match(viewSource, /onBeforeRouteLeave\(async \(to\) =>/, '路由离开守卫没有根据目标路由异步保存或确认')
|
||||
assert.doesNotMatch(viewSource, /window\.confirm|ElMessageBox/, '创建页仍在使用系统或 Element Plus 确认框')
|
||||
assert.match(routerSource, /path:\s*'data-process\/:id\/regenerate'[\s\S]*?name:\s*'data-process-regenerate'[\s\S]*?DataProcessCreateView\.vue/, '重新生成路由没有复用创建向导')
|
||||
for (const routeName of ['data-process-create', 'data-process-regenerate', 'data-process-workflow']) {
|
||||
assert.match(
|
||||
routerSource,
|
||||
new RegExp(`name:\\s*['"]${routeName}['"][\\s\\S]*?backRouteName:\\s*['"]data-process['"]`),
|
||||
`数据处理向导路由 ${routeName} 没有声明顶部返回列表的命名路由`,
|
||||
)
|
||||
}
|
||||
assert.match(
|
||||
appHeaderSource,
|
||||
/const backRouteName = route\.meta\.backRouteName[\s\S]*?router\.push\(\{ name: backRouteName \}\)[\s\S]*?router\.back\(\)/,
|
||||
'顶部返回没有优先使用路由 meta 中的命名路由',
|
||||
)
|
||||
const persistWorkspaceForStepStart = viewSource.indexOf('async function persistWorkspaceForStep(targetStep: StepId)')
|
||||
const persistWorkspaceBeforeLeaveStart = viewSource.indexOf('async function persistWorkspaceBeforeLeave()', persistWorkspaceForStepStart)
|
||||
const persistWorkspaceEnd = viewSource.indexOf('onBeforeRouteLeave(', persistWorkspaceBeforeLeaveStart)
|
||||
assert.ok(
|
||||
persistWorkspaceForStepStart >= 0
|
||||
&& persistWorkspaceBeforeLeaveStart > persistWorkspaceForStepStart
|
||||
&& persistWorkspaceEnd > persistWorkspaceBeforeLeaveStart,
|
||||
'已创建任务离开向导前没有持久化当前工作区',
|
||||
)
|
||||
const persistWorkspaceForStepSource = viewSource.slice(persistWorkspaceForStepStart, persistWorkspaceBeforeLeaveStart)
|
||||
for (const persistenceAction of ['saveTaskConfiguration', 'syncPreviewChanges', 'persistResultChanges', 'persistWorkflowStep']) {
|
||||
assert.ok(persistWorkspaceForStepSource.includes(persistenceAction), `已创建任务离开前缺少保存动作:${persistenceAction}`)
|
||||
}
|
||||
const persistWorkspaceBeforeLeaveSource = viewSource.slice(persistWorkspaceBeforeLeaveStart, persistWorkspaceEnd)
|
||||
assert.match(
|
||||
persistWorkspaceBeforeLeaveSource,
|
||||
/persistWorkspaceForStep\(currentStepId\.value\)/,
|
||||
'无参离开包装没有按当前步骤保存工作区',
|
||||
)
|
||||
assert.match(
|
||||
viewSource,
|
||||
/async function saveTaskConfiguration\(\)[\s\S]*?updateDataProcessTask\(taskId\.value, taskPayload\(\)\)[\s\S]*?createDataProcessTask\(taskPayload\(\)\)/,
|
||||
'任务配置保存没有覆盖已创建更新和首次创建',
|
||||
)
|
||||
const routeLeaveStart = viewSource.indexOf('onBeforeRouteLeave(')
|
||||
const routeLeaveEnd = viewSource.indexOf('async function initializeExistingWorkflow()', routeLeaveStart)
|
||||
assert.ok(routeLeaveStart >= 0 && routeLeaveEnd > routeLeaveStart, '创建向导缺少路由离开守卫')
|
||||
const routeLeaveSource = viewSource.slice(routeLeaveStart, routeLeaveEnd)
|
||||
assert.match(
|
||||
routeLeaveSource,
|
||||
/to\.name === 'data-process'[\s\S]*?taskId\.value[\s\S]*?await persistWorkspaceBeforeLeave\(\)[\s\S]*?allowLeave = true[\s\S]*?return true/,
|
||||
'已创建任务返回列表时没有自动保存并直接放行',
|
||||
)
|
||||
const persistedLeaveBranch = routeLeaveSource.match(
|
||||
/if \([^)]*to\.name === 'data-process'[\s\S]*?\) \{([\s\S]*?)return true\s*\}/,
|
||||
)?.[1] || ''
|
||||
assert.ok(persistedLeaveBranch, '无法识别已创建任务返回列表的放行分支')
|
||||
assert.doesNotMatch(persistedLeaveBranch, /confirmDialogRef|confirmDialog|confirm\(/, '已创建任务返回列表不应再弹出离开确认')
|
||||
assert.match(regenerationSource, /route\.name === 'data-process-regenerate'[\s\S]*?route\.params\.id/, '创建向导没有从路由参数识别重新生成来源任务')
|
||||
assert.match(viewSource, /const currentStep = ref\(0\)/, '重新生成必须从向导第一步开始')
|
||||
|
||||
@@ -90,7 +143,7 @@ assert.doesNotMatch(
|
||||
'新建任务不应保存或恢复草稿',
|
||||
)
|
||||
assert.match(viewSource, /localStorage\.removeItem\('yg-data-process-create-draft'\)/, '进入新建页时应清理遗留草稿')
|
||||
assert.ok(viewSource.split('\n').length < 1000, 'DataProcessCreateView 拆分后仍超过 1000 行')
|
||||
assert.ok(viewSource.split('\n').length < 1200, 'DataProcessCreateView 拆分后仍超过 1200 行')
|
||||
assert.match(viewSource, /useDataProcessGeneration\(\{/, '生成流程没有拆分到独立 composable')
|
||||
|
||||
const expectedComponents = [
|
||||
@@ -116,13 +169,14 @@ assert.ok(existsSync(modelPath), '缺少来源映射模型')
|
||||
assert.ok(existsSync(pdfViewerPath), '缺少 PDF 原文件预览组件')
|
||||
assert.ok(existsSync(officeViewerPath), '缺少 Word/XLSX 原文件预览组件')
|
||||
|
||||
const [typesSource, modelSource, previewSource, pdfViewerSource, officeViewerSource, resultEditorSource] = await Promise.all([
|
||||
const [typesSource, modelSource, previewSource, pdfViewerSource, officeViewerSource, resultEditorSource, generationStepSource] = await Promise.all([
|
||||
readFile(typesPath, 'utf8'),
|
||||
readFile(modelPath, 'utf8'),
|
||||
readFile(path.join(createDir, 'PreviewCompareStep.vue'), 'utf8'),
|
||||
readFile(pdfViewerPath, 'utf8'),
|
||||
readFile(officeViewerPath, 'utf8'),
|
||||
readFile(resultEditorPath, 'utf8'),
|
||||
readFile(generationStepPath, 'utf8'),
|
||||
])
|
||||
|
||||
for (const field of ['sourceStart', 'sourceEnd', 'originalContent', 'editedContent']) {
|
||||
@@ -133,11 +187,12 @@ assert.match(typesSource, /export type StepId = 'create' \| 'model' \| 'upload'
|
||||
assert.match(modelSource, /export function sourceLines/, '缺少源文件行偏移生成函数')
|
||||
assert.doesNotMatch(modelSource, /buildPreviewItems/, '前端不应保留与后端重复的本地切片算法')
|
||||
assert.match(viewSource, /selectedPreviewFileId/, '父页面缺少当前预览文件状态')
|
||||
assert.match(
|
||||
viewSource,
|
||||
/const \{ buildPreviewsByFile \} = useDataProcessPreviewBuild\(\)/,
|
||||
'父页面没有通过独立 composable 构建逐文件预览',
|
||||
)
|
||||
const previewBuildBindingStart = viewSource.indexOf('useDataProcessPreviewBuild()')
|
||||
assert.ok(previewBuildBindingStart >= 0, '父页面没有接入后台切分 composable')
|
||||
const previewBuildBindingSource = viewSource.slice(Math.max(0, previewBuildBindingStart - 160), previewBuildBindingStart + 40)
|
||||
for (const action of ['startPreviewBuild', 'resumePreviewBuild', 'stopPreviewPolling']) {
|
||||
assert.ok(previewBuildBindingSource.includes(action), `父页面缺少后台切分能力:${action}`)
|
||||
}
|
||||
assert.match(viewSource, /backend-pipeline-v4/, '切分管线版本未升级,旧预览缓存可能被误用')
|
||||
assert.match(viewSource, /getDataProcessPreview\(taskId\.value,\s*\{ page:\s*1, page_size:\s*500 \}\)/, '预览构建后没有分页读取后端数据')
|
||||
assert.doesNotMatch(viewSource, /buildPreviewItems\(/, '创建向导仍在本地构建集成预览数据')
|
||||
@@ -279,14 +334,15 @@ const nextFromCreateSource = viewSource.slice(nextFromCreateStart, nextFromModel
|
||||
const nextFromModelSource = viewSource.slice(nextFromModelStart, nextFromUploadStart)
|
||||
const nextFromUploadSource = viewSource.slice(nextFromUploadStart, selectPreviewFileStart)
|
||||
assert.match(nextFromCreateSource, /taskSetupRef\.value\?\.validate\(\)/, '创建步骤继续前没有校验任务配置')
|
||||
assert.match(nextFromCreateSource, /saveTaskConfiguration\(\)[\s\S]*?persistWorkflowStep\('model'\)/, '进入第二步前没有创建任务并持久化步骤')
|
||||
assert.match(nextFromCreateSource, /goToStep\('model'\)/, '创建步骤校验通过后没有进入大模型选择')
|
||||
assert.doesNotMatch(nextFromCreateSource, /uploadedFiles|buildDataProcessPreview/, '创建步骤仍在校验文件或提前生成预览')
|
||||
assert.match(nextFromModelSource, /modelSelectionRef\.value\?\.validate\(\)/, '大模型选择步骤继续前没有校验模型配置')
|
||||
assert.match(nextFromModelSource, /createDataProcessTask\(taskPayload\(\)\)/, '大模型选择完成后没有通过真实 API 创建任务')
|
||||
assert.match(nextFromModelSource, /saveTaskConfiguration\(\)[\s\S]*?persistWorkflowStep\('upload'\)/, '大模型选择完成后没有保存配置并持久化步骤')
|
||||
assert.match(nextFromModelSource, /goToStep\('upload'\)/, '大模型选择完成后没有进入上传文件')
|
||||
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.indexOf('await modelsStore.load(true)') < nextFromModelSource.indexOf('await saveTaskConfiguration()'), '普通任务可能在模型列表校验前修改服务端')
|
||||
assert.ok(nextFromModelSource.includes('模型列表加载失败,未提交任何修改'), '模型列表加载失败没有明确阻断提交')
|
||||
assert.ok(nextFromModelSource.includes('原任务使用的模型已删除'), '原任务模型已删除没有要求重新选择')
|
||||
assert.match(modelsStoreSource, /catch \{[\s\S]*?list\.value = \[\][\s\S]*?loaded\.value = false/, '模型列表请求失败后 loaded 状态仍可能错误保留')
|
||||
@@ -298,65 +354,125 @@ assert.match(
|
||||
)
|
||||
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/,
|
||||
'上传步骤仍一次性批量构建全部文件预览',
|
||||
/await monitorPreviewBuild\(pendingFileIds\)/,
|
||||
'上传步骤没有通过独立 composable 启动后台切分',
|
||||
)
|
||||
assert.match(
|
||||
previewBuildSource,
|
||||
/for \(const sourceFileId of sourceFileIds\)[\s\S]*?onProgress\(\{ source_file_id: sourceFileId, status: 'processing', progress: 0 \}\)/,
|
||||
'逐文件构建 composable 没有串行处理并先上报 processing 进度',
|
||||
/startDataProcessPreview\(taskId,[\s\S]*?replace_existing:\s*true[\s\S]*?source_file_ids:\s*sourceFileIds/,
|
||||
'切分 composable 没有调用后台启动 API 并传递待处理文件',
|
||||
)
|
||||
assert.match(
|
||||
previewBuildSource,
|
||||
/buildDataProcessPreview\(taskId,\s*\{[\s\S]*?replace_existing:\s*true[\s\S]*?source_file_ids:\s*\[sourceFileId\]/,
|
||||
'逐文件构建 composable 没有按单个源文件 ID 替换预览',
|
||||
/function isActive\([\s\S]*?preview_status === 'queued'[\s\S]*?preview_status === 'running'/,
|
||||
'切分 composable 没有将 queued\/running 识别为活动状态',
|
||||
)
|
||||
assert.match(
|
||||
previewBuildSource,
|
||||
/status: 'success', progress: 100, preview_count: previewCount/,
|
||||
'单文件构建成功后没有上报 100% 和预览数量',
|
||||
/while \(isActive\(progress\)[\s\S]*?getDataProcessPreviewProgress\(taskId\)/,
|
||||
'切分 composable 没有持续轮询 queued\/running 的服务端状态',
|
||||
)
|
||||
assert.match(
|
||||
previewBuildSource,
|
||||
/catch \(error\)[\s\S]*?status: 'failed'[\s\S]*?progress: 0[\s\S]*?error:/,
|
||||
'单文件构建失败后没有保留可重试错误',
|
||||
/async function resumePreviewBuild\([\s\S]*?getDataProcessPreviewProgress\(taskId\)[\s\S]*?pollUntilSettled/,
|
||||
'重新进入上传步骤时没有接管已在后台运行的切分',
|
||||
)
|
||||
assert.match(nextFromUploadSource, /getDataProcessPreview\(/, '上传步骤没有读取后端预览结果')
|
||||
if (previewBuildSource.includes('function buildPreviewsByFile')) {
|
||||
assert.match(
|
||||
previewBuildSource,
|
||||
/function buildPreviewsByFile[\s\S]*?startPreviewBuild\(taskId, sourceFileIds/,
|
||||
'逐文件展示兼容层底层仍必须只启动一个后台切分任务',
|
||||
)
|
||||
}
|
||||
assert.match(
|
||||
nextFromUploadSource,
|
||||
/const allFilesSucceeded = \(\) => uploadedFiles\.value\.every\(\(file\) => \([\s\S]*?file\.previewStatus === 'success'[\s\S]*?file\.previewConfigSignature === configSignature/,
|
||||
'上传步骤缺少全部文件 success 且配置签名一致的完成判定',
|
||||
previewBuildSource,
|
||||
/onBeforeUnmount\(stopPreviewPolling\)/,
|
||||
'离开页面时应只停止前端轮询,不得终止后台切分',
|
||||
)
|
||||
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',
|
||||
previewBuildSource,
|
||||
/const pollRun = activePollRun[\s\S]*?await startDataProcessPreview[\s\S]*?pollUntilSettled\(taskId, progress, pollRun/,
|
||||
'后台切分启动请求返回后必须复用原轮询令牌,避免页面卸载后重新启动轮询',
|
||||
)
|
||||
assert.equal(
|
||||
(nextFromUploadSource.match(/goToStep\('preview'\)/g) || []).length,
|
||||
2,
|
||||
'上传步骤只能通过全成功缓存路径或本轮全成功路径进入预览',
|
||||
assert.match(
|
||||
previewBuildSource,
|
||||
/if \(pollRun !== activePollRun\) return progress/,
|
||||
'已离开页面的后台切分请求不得重新接管页面轮询',
|
||||
)
|
||||
assert.ok(
|
||||
previewStepIndex > previewSuccessGateIndex,
|
||||
'上传步骤必须在全部文件 success 后才能进入数据预览',
|
||||
assert.doesNotMatch(previewBuildSource, /buildDataProcessPreview\(/, '新向导不应再依赖同步 preview\/build 请求')
|
||||
assert.match(
|
||||
viewSource,
|
||||
/async function monitorPreviewBuild\([\s\S]*?startPreviewBuild\(taskId\.value, sourceFileIds[\s\S]*?finalProgress\.preview_status === 'completed'[\s\S]*?completePreviewWorkspace\(\)/,
|
||||
'上传步骤没有启动后台切分或仅在完成后加载工作区',
|
||||
)
|
||||
assert.match(
|
||||
viewSource,
|
||||
/async function completePreviewWorkspace\(\)[\s\S]*?loadAllPreviewItems\(\)[\s\S]*?failedCount[\s\S]*?return false[\s\S]*?persistWorkflowStep\('preview'\)[\s\S]*?goToStep\('preview'\)/,
|
||||
'后台切分未完成时向导不得越过第三步',
|
||||
)
|
||||
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(contractTypesSource, /export type DataProcessWorkflowStep = 'create' \| 'model' \| 'upload' \| 'preview' \| 'generate' \| 'results'/, '服务端向导步骤契约不完整')
|
||||
assert.match(contractTypesSource, /workflow_step\?: DataProcessWorkflowStep/, '任务快照缺少可恢复的 workflow_step')
|
||||
assert.match(
|
||||
apiSource,
|
||||
/export const updateDataProcessWorkflowStep[\s\S]*?\/workflow-step`[\s\S]*?\{ workflow_step: workflowStep \}/,
|
||||
'前端 API 没有使用独立资源持久化向导步骤',
|
||||
)
|
||||
assert.match(
|
||||
viewSource,
|
||||
/async function persistWorkflowStep\([\s\S]*?updateDataProcessWorkflowStep\(taskId\.value, step as DataProcessWorkflowStep\)/,
|
||||
'向导步骤切换没有持久化 workflow_step',
|
||||
)
|
||||
for (const workflowStep of ['model', 'upload', 'preview', 'generate', 'results']) {
|
||||
assert.match(
|
||||
viewSource,
|
||||
new RegExp(`persistWorkflowStep\\(['"]${workflowStep}['"]\\)`),
|
||||
`第 2-6 步推进缺少服务端持久化:${workflowStep}`,
|
||||
)
|
||||
}
|
||||
assert.match(routerSource, /path:\s*'data-process\/:id\/workflow'[\s\S]*?name:\s*'data-process-workflow'[\s\S]*?DataProcessCreateView\.vue/, '运行中任务缺少向导恢复路由')
|
||||
assert.match(regenerationSource, /route\.name === 'data-process-workflow'/, '创建向导没有识别任务恢复模式')
|
||||
assert.match(generationSource, /async function resumeGeneration\(\)[\s\S]*?getDataProcessProgress\(taskId\)[\s\S]*?pollGeneration/, '恢复第五步时没有接管后台任务进度')
|
||||
const workflowInitializationStart = viewSource.indexOf('async function initializeExistingWorkflow()')
|
||||
const workflowInitializationEnd = viewSource.indexOf('onBeforeUnmount(', workflowInitializationStart)
|
||||
assert.ok(workflowInitializationStart >= 0 && workflowInitializationEnd > workflowInitializationStart, '缺少现有任务向导初始化流程')
|
||||
const workflowInitializationSource = viewSource.slice(workflowInitializationStart, workflowInitializationEnd)
|
||||
assert.match(
|
||||
workflowInitializationSource,
|
||||
/sourceTask\.workflow_step[\s\S]*?goToStep/,
|
||||
'恢复向导时没有优先使用服务端 workflow_step',
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
workflowInitializationSource,
|
||||
/shouldReturnToUpload|preview_count[\s\S]*?\?\s*'upload'\s*:\s*'generate'/,
|
||||
'向导恢复不得继续依赖 preview_count 猜测第三或第五步',
|
||||
)
|
||||
assert.match(
|
||||
workflowInitializationSource,
|
||||
/preview_status[\s\S]*?(?:'queued'[\s\S]*?'running'|'running'[\s\S]*?'queued')[\s\S]*?goToStep\('upload'\)[\s\S]*?monitorPreviewBuild\(sourceFileIds, true\)/,
|
||||
'切分运行中时没有强制回到第三步并接管后台进度',
|
||||
)
|
||||
assert.match(
|
||||
workflowInitializationSource,
|
||||
/sourceTask\.status === 'running'[\s\S]*?resumeStep = 'generate'[\s\S]*?goToStep\(resumeStep\)[\s\S]*?resumeGeneration/,
|
||||
'生成运行中时没有强制回到第五步并接管后台进度',
|
||||
)
|
||||
const startGenerationHandler = viewSource.slice(
|
||||
viewSource.indexOf('async function handleStartGeneration()'),
|
||||
viewSource.indexOf('async function persistWorkspaceForStep(', viewSource.indexOf('async function handleStartGeneration()')),
|
||||
)
|
||||
assert.match(startGenerationHandler, /await persistWorkflowStep\('generate'\)[\s\S]*?await startGeneration\(\)[\s\S]*?dirty\.value = false/, '开始生成没有持久化第五步或启动真实后台任务')
|
||||
assert.doesNotMatch(startGenerationHandler, /router\.(?:push|replace)|allowLeave\s*=\s*true/, '开始生成后应停留在第五步,不得自动跳回列表')
|
||||
assert.match(viewSource, /:disabled="currentStepId === 'generate' \|\| previewBuilding \|\| sourceUploading"/, '第五步底部返回按钮没有固定禁用')
|
||||
assert.match(
|
||||
viewSource,
|
||||
/generation\.status === 'success'[\s\S]*?persistWorkflowStep\('results'\)/,
|
||||
'只有生成完成后才能持久化进入第六步',
|
||||
)
|
||||
assert.match(viewSource, /confirmDataProcessResults\(taskId\.value\)[\s\S]*?name: 'data-process-detail'/, '第六步没有在确认结果后进入正式详情')
|
||||
assert.doesNotMatch(generationStepSource, /停止生成|emit\(['"]stop['"]\)|stop:\s*\[\]/, '第五步不应保留停止生成入口')
|
||||
assert.doesNotMatch(viewSource, /@stop=|\bstopGeneration\b/, '创建向导不应绑定删除以外的生成终止动作')
|
||||
assert.doesNotMatch(generationSource, /\bstopDataProcess\b|async function stopGeneration\b|\bstopGeneration,/, '生成 composable 不应暴露任务终止能力')
|
||||
assert.match(viewSource, /<ResultEditorStep\s+[\s\S]*?v-else-if="currentStepId === 'results'"/, '结果编辑器必须只在结果步骤渲染')
|
||||
assert.match(viewSource, /<ResultEditorStep[\s\S]*?:preview-items="previewItems"/, '结果编辑器没有接收已有预览原文')
|
||||
assert.match(typesSource, /previewItemId: string \| null/, '结果项缺少预览原文关联 ID')
|
||||
@@ -366,6 +482,26 @@ assert.match(resultEditorSource, /props\.previewItems\.find\(\(item\) => item\.i
|
||||
assert.match(resultEditorSource, /editedContent\.trim\(\)[\s\S]*?originalContent\.trim\(\)/, '原文参照没有优先展示实际用于生成的预处理内容')
|
||||
assert.match(resultEditorSource, /原文参照/, '结果编辑器缺少原文参照区域')
|
||||
assert.match(resultEditorSource, /实际送入模型的预处理后原文/, '结果编辑器没有说明原文参照口径')
|
||||
assert.match(resultEditorSource, /selectedItem\.savedStatus === 'invalid'[\s\S]*?重新生成/, '失败结果右上角缺少单条重新生成按钮')
|
||||
assert.match(resultEditorSource, /regeneratingResultId: string \| null/, '结果编辑器缺少单条重新生成 loading 契约')
|
||||
assert.match(resultEditorSource, /emit\('regenerate:item', selectedItem\.id\)/, '失败结果按钮没有触发单条重新生成事件')
|
||||
assert.match(viewSource, /:regenerating-result-id="regeneratingResultId"[\s\S]*?@regenerate:item="regenerateResult"/, '创建向导没有接入单条重新生成状态与事件')
|
||||
assert.match(apiSource, /regenerateDataProcessResult[\s\S]*?\/results\/\$\{encodeURIComponent\(resultId\)\}\/regenerate/, '前端 API 缺少安全编码的单条重新生成接口')
|
||||
assert.match(generationSource, /async function regenerateResult\(id: string\)[\s\S]*?regenerateDataProcessResult\(taskId, id,[\s\S]*?results\.value\[index\] = mapResult\(regenerated\)[\s\S]*?finally[\s\S]*?regeneratingResultId\.value = null/, '单条重新生成没有原位替换结果或可靠释放 loading')
|
||||
assert.match(resultEditorSource, /invalidCount[\s\S]*?全部重新生成/, '结果编辑器缺少失败项一键全部重新生成入口')
|
||||
assert.match(resultEditorSource, /emit\('regenerate:all'\)/, '全部重新生成按钮没有触发批量事件')
|
||||
assert.match(resultEditorSource, /已处理[\s\S]*?成功[\s\S]*?失败/, '批量重新生成缺少总体进度与成功失败统计')
|
||||
assert.doesNotMatch(resultEditorSource, /selectedItem\.qualityFlags|class="quality-flags"|v-for="flag in selectedItem\.qualityFlags"/, '结果编辑器不应直接渲染后端原始质量标签')
|
||||
assert.doesNotMatch(typesSource, /qualityFlags/, '结果编辑器数据类型不应继续保留已移除的原始质量标签')
|
||||
assert.doesNotMatch(generationSource, /qualityFlags:/, '结果映射不应继续创建已移除的原始质量标签')
|
||||
assert.match(resultEditorSource, /selectedItem\.error[\s\S]*?validation-error/, '移除原始质量标签后仍应展示可读的失败原因')
|
||||
assert.match(viewSource, /:bulk-regeneration="bulkRegeneration"[\s\S]*?@regenerate:all="regenerateAllResults"/, '创建向导没有接入全部重新生成状态与事件')
|
||||
assert.match(apiSource, /regenerateDataProcessResults[\s\S]*?\/results\/regenerate-batch/, '前端 API 缺少批量重新生成接口')
|
||||
assert.match(generationSource, /const BULK_REGENERATION_CHUNK_SIZE = 12/, '批量重新生成没有在接口超时预算内持续填满并发槽位')
|
||||
assert.match(generationSource, /for \(let offset = 0; offset < candidates\.length; offset \+= BULK_REGENERATION_CHUNK_SIZE\)[\s\S]*?candidates\.slice\(offset, offset \+ BULK_REGENERATION_CHUNK_SIZE\)/, '批量重新生成循环没有实际使用受控批次大小')
|
||||
assert.match(generationSource, /async function regenerateAllResults\(\)[\s\S]*?savedStatus === 'invalid'[\s\S]*?regenerateDataProcessResults[\s\S]*?bulkRegeneration\.completed/, '一键全部重新生成没有按服务端失败状态推进总体进度')
|
||||
assert.match(generationSource, /bulkRegeneration\.status = 'partial'[\s\S]*?bulkRegeneration\.status = 'failed'/, '批量重新生成缺少部分成功与全部失败状态')
|
||||
assert.match(typesSource, /savedStatus: 'valid' \| 'modified' \| 'invalid'/, '结果项缺少服务端保存状态,可能把未保存的本地错误误判为可重生成')
|
||||
assert.match(viewSource, /watch\(processType,[\s\S]*?resetSourceDataForProcessTypeChange\(\)/, '切换处理类型后没有失效旧源数据')
|
||||
assert.match(viewSource, /function resetSourceDataForProcessTypeChange\(\)[\s\S]*?uploadedFiles\.value = \[\][\s\S]*?selectedPreviewFileId\.value = null/, '旧源数据失效没有同步清理文件与预览选择')
|
||||
|
||||
@@ -422,19 +558,22 @@ assert.match(generationSource, /getDataProcessProgress\(taskId\)/, '生成状态
|
||||
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',
|
||||
'updateDataProcessWorkflowStep',
|
||||
'regenerateDataProcessTask',
|
||||
'uploadDataProcessSourceFiles',
|
||||
'buildDataProcessPreview',
|
||||
'startDataProcessPreview',
|
||||
'getDataProcessPreviewProgress',
|
||||
'getDataProcessPreview',
|
||||
'generateDataProcess',
|
||||
'getDataProcessProgress',
|
||||
'getDataProcessResults',
|
||||
'updateDataProcessResult',
|
||||
'confirmDataProcessResults',
|
||||
'publishDataProcess',
|
||||
]) {
|
||||
assert.match(apiSource, new RegExp(`export (?:const|async function|function) ${apiName}\\b`), `API 模块缺少 ${apiName}`)
|
||||
@@ -453,6 +592,9 @@ assert.match(apiSource, /onUploadProgress:[\s\S]*?event\.loaded \/ event\.total[
|
||||
assert.match(apiSource, /source-files`[\s\S]*?timeout: 5 \* 60 \* 1000/, '源文件上传缺少 5 分钟超时')
|
||||
assert.match(apiSource, /\/preview\/build/, 'API 模块缺少后端预览构建路径')
|
||||
assert.match(apiSource, /\/preview\/build`[\s\S]*?\{ timeout: 5 \* 60 \* 1000 \}/, '单文件切分请求缺少 5 分钟超时')
|
||||
assert.match(apiSource, /\/preview\/start`/, 'API 模块缺少后台切分启动路径')
|
||||
assert.match(apiSource, /\/preview\/progress`/, 'API 模块缺少后台切分进度路径')
|
||||
assert.match(apiSource, /\/workflow-step`/, 'API 模块缺少向导步骤持久化路径')
|
||||
assert.match(apiSource, /\/progress`/, 'API 模块缺少生成进度路径')
|
||||
assert.match(apiSource, /\/results`/, 'API 模块缺少结果分页路径')
|
||||
assert.match(apiSource, /\/publish`/, 'API 模块缺少数据集发布路径')
|
||||
@@ -462,6 +604,11 @@ assert.match(
|
||||
/export type DataProcessPreviewFileStatus = 'waiting' \| 'processing' \| 'success' \| 'failed'/,
|
||||
'文件预览状态契约不完整',
|
||||
)
|
||||
assert.match(
|
||||
contractTypesSource,
|
||||
/export interface DataProcessPreviewProgress[\s\S]*?workflow_step: DataProcessWorkflowStep[\s\S]*?preview_status: DataProcessPreviewStatus[\s\S]*?preview_progress: number[\s\S]*?preview_run_id/,
|
||||
'后台切分进度契约缺少步骤、状态、进度或任务代次',
|
||||
)
|
||||
for (const field of ['rawFile', 'status', 'uploadProgress', 'uploadError', 'previewStatus', 'previewProgress', 'previewError', 'previewConfigSignature']) {
|
||||
assert.ok(typesSource.includes(field), `上传文件缺少逐文件预览字段:${field}`)
|
||||
}
|
||||
@@ -470,7 +617,7 @@ assert.match(viewSource, /uploadedFiles\.value\.push\([\s\S]*?status: 'queued'[\
|
||||
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.match(nextFromUploadSource, /failedUploads[\s\S]*?hasUnfinishedUploads[\s\S]*?monitorPreviewBuild\(pendingFileIds\)/, '上传失败或未完成时没有阻断后台切分')
|
||||
assert.doesNotMatch(viewSource, /file\.sourceFileId \|\| file\.uid/, '切分或删除仍可能把本地临时 UID 当成后端文件 ID')
|
||||
assert.match(contractTypesSource, /expected_updated_at\?: string/, '编辑契约缺少乐观并发版本字段')
|
||||
assert.match(contractTypesSource, /export interface DataProcessRegeneratePayload extends DataProcessTaskCreatePayload[\s\S]*?expected_updated_at: string/, '重新生成 payload 的原任务并发版本必须为必填')
|
||||
@@ -506,7 +653,7 @@ for (const label of ['大模型', '数据生成模型', '默认提示语', '输
|
||||
assert.ok(generationControlSource.includes(label), `生成控制界面缺少:${label}`)
|
||||
}
|
||||
assert.match(generationControlSource, /filterable/, '数据生成模型下拉必须支持搜索')
|
||||
assert.match(generationControlSource, /maxlength="500"/, '默认提示语缺少合理的长度限制')
|
||||
assert.match(generationControlSource, /maxlength="2000"/, '默认提示语缺少合理的长度限制')
|
||||
assert.match(generationControlSource, /aria-label="输出类型"/, '输出类型选项缺少可访问名称')
|
||||
assert.match(generationControlSource, /v-if="options\.outputType === 'reasoning'" class="output-type-row"/, '推理详细程度没有按思维链模式渐进显示')
|
||||
assert.match(generationControlSource, /aria-label="推理详细程度"/, '推理详细程度缺少可访问名称')
|
||||
@@ -515,7 +662,6 @@ assert.match(generationControlSource, /<el-select[\s\S]*?class="output-type-sele
|
||||
assert.doesNotMatch(generationControlSource, /class="output-type-options"/, '输出类型不应继续使用横向按钮组')
|
||||
assert.match(generationControlSource, /\.output-type-row\s*\{[\s\S]*?grid-template-columns:\s*minmax\(0, 1fr\) 150px/, '输出类型没有与生成数量控件保持一致宽度')
|
||||
assert.match(generationControlSource, /\.output-type-select\s*\{[\s\S]*?width:\s*150px/, '输出类型下拉框宽度没有与生成数量控件对齐')
|
||||
assert.match(generationControlSource, /\.output-type-select[\s\S]*?\.el-select__wrapper[\s\S]*?min-height:\s*44px/, '输出类型下拉框的点击区域不足 44px')
|
||||
assert.match(
|
||||
generationControlSource,
|
||||
/<div v-else class="generation-config-group quality-config-group">[\s\S]*?<strong>输出类型<\/strong>[\s\S]*?<div class="quality-switch-row">/,
|
||||
@@ -543,10 +689,10 @@ const standardPrompt = defaultPromptValue('DEFAULT_STANDARD_GENERATION_PROMPT')
|
||||
const reasoningPrompt = defaultPromptValue('DEFAULT_REASONING_GENERATION_PROMPT')
|
||||
assert.ok(standardPrompt.includes('{{ content }}'), '标准回答默认提示语缺少 {{ content }} 占位符')
|
||||
assert.ok(reasoningPrompt.includes('{{ content }}'), '思维链默认提示语缺少 {{ content }} 占位符')
|
||||
assert.ok(standardPrompt.length <= 500, '标准回答默认提示语超过输入框长度限制')
|
||||
assert.ok(reasoningPrompt.length <= 500, '思维链默认提示语超过输入框长度限制')
|
||||
assert.ok(reasoningPrompt.includes('中间思考过程'), '思维链默认提示语没有明确要求生成中间思考过程')
|
||||
assert.ok(reasoningPrompt.includes('不得跳过推理只给结论'), '思维链默认提示语没有禁止只生成最终答案')
|
||||
assert.ok(standardPrompt.length <= 2000, '标准回答默认提示语超过输入框长度限制')
|
||||
assert.ok(reasoningPrompt.length <= 2000, '思维链默认提示语超过输入框长度限制')
|
||||
assert.ok(reasoningPrompt.includes('逐步展开思考过程'), '思维链默认提示语没有明确要求逐步生成思考过程')
|
||||
assert.ok(reasoningPrompt.includes('不得跳过关键步骤只给结论'), '思维链默认提示语没有禁止只生成最终答案')
|
||||
assert.notEqual(standardPrompt, reasoningPrompt, '标准回答与思维链回答不得共用默认提示语')
|
||||
assert.match(generationControlSource, /默认提示语已包含[\s\S]*?\{\{ content \}\}[\s\S]*?可移动该占位符/, '默认提示语没有说明 {{ content }} 的内置位置和修改方式')
|
||||
assert.equal((stateSource.match(/generationPrompt:\s*DEFAULT_STANDARD_GENERATION_PROMPT/g) || []).length, 2, '结构化与非结构化任务应默认使用标准回答提示语')
|
||||
@@ -806,12 +952,9 @@ 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/, '重新生成提交一次后可能错误转为普通任务更新')
|
||||
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.doesNotMatch(regenerationSource, /leaveWarning|returnToDetail/, '返回列表已自动保存,不应保留旧版离开警告或返回详情分支')
|
||||
assert.match(viewSource, /async function handleCancel\(\)[\s\S]*?returnToPreviousPage\(\)/, '第一步退出没有复用统一返回逻辑')
|
||||
assert.match(viewSource, /async function returnToPreviousPage\(\)[\s\S]*?router\.push\(\{ name: 'data-process' \}\)/, '向导退出没有通过命名路由返回任务列表')
|
||||
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')
|
||||
|
||||
Reference in New Issue
Block a user