Files
AIclinicalresearch/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts
HaHafeng 9b81aef9a7 feat(dc): Add multi-metric transformation feature (direction 1+2)
Summary:
- Implement intelligent multi-metric grouping detection algorithm
- Add direction 1: timepoint-as-row, metric-as-column (analysis format)
- Add direction 2: timepoint-as-column, metric-as-row (display format)
- Fix column name pattern detection (FMA___ issue)
- Maintain original Record ID order in output
- Add full-select/clear buttons in UI
- Integrate into TransformDialog with Radio selection
- Update 3 documentation files

Technical Details:
- Python: detect_metric_groups(), apply_multi_metric_to_long(), apply_multi_metric_to_matrix()
- Backend: 3 new methods in QuickActionService
- Frontend: MultiMetricPanel.tsx (531 lines)
- Total: ~1460 lines of new code

Status: Fully tested and verified, ready for production
2025-12-21 15:06:15 +08:00

326 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 全文复筛API集成测试
*
* 运行方式:
* npx tsx src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const BASE_URL = 'http://localhost:3001';
const API_PREFIX = '/api/v1/asl/fulltext-screening';
// 测试辅助函数
async function fetchJSON(url: string, options: RequestInit = {}) {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
const data = await response.json();
return { response, data };
}
// 等待函数
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function runTests() {
console.log('🧪 开始全文复筛API集成测试\n');
try {
// ==================== 准备测试数据 ====================
console.log('📋 步骤1: 准备测试数据...');
// 获取第一个项目
const project = await prisma.aslScreeningProject.findFirst({
include: {
literatures: {
take: 3,
select: {
id: true,
title: true,
},
},
},
});
if (!project) {
throw new Error('未找到测试项目,请先创建项目和文献');
}
if (project.literatures.length === 0) {
throw new Error('项目中没有文献,请先导入文献');
}
const projectId = project.id;
const literatureIds = project.literatures.map((lit) => lit.id).slice(0, 2);
console.log(` ✅ 项目ID: ${projectId}`);
console.log(` ✅ 文献数量: ${literatureIds.length}`);
console.log(` ✅ 文献列表:`, literatureIds);
console.log('');
// ==================== 测试API 1: 创建任务 ====================
console.log('📋 步骤2: 测试创建全文复筛任务...');
const { response: createResponse, data: createData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/tasks`,
{
method: 'POST',
body: JSON.stringify({
projectId,
literatureIds,
modelA: 'deepseek-v3',
modelB: 'qwen-max',
promptVersion: 'v1.0.0',
}),
}
);
if (createResponse.status !== 201 || !createData.success) {
throw new Error(`创建任务失败: ${JSON.stringify(createData)}`);
}
const taskId = createData.data.taskId;
console.log(` ✅ 任务创建成功`);
console.log(` ✅ 任务ID: ${taskId}`);
console.log(` ✅ 状态: ${createData.data.status}`);
console.log(` ✅ 文献总数: ${createData.data.totalCount}`);
console.log('');
// ==================== 测试API 2: 获取任务进度 ====================
console.log('📋 步骤3: 测试获取任务进度...');
// 等待一段时间让任务开始处理
console.log(' ⏳ 等待3秒让任务开始处理...');
await sleep(3000);
const { response: progressResponse, data: progressData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/tasks/${taskId}`
);
if (progressResponse.status !== 200 || !progressData.success) {
throw new Error(`获取进度失败: ${JSON.stringify(progressData)}`);
}
console.log(` ✅ 任务状态: ${progressData.data.status}`);
console.log(` ✅ 进度: ${progressData.data.progress.processedCount}/${progressData.data.progress.totalCount} (${progressData.data.progress.progressPercent}%)`);
console.log(` ✅ 成功: ${progressData.data.progress.successCount}`);
console.log(` ✅ 失败: ${progressData.data.progress.failedCount}`);
console.log(` ✅ 降级: ${progressData.data.progress.degradedCount}`);
console.log(` ✅ Token: ${progressData.data.statistics.totalTokens}`);
console.log(` ✅ 成本: ¥${progressData.data.statistics.totalCost.toFixed(4)}`);
console.log('');
// 如果任务还在处理,等待完成
if (progressData.data.status === 'processing' || progressData.data.status === 'pending') {
console.log(' ⏳ 任务仍在处理中,等待完成...');
let attempts = 0;
const maxAttempts = 20; // 最多等待20次约100秒
while (attempts < maxAttempts) {
await sleep(5000); // 每5秒查询一次
const { data: checkData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/tasks/${taskId}`
);
console.log(` 📊 [${attempts + 1}/${maxAttempts}] 进度: ${checkData.data.progress.progressPercent}%, 状态: ${checkData.data.status}`);
if (checkData.data.status === 'completed' || checkData.data.status === 'failed') {
console.log(` ✅ 任务已完成,状态: ${checkData.data.status}`);
break;
}
attempts++;
}
if (attempts >= maxAttempts) {
console.log(' ⚠️ 任务处理超时但继续测试后续API');
}
console.log('');
}
// ==================== 测试API 3: 获取任务结果 ====================
console.log('📋 步骤4: 测试获取任务结果...');
// 4.1 获取所有结果
const { response: resultsResponse, data: resultsData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/results`
);
if (resultsResponse.status !== 200 || !resultsData.success) {
throw new Error(`获取结果失败: ${JSON.stringify(resultsData)}`);
}
console.log(` ✅ 获取所有结果成功`);
console.log(` ✅ 总结果数: ${resultsData.data.total}`);
console.log(` ✅ 当前页结果数: ${resultsData.data.results.length}`);
console.log(` ✅ 冲突数: ${resultsData.data.summary.conflictCount}`);
console.log(` ✅ 待审核: ${resultsData.data.summary.pendingReview}`);
console.log(` ✅ 已审核: ${resultsData.data.summary.reviewed}`);
if (resultsData.data.results.length > 0) {
const firstResult = resultsData.data.results[0];
console.log(`\n 📄 第一个结果详情:`);
console.log(` - 文献ID: ${firstResult.literatureId}`);
console.log(` - 标题: ${firstResult.literature.title.slice(0, 60)}...`);
console.log(` - 模型A状态: ${firstResult.modelAResult.status}`);
console.log(` - 模型B状态: ${firstResult.modelBResult.status}`);
console.log(` - 是否冲突: ${firstResult.conflict.isConflict ? '是' : '否'}`);
console.log(` - 最终决策: ${firstResult.review.finalDecision || '待审核'}`);
}
console.log('');
// 4.2 测试筛选功能
console.log(' 🔍 测试结果筛选功能...');
const { data: conflictData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/results?filter=conflict`
);
console.log(` ✅ 冲突项筛选: ${conflictData.data.filtered}`);
const { data: pendingData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/results?filter=pending`
);
console.log(` ✅ 待审核筛选: ${pendingData.data.filtered}`);
console.log('');
// ==================== 测试API 4: 人工审核决策 ====================
if (resultsData.data.results.length > 0) {
console.log('📋 步骤5: 测试人工审核决策...');
const resultId = resultsData.data.results[0].resultId;
// 5.1 测试纳入决策
const { response: decisionResponse, data: decisionData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/results/${resultId}/decision`,
{
method: 'PUT',
body: JSON.stringify({
finalDecision: 'include',
reviewNotes: '集成测试 - 自动审核纳入',
}),
}
);
if (decisionResponse.status !== 200 || !decisionData.success) {
throw new Error(`更新决策失败: ${JSON.stringify(decisionData)}`);
}
console.log(` ✅ 更新决策成功`);
console.log(` ✅ 结果ID: ${decisionData.data.resultId}`);
console.log(` ✅ 最终决策: ${decisionData.data.finalDecision}`);
console.log(` ✅ 审核人: ${decisionData.data.reviewedBy}`);
console.log(` ✅ 审核时间: ${new Date(decisionData.data.reviewedAt).toLocaleString('zh-CN')}`);
console.log('');
// 5.2 测试排除决策(如果有第二个结果)
if (resultsData.data.results.length > 1) {
const secondResultId = resultsData.data.results[1].resultId;
const { data: excludeData } = await fetchJSON(
`${BASE_URL}${API_PREFIX}/results/${secondResultId}/decision`,
{
method: 'PUT',
body: JSON.stringify({
finalDecision: 'exclude',
exclusionReason: '测试排除原因 - 数据不完整',
reviewNotes: '集成测试 - 自动审核排除',
}),
}
);
console.log(` ✅ 排除决策测试成功`);
console.log(` ✅ 排除原因: ${excludeData.data.exclusionReason}`);
console.log('');
}
}
// ==================== 测试API 5: 导出Excel ====================
console.log('📋 步骤6: 测试导出Excel...');
const exportResponse = await fetch(
`${BASE_URL}${API_PREFIX}/tasks/${taskId}/export`,
{
headers: {
Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
}
);
if (exportResponse.status !== 200) {
throw new Error(`导出Excel失败: ${exportResponse.statusText}`);
}
const buffer = await exportResponse.arrayBuffer();
console.log(` ✅ Excel导出成功`);
console.log(` ✅ 文件大小: ${(buffer.byteLength / 1024).toFixed(2)} KB`);
console.log(` ✅ Content-Type: ${exportResponse.headers.get('Content-Type')}`);
console.log('');
// ==================== 测试完成 ====================
console.log('✅ 所有测试通过!\n');
console.log('📊 测试总结:');
console.log(' ✅ API 1: 创建任务 - 通过');
console.log(' ✅ API 2: 获取进度 - 通过');
console.log(' ✅ API 3: 获取结果 - 通过');
console.log(' ✅ API 4: 人工审核 - 通过');
console.log(' ✅ API 5: 导出Excel - 通过');
console.log('');
} catch (error: any) {
console.error('\n❌ 测试失败:', error.message);
console.error('\n详细错误:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// 运行测试
runTests().catch((error) => {
console.error('测试运行失败:', error);
process.exit(1);
});