2026-07-11 10:39:26 +08:00
|
|
|
import fs from 'node:fs'
|
|
|
|
|
import path from 'node:path'
|
2026-07-13 10:31:20 +08:00
|
|
|
import ts from 'typescript'
|
2026-07-11 10:39:26 +08:00
|
|
|
|
|
|
|
|
const root = process.cwd()
|
|
|
|
|
const previewPath = path.join(root, 'src/views/dataset/DatasetPreviewView.vue')
|
2026-07-13 15:30:11 +08:00
|
|
|
const previewComponentsDir = path.join(root, 'src/views/dataset/preview')
|
|
|
|
|
const previewComponentPaths = [
|
|
|
|
|
'DatasetVersionBar.vue',
|
|
|
|
|
'DatasetRecordTable.vue',
|
|
|
|
|
'DatasetRecordEditorDialog.vue',
|
|
|
|
|
'DatasetRawPreview.vue',
|
2026-07-24 21:11:01 +08:00
|
|
|
'DatasetSplitNavigator.vue',
|
2026-07-13 15:30:11 +08:00
|
|
|
].map((name) => path.join(previewComponentsDir, name))
|
2026-07-13 10:31:20 +08:00
|
|
|
const apiPath = path.join(root, 'src/api/modules/dataset.ts')
|
|
|
|
|
const adapterPath = path.join(root, 'src/mock/adapter.ts')
|
2026-07-11 10:39:26 +08:00
|
|
|
const mockPath = path.join(root, 'src/mock/data.ts')
|
2026-07-13 10:31:20 +08:00
|
|
|
const recordsPath = path.join(root, 'src/views/dataset/datasetRecords.ts')
|
|
|
|
|
const versionsPath = path.join(root, 'src/mock/datasetVersions.ts')
|
2026-07-11 10:39:26 +08:00
|
|
|
const source = fs.readFileSync(previewPath, 'utf8')
|
2026-07-14 14:47:12 +08:00
|
|
|
const previewLogicSource = source.slice(0, source.indexOf('<style'))
|
2026-07-13 15:30:11 +08:00
|
|
|
const previewComponentSources = previewComponentPaths.map((componentPath) => {
|
|
|
|
|
expect(fs.existsSync(componentPath), `缺少数据集预览叶子组件:${path.basename(componentPath)}`)
|
|
|
|
|
return fs.readFileSync(componentPath, 'utf8')
|
|
|
|
|
})
|
|
|
|
|
const previewSurfaceSource = [source, ...previewComponentSources].join('\n')
|
2026-07-13 10:31:20 +08:00
|
|
|
const apiSource = fs.readFileSync(apiPath, 'utf8')
|
|
|
|
|
const adapterSource = fs.readFileSync(adapterPath, 'utf8')
|
2026-07-11 10:39:26 +08:00
|
|
|
const mockSource = fs.readFileSync(mockPath, 'utf8')
|
2026-07-13 10:31:20 +08:00
|
|
|
const recordsSource = fs.readFileSync(recordsPath, 'utf8')
|
|
|
|
|
const versionsSource = fs.readFileSync(versionsPath, 'utf8')
|
|
|
|
|
const recordsModuleCode = ts.transpileModule(recordsSource, {
|
|
|
|
|
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
|
|
|
|
}).outputText
|
|
|
|
|
const { 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,
|
2026-07-14 14:47:12 +08:00
|
|
|
deleteDatasetVersion,
|
2026-07-13 10:31:20 +08:00
|
|
|
getActiveDatasetVersion,
|
|
|
|
|
} = await import(`data:text/javascript;base64,${Buffer.from(versionsModuleCode).toString('base64')}`)
|
2026-07-11 10:39:26 +08:00
|
|
|
|
|
|
|
|
function expect(condition, message) {
|
|
|
|
|
if (!condition) throw new Error(message)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-14 14:47:12 +08:00
|
|
|
expect(previewLogicSource.split('\n').length < 520, 'DatasetPreviewView.vue 的逻辑与模板应保持在 520 行以内')
|
2026-07-24 21:11:01 +08:00
|
|
|
for (const componentName of ['DatasetVersionBar', 'DatasetRecordTable', 'DatasetRecordEditorDialog', 'DatasetRawPreview', 'DatasetSplitNavigator']) {
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(source.includes(`import ${componentName}`), `主页面未导入叶子组件:${componentName}`)
|
|
|
|
|
expect(source.includes(`<${componentName}`), `主页面未挂载叶子组件:${componentName}`)
|
|
|
|
|
}
|
2026-07-13 10:31:20 +08:00
|
|
|
expect(source.includes('class="preview-workspace"'), '详情页应提供数据文件工作区')
|
2026-07-24 21:11:01 +08:00
|
|
|
expect(previewSurfaceSource.includes('class="split-file-tabs"'), '多切分数据集应提供训练/验证/测试文件切换')
|
|
|
|
|
expect(source.includes('@select="handleFileChange"'), '切换数据切分时应加载对应文件版本')
|
|
|
|
|
expect(previewSurfaceSource.includes('splitCounts.train'), '数据集概览应显示训练集数量')
|
|
|
|
|
expect(previewSurfaceSource.includes('splitCounts.validation'), '数据集概览应显示验证集数量')
|
|
|
|
|
expect(previewSurfaceSource.includes('splitCounts.test'), '数据集概览应显示测试集数量')
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(previewSurfaceSource.includes('class="code-line"'), '内容查看器应提供逐行展示')
|
|
|
|
|
expect(previewSurfaceSource.includes('class="line-number"'), '内容查看器应显示行号')
|
2026-07-11 10:39:26 +08:00
|
|
|
expect(source.includes('previewLoading'), '切换文件时应提供独立加载状态')
|
|
|
|
|
expect(source.includes('previewError'), '内容加载失败时应提供错误状态')
|
2026-07-13 15:30:11 +08:00
|
|
|
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('暂存修改'), '单条编辑应先暂存到当前页面')
|
2026-07-13 10:31:20 +08:00
|
|
|
expect(source.includes('hasPendingVersionChanges && isViewingActiveVersion'), '存在暂存修改时才应显示保存版本按钮')
|
|
|
|
|
expect(source.includes('保存版本'), '文件工具栏应提供保存版本按钮')
|
|
|
|
|
expect(source.includes('baseVersionContent'), '页面应区分已保存版本与待保存工作副本')
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(previewSurfaceSource.includes('class="version-control-bar"'), '详情页应提供独立版本控制区')
|
|
|
|
|
expect(previewSurfaceSource.includes('设为当前版本'), '历史版本应支持显式切换为当前版本')
|
2026-07-14 14:47:12 +08:00
|
|
|
expect(previewSurfaceSource.includes('class="version-actions"'), '历史版本操作应收纳到省略号菜单')
|
|
|
|
|
expect(previewSurfaceSource.includes('删除版本'), '历史版本操作菜单应提供删除入口')
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(previewSurfaceSource.includes('历史版本(只读)'), '历史版本应明确展示只读状态')
|
2026-07-14 14:47:12 +08:00
|
|
|
expect(source.includes('AppConfirmDialog'), '删除历史版本前应使用公共危险确认弹窗')
|
|
|
|
|
expect(source.includes("tone: 'danger'"), '删除历史版本确认弹窗应使用危险态')
|
2026-07-13 10:31:20 +08:00
|
|
|
expect(source.includes('loadedVersionId.value'), '下载和展示应绑定正在查看的具体版本')
|
|
|
|
|
expect(source.includes('versionRequestId'), '快速切换历史版本时应防止旧响应覆盖新内容')
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(previewSurfaceSource.includes('v-model:current-page="currentPage"'), '样本列表应支持分页浏览')
|
2026-07-13 10:31:20 +08:00
|
|
|
expect(!source.includes('class="content-editor"'), '详情页不应继续提供整文件编辑器')
|
|
|
|
|
expect(source.includes('saveRecord'), '逐条编辑器应提供单条保存动作')
|
|
|
|
|
expect(source.includes('hasUnsavedChanges'), '在线编辑器应跟踪未保存修改')
|
|
|
|
|
expect(source.includes('onBeforeRouteLeave'), '离开页面时应保护未保存修改')
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(previewSurfaceSource.includes("event.key.toLowerCase() === 's'"), '在线编辑器应支持快捷键保存')
|
2026-07-13 10:31:20 +08:00
|
|
|
expect(source.includes('loadVersions(selectedFile)'), '版本内容加载失败后应支持重新加载')
|
2026-07-13 15:30:11 +08:00
|
|
|
expect(source.includes('function resetRecordEditor()'), '主页面应提供统一的编辑器状态清理函数')
|
|
|
|
|
const resetEditorStart = source.indexOf('function resetRecordEditor()')
|
|
|
|
|
const updateFieldStart = source.indexOf('function updateEditField', resetEditorStart)
|
|
|
|
|
const resetEditorSource = source.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()'), '成功切换版本后必须关闭并清空旧编辑器状态')
|
2026-07-11 10:39:26 +08:00
|
|
|
expect(!source.includes('handleDownloadAll'), '页面不应保留整包下载逻辑')
|
2026-07-14 14:47:12 +08:00
|
|
|
expect(!source.includes('deleteDataset('), '详情页不应恢复删除整个数据集的逻辑')
|
2026-07-11 10:39:26 +08:00
|
|
|
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 数据集都应自动补齐预览文件')
|
2026-07-13 10:31:20 +08:00
|
|
|
expect(mockSource.includes(': [{ id: `dataset-${dataset.id}-samples`'), '每个 Mock 数据集应只对应一个数据文件')
|
|
|
|
|
expect(apiSource.includes('createDatasetFileVersion'), '数据集 API 应提供创建新版本方法')
|
|
|
|
|
expect(apiSource.includes('activateDatasetFileVersion'), '数据集 API 应提供切换当前版本方法')
|
2026-07-14 14:47:12 +08:00
|
|
|
expect(apiSource.includes('deleteDatasetFileVersion'), '数据集 API 应提供删除历史版本方法')
|
2026-07-13 10:31:20 +08:00
|
|
|
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('历史版本不可覆盖'), '旧的覆盖保存接口应明确拒绝修改历史版本')
|
2026-07-14 14:47:12 +08:00
|
|
|
expect(versionsSource.includes('当前版本不可删除'), 'Mock 应拒绝删除当前版本')
|
|
|
|
|
expect(versionsSource.includes('初始版本不可删除'), 'Mock 应拒绝删除初始版本')
|
2026-07-13 10:31:20 +08:00
|
|
|
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 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', '切换版本不得污染原状态或历史快照')
|
2026-07-14 14:47:12 +08:00
|
|
|
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} 受保护版本不得删除`)
|
|
|
|
|
}
|
2026-07-13 10:31:20 +08:00
|
|
|
let invalidVersionRejected = false
|
|
|
|
|
try {
|
|
|
|
|
activateDatasetVersion(v2State, 'missing')
|
|
|
|
|
} catch {
|
|
|
|
|
invalidVersionRejected = true
|
|
|
|
|
}
|
|
|
|
|
expect(invalidVersionRejected, '不存在的版本不得切换为当前版本')
|
2026-07-11 10:39:26 +08:00
|
|
|
|
|
|
|
|
console.log('dataset preview regression checks passed')
|