Files
YG_FT/frontend/scripts/regression-dataset-preview.mjs

197 lines
14 KiB
JavaScript
Raw Permalink Normal View History

import fs from 'node:fs'
import path from 'node:path'
import ts from 'typescript'
const root = process.cwd()
const previewPath = path.join(root, 'src/views/dataset/DatasetPreviewView.vue')
const previewComponentsDir = path.join(root, 'src/views/dataset/preview')
const previewComponentPaths = [
'DatasetVersionBar.vue',
'DatasetRecordTable.vue',
'DatasetRecordEditorDialog.vue',
'DatasetRawPreview.vue',
].map((name) => path.join(previewComponentsDir, name))
const apiPath = path.join(root, 'src/api/modules/dataset.ts')
const adapterPath = path.join(root, 'src/mock/adapter.ts')
const mockPath = path.join(root, 'src/mock/data.ts')
const recordsPath = path.join(root, 'src/views/dataset/datasetRecords.ts')
const recordEditorPath = path.join(root, 'src/views/dataset/preview/useDatasetRecordEditor.ts')
const versionsPath = path.join(root, 'src/mock/datasetVersions.ts')
const source = fs.readFileSync(previewPath, 'utf8')
const previewLogicSource = source.slice(0, source.indexOf('<style'))
const previewComponentSources = previewComponentPaths.map((componentPath) => {
expect(fs.existsSync(componentPath), `缺少数据集预览叶子组件:${path.basename(componentPath)}`)
return fs.readFileSync(componentPath, 'utf8')
})
const previewSurfaceSource = [source, ...previewComponentSources].join('\n')
const apiSource = fs.readFileSync(apiPath, 'utf8')
const adapterSource = fs.readFileSync(adapterPath, 'utf8')
const mockSource = fs.readFileSync(mockPath, 'utf8')
const recordsSource = fs.readFileSync(recordsPath, 'utf8')
const recordEditorSource = fs.readFileSync(recordEditorPath, 'utf8')
const versionsSource = fs.readFileSync(versionsPath, 'utf8')
const recordsModuleCode = ts.transpileModule(recordsSource, {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText
const { deleteDatasetRecord, parseDatasetRecords, updateDatasetRecord } = await import(
`data:text/javascript;base64,${Buffer.from(recordsModuleCode).toString('base64')}`
)
const versionsModuleCode = ts.transpileModule(versionsSource, {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText
const {
activateDatasetVersion,
appendDatasetVersion,
createInitialVersionState,
deleteDatasetVersion,
getActiveDatasetVersion,
} = await import(`data:text/javascript;base64,${Buffer.from(versionsModuleCode).toString('base64')}`)
function expect(condition, message) {
if (!condition) throw new Error(message)
}
expect(previewLogicSource.split('\n').length < 520, 'DatasetPreviewView.vue 的逻辑与模板应保持在 520 行以内')
for (const componentName of ['DatasetVersionBar', 'DatasetRecordTable', 'DatasetRecordEditorDialog', 'DatasetRawPreview']) {
expect(source.includes(`import ${componentName}`), `主页面未导入叶子组件:${componentName}`)
expect(source.includes(`<${componentName}`), `主页面未挂载叶子组件:${componentName}`)
}
expect(source.includes('class="preview-workspace"'), '详情页应提供数据文件工作区')
expect(!source.includes('DatasetSplitNavigator'), '独立数据集详情不应再混合展示三个切分')
expect(previewSurfaceSource.includes('class="code-line"'), '内容查看器应提供逐行展示')
expect(previewSurfaceSource.includes('class="line-number"'), '内容查看器应显示行号')
expect(source.includes('previewLoading'), '切换文件时应提供独立加载状态')
expect(source.includes('previewError'), '内容加载失败时应提供错误状态')
expect(previewSurfaceSource.includes('class="records-viewer"'), '详情页应将结构化文件展示为样本列表')
expect(previewSurfaceSource.includes('class="record-table"'), '样本数据应使用企业级表格展示')
expect(previewSurfaceSource.includes('fixed="right"'), '逐条编辑操作列应固定在表格右侧')
expect(previewSurfaceSource.includes('v-for="fieldKey in tableFieldKeys"'), '企业表格应根据数据结构动态生成字段列')
expect(previewSurfaceSource.includes("emit('edit', asDatasetRecord(row))"), '每条样本应向主页面发送编辑事件')
expect(previewSurfaceSource.includes("emit('delete', asDatasetRecord(row))"), '每条样本应提供删除操作')
expect(previewSurfaceSource.includes('label="原文参照"'), '样本列表应展示生成内容对应的原文参照')
expect(previewSurfaceSource.includes('popper-class="dataset-record-content-tooltip"'), '长文本提示应使用受控尺寸样式')
expect(previewSurfaceSource.includes('max-width: min(480px, calc(100vw - 32px))'), '长文本提示不得横跨整个页面')
expect(previewSurfaceSource.includes('max-height: min(240px, calc(100vh - 32px))'), '长文本提示应限制高度')
expect(previewSurfaceSource.includes('overflow-y: auto'), '超长提示内容应允许纵向滚动')
expect(previewSurfaceSource.includes('white-space: pre-wrap'), '提示内容应保留换行并自动折行')
expect(source.includes('getDatasetFileRecordSources'), '详情页应从后端读取发布样本的真实原文')
expect(recordEditorSource.includes('deleteDatasetRecord('), '删除样本应只修改当前文件草稿')
expect(recordEditorSource.includes('删除后将暂存到当前草稿'), '删除样本前应说明保存版本后才生效')
expect(source.includes('selectedFileSize'), '详情文件大小也应统一显示为 MB')
expect(previewSurfaceSource.includes('暂存修改'), '单条编辑应先暂存到当前页面')
expect(source.includes('hasPendingVersionChanges && isViewingActiveVersion'), '存在暂存修改时才应显示保存版本按钮')
expect(source.includes('保存版本'), '文件工具栏应提供保存版本按钮')
expect(source.includes('baseVersionContent'), '页面应区分已保存版本与待保存工作副本')
expect(previewSurfaceSource.includes('class="version-control-bar"'), '详情页应提供独立版本控制区')
expect(previewSurfaceSource.includes('设为当前版本'), '历史版本应支持显式切换为当前版本')
expect(previewSurfaceSource.includes('class="version-actions"'), '历史版本操作应收纳到省略号菜单')
expect(previewSurfaceSource.includes('删除版本'), '历史版本操作菜单应提供删除入口')
expect(previewSurfaceSource.includes('历史版本(只读)'), '历史版本应明确展示只读状态')
expect(source.includes('AppConfirmDialog'), '删除历史版本前应使用公共危险确认弹窗')
expect(source.includes("tone: 'danger'"), '删除历史版本确认弹窗应使用危险态')
expect(source.includes('loadedVersionId.value'), '下载和展示应绑定正在查看的具体版本')
expect(source.includes('versionRequestId'), '快速切换历史版本时应防止旧响应覆盖新内容')
expect(previewSurfaceSource.includes('v-model:current-page="currentPage"'), '样本列表应支持分页浏览')
expect(!source.includes('class="content-editor"'), '详情页不应继续提供整文件编辑器')
expect(source.includes('saveRecord'), '逐条编辑器应提供单条保存动作')
expect(source.includes('hasUnsavedChanges'), '在线编辑器应跟踪未保存修改')
expect(source.includes('onBeforeRouteLeave'), '离开页面时应保护未保存修改')
expect(previewSurfaceSource.includes("event.key.toLowerCase() === 's'"), '在线编辑器应支持快捷键保存')
expect(source.includes('loadVersions(selectedFile)'), '版本内容加载失败后应支持重新加载')
expect(recordEditorSource.includes('function resetRecordEditor()'), '编辑器逻辑应提供统一的状态清理函数')
const resetEditorStart = recordEditorSource.indexOf('function resetRecordEditor()')
const updateFieldStart = recordEditorSource.indexOf('function openRecordEditor', resetEditorStart)
const resetEditorSource = recordEditorSource.slice(resetEditorStart, updateFieldStart)
for (const marker of [
'editorVisible.value = false',
'editingRecord.value = null',
'editFields.value = []',
"rawDraft.value = ''",
"originalDraft.value = ''",
]) {
expect(resetEditorSource.includes(marker), `编辑器状态清理不完整:${marker}`)
}
const versionChangeStart = source.indexOf('async function handleViewedVersionChange')
const activateVersionStart = source.indexOf('async function activateViewedVersion', versionChangeStart)
const versionChangeSource = source.slice(versionChangeStart, activateVersionStart)
expect(versionChangeSource.includes('resetRecordEditor()'), '成功切换版本后必须关闭并清空旧编辑器状态')
expect(!source.includes('handleDownloadAll'), '页面不应保留整包下载逻辑')
expect(!source.includes('deleteDataset('), '详情页不应恢复删除整个数据集的逻辑')
expect(!source.includes('router.back()'), '页面不应保留页头返回逻辑')
expect(mockSource.includes('files: ['), 'Mock 数据集应包含可验收的文件列表')
expect(mockSource.includes('mockDatasetPreviews'), 'Mock 数据应包含文件预览内容')
expect(mockSource.includes("'mock-jsonl'"), 'Mock 数据应为通用数据集提供模拟样本')
expect(mockSource.includes("'mock-readme'"), 'Mock 数据应为通用数据集提供模拟说明')
expect(mockSource.includes('.map((dataset)'), '所有 Mock 数据集都应自动补齐预览文件')
expect(mockSource.includes(': [{ id: `dataset-${dataset.id}-samples`'), '每个 Mock 数据集应只对应一个数据文件')
expect(apiSource.includes('createDatasetFileVersion'), '数据集 API 应提供创建新版本方法')
expect(apiSource.includes('activateDatasetFileVersion'), '数据集 API 应提供切换当前版本方法')
expect(apiSource.includes('deleteDatasetFileVersion'), '数据集 API 应提供删除历史版本方法')
expect(apiSource.includes('getDatasetFileVersionContent'), '数据集 API 应支持读取历史版本内容')
expect(apiSource.includes('expected_current_version_id'), '创建和激活版本应携带乐观锁版本指针')
expect(!apiSource.includes('updateDatasetFileContent'), '数据编辑不应继续覆盖历史版本内容')
expect(adapterSource.includes('mock:dataset-versions:'), 'Mock 版本历史应支持刷新持久化')
expect(adapterSource.includes('当前版本已被其他用户更新'), 'Mock 应拒绝基于过期版本的并发覆盖')
expect(adapterSource.includes('历史版本不可覆盖'), '旧的覆盖保存接口应明确拒绝修改历史版本')
expect(versionsSource.includes('当前版本不可删除'), 'Mock 应拒绝删除当前版本')
expect(versionsSource.includes('初始版本不可删除'), 'Mock 应拒绝删除初始版本')
expect(adapterSource.includes('Mock 存储空间不足'), 'Mock 存储失败时应返回明确错误且保留编辑草稿')
const jsonl = [
'{"instruction":"第一条","output":"A"}',
'',
'{"instruction":"第二条","output":["B","C"]}',
].join('\n')
const parsed = parseDatasetRecords(jsonl, 'samples.jsonl')
expect(parsed.supported && parsed.records.length === 2, 'JSONL 应跳过空行并解析为逐条样本')
expect(parsed.records[1].sourceIndex === 2, '样本应保留原文件物理行位置')
const updated = updateDatasetRecord(jsonl, 'samples.jsonl', 2, {
instruction: '第二条已修改',
output: ['B', 'C'],
})
const beforeLines = jsonl.split('\n')
const afterLines = updated.split('\n')
expect(afterLines[0] === beforeLines[0] && afterLines[1] === '', '单条保存不得改写其他样本或空行')
expect(JSON.parse(afterLines[2]).instruction === '第二条已修改', '单条保存应准确更新目标样本')
expect(Array.isArray(JSON.parse(afterLines[2]).output), '单条保存不应丢失数组等非字符串字段类型')
const deleted = deleteDatasetRecord(updated, 'samples.jsonl', 2)
expect(!deleted.includes('第二条已修改'), '删除样本应准确移除目标 JSONL 记录')
expect(deleted.split('\n')[0] === beforeLines[0], '删除样本不得改写其他 JSONL 记录')
const invalid = parseDatasetRecords('{invalid json}', 'samples.jsonl')
expect(invalid.records[0].kind === 'invalid' && invalid.records[0].error, '非法 JSON 行应被单独标记并给出错误')
const repairedJson = updateDatasetRecord('{invalid json}', 'samples.json', 0, { repaired: true })
expect(JSON.parse(repairedJson).repaired === true, '非法 JSON 文件应支持按单条方式修复')
const v1State = createInitialVersionState('A', '2026-07-13T00:00:00Z')
const v2State = appendDatasetVersion(v1State, 'B', '2026-07-13T01:00:00Z', '修改第一条')
expect(v2State !== v1State, '创建版本应返回新的不可变状态')
expect(v1State.versions.length === 1 && v1State.versions[0].content === 'A', '创建新版本不得修改历史版本')
expect(v2State.versions[1].version === 2 && v2State.active_version_id === 'v2', '新版本编号应递增并自动激活')
const switchedToV1 = activateDatasetVersion(v2State, 'v1')
expect(getActiveDatasetVersion(switchedToV1).content === 'A', '切换当前版本后应读取对应历史内容')
expect(v2State.active_version_id === 'v2' && v2State.versions[1].content === 'B', '切换版本不得污染原状态或历史快照')
const v3State = appendDatasetVersion(v2State, 'C', '2026-07-13T02:00:00Z', '修改第二条')
const deletedV2State = deleteDatasetVersion(v3State, 'v2')
expect(!deletedV2State.versions.some((item) => item.id === 'v2'), '删除历史版本后不应继续返回该版本')
expect(deletedV2State.active_version_id === 'v3', '删除历史版本不得改变当前版本指针')
const v4State = appendDatasetVersion(deletedV2State, 'D', '2026-07-13T03:00:00Z', '继续编辑')
expect(v4State.versions.at(-1)?.version === 4, '删除历史版本后不得复用旧版本号')
for (const protectedVersionId of ['v1', 'v3']) {
let protectedVersionRejected = false
try {
deleteDatasetVersion(v3State, protectedVersionId)
} catch {
protectedVersionRejected = true
}
expect(protectedVersionRejected, `${protectedVersionId} 受保护版本不得删除`)
}
let invalidVersionRejected = false
try {
activateDatasetVersion(v2State, 'missing')
} catch {
invalidVersionRejected = true
}
expect(invalidVersionRejected, '不存在的版本不得切换为当前版本')
console.log('dataset preview regression checks passed')