| import axios from 'axios'; |
|
|
| |
| const BASE_URL = process.env.API_URL || 'https://medagen-backend.hf.space/api/health-check'; |
| const HEALTH_CHECK_URL = process.env.HEALTH_URL || BASE_URL.replace('/api/health-check', '/health'); |
|
|
| |
| const useCases = [ |
| { |
| id: 1, |
| name: 'MCP CV - Da liễu với hình ảnh', |
| expectedMCP: 'CV', |
| payload: { |
| text: 'Da tay nổi mẩn đỏ ngứa', |
| image_url: 'https://www.rosacea.org/sites/default/files/images/rosacea_subtype2.jpg', |
| user_id: 'test_user_1' |
| } |
| }, |
| { |
| id: 2, |
| name: 'MCP CV + RAG - Triệu chứng với hình ảnh', |
| expectedMCP: 'CV+RAG', |
| payload: { |
| text: 'Mặt nổi nhiều mụn trứng cá, đỏ và sưng', |
| image_url: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/Atopic_dermatitis.jpg/800px-Atopic_dermatitis.jpg', |
| user_id: 'test_user_2' |
| } |
| }, |
| { |
| id: 11, |
| name: 'MCP Hospital - Emergency case với location', |
| expectedMCP: 'HOSPITAL', |
| payload: { |
| text: 'Tôi bị đau ngực dữ dội, khó thở, đổ mồ hôi lạnh', |
| user_id: 'test_user_11', |
| location: { |
| lat: 10.762622, |
| lng: 106.660172 |
| } |
| } |
| }, |
| { |
| id: 12, |
| name: 'MCP Hospital - Urgent case với location', |
| expectedMCP: 'HOSPITAL', |
| payload: { |
| text: 'Tôi bị sốt cao 39 độ, đau đầu dữ dội, buồn nôn', |
| user_id: 'test_user_12', |
| location: { |
| lat: 21.028511, |
| lng: 105.804817 |
| } |
| } |
| }, |
| { |
| id: 13, |
| name: 'MCP Hospital - User yêu cầu tìm bệnh viện', |
| expectedMCP: 'HOSPITAL', |
| payload: { |
| text: 'Tôi cần tìm bệnh viện gần nhất để khám', |
| user_id: 'test_user_13', |
| location: { |
| lat: 10.762622, |
| lng: 106.660172 |
| } |
| } |
| }, |
| { |
| id: 14, |
| name: 'MCP CV + RAG + Hospital - Da liễu emergency với location', |
| expectedMCP: 'CV+RAG+HOSPITAL', |
| payload: { |
| text: 'Da tôi bị viêm nặng, đỏ rát, có mủ', |
| image_url: 'https://www.rosacea.org/sites/default/files/images/rosacea_subtype2.jpg', |
| user_id: 'test_user_14', |
| location: { |
| lat: 10.762622, |
| lng: 106.660172 |
| } |
| } |
| }, |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| ]; |
|
|
| let lastSessionId = null; |
|
|
| async function runTests() { |
| console.log('🚀 Starting MCP CV, RAG & CSDL API Tests...\n'); |
| console.log(`🌐 Testing endpoint: ${BASE_URL}`); |
| console.log(`🏥 Health check: ${HEALTH_CHECK_URL}\n`); |
| console.log('='.repeat(80)); |
| |
| const results = []; |
| let passed = 0; |
| let failed = 0; |
| let warnings = 0; |
|
|
| for (const useCase of useCases) { |
| console.log(`\n--- Use Case ${useCase.id}: ${useCase.name} ---`); |
| |
| |
| if (useCase.id === 9 && lastSessionId) { |
| useCase.payload.session_id = lastSessionId; |
| } |
| |
| try { |
| const startTime = Date.now(); |
| const response = await axios.post(BASE_URL, useCase.payload, { |
| timeout: 60000, |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| validateStatus: (status) => status < 500 |
| }); |
| const duration = Date.now() - startTime; |
|
|
| |
| if (response.data.session_id) { |
| lastSessionId = response.data.session_id; |
| } |
|
|
| |
| const responseText = JSON.stringify(response.data).toLowerCase(); |
| const hasRAGContent = responseText.includes('guideline') || |
| responseText.includes('hướng dẫn') || |
| responseText.includes('phòng bệnh') || |
| responseText.includes('điều trị') || |
| (response.data.recommendation?.details && |
| response.data.recommendation.details.length > 200); |
| |
| const hasCSDLContent = responseText.includes('định nghĩa') || |
| responseText.includes('nguyên nhân') || |
| responseText.includes('triệu chứng') || |
| responseText.includes('biến chứng') || |
| responseText.includes('tiên lượng') || |
| response.data.suspected_conditions?.length > 0; |
| |
| const hasCVContent = response.data.cv_findings?.model_used && |
| response.data.cv_findings.model_used !== 'none'; |
| |
| const hasHospitalContent = !!response.data.nearest_clinic; |
|
|
| const result = { |
| id: useCase.id, |
| name: useCase.name, |
| expectedMCP: useCase.expectedMCP, |
| status: 'PASS', |
| statusCode: response.status, |
| duration: `${duration}ms`, |
| triageLevel: response.data.triage_level, |
| hasRedFlags: response.data.red_flags?.length > 0, |
| hasSuspectedConditions: response.data.suspected_conditions?.length > 0, |
| hasRecommendation: !!response.data.recommendation?.action, |
| sessionId: response.data.session_id || null, |
| ragDetected: hasRAGContent, |
| csdlDetected: hasCSDLContent, |
| cvDetected: hasCVContent, |
| hospitalDetected: hasHospitalContent, |
| cvModel: response.data.cv_findings?.model_used || 'none', |
| hospitalName: response.data.nearest_clinic?.name || null, |
| hospitalDistance: response.data.nearest_clinic?.distance_km || null, |
| responseLength: JSON.stringify(response.data).length |
| }; |
|
|
| |
| const validations = []; |
| |
| if (!response.data.triage_level) { |
| validations.push('Missing triage_level'); |
| } |
| |
| if (!response.data.symptom_summary) { |
| validations.push('Missing symptom_summary'); |
| } |
| |
| if (!response.data.recommendation) { |
| validations.push('Missing recommendation'); |
| } |
| |
| |
| if (useCase.expectedMCP === 'CV' && !result.cvDetected) { |
| validations.push('Expected CV but not detected in response'); |
| } |
| |
| if (useCase.expectedMCP === 'CV+RAG') { |
| if (!result.cvDetected) validations.push('Expected CV but not detected'); |
| if (!result.ragDetected) validations.push('Expected RAG but not detected'); |
| } |
| |
| if (useCase.expectedMCP === 'CV+RAG+HOSPITAL') { |
| if (!result.cvDetected) validations.push('Expected CV but not detected'); |
| if (!result.ragDetected) validations.push('Expected RAG but not detected'); |
| if (!result.hospitalDetected) validations.push('Expected Hospital but not detected'); |
| } |
| |
| if (useCase.expectedMCP === 'RAG' && !result.ragDetected) { |
| validations.push('Expected RAG but not detected in response'); |
| } |
| |
| if (useCase.expectedMCP === 'CSDL' && !result.csdlDetected) { |
| validations.push('Expected CSDL but not detected in response'); |
| } |
| |
| if (useCase.expectedMCP === 'HOSPITAL') { |
| if (!result.hospitalDetected) validations.push('Expected Hospital but not detected'); |
| } |
| |
| if (useCase.expectedMCP === 'BOTH') { |
| if (!result.ragDetected && !result.csdlDetected) { |
| validations.push('Expected both RAG and CSDL but neither detected'); |
| } else if (!result.ragDetected) { |
| validations.push('Expected RAG but not detected'); |
| } else if (!result.csdlDetected) { |
| validations.push('Expected CSDL but not detected'); |
| } |
| } |
| |
| |
| if (result.responseLength < 500) { |
| validations.push('Response seems too short (may lack MCP content)'); |
| } |
|
|
| if (validations.length > 0) { |
| result.status = 'WARNING'; |
| result.validations = validations; |
| warnings++; |
| } else { |
| passed++; |
| } |
|
|
| results.push(result); |
|
|
| |
| const statusIcon = response.status === 200 ? '✅' : response.status < 300 ? '⚠️' : '❌'; |
| console.log(`${statusIcon} Status: ${response.status} (${duration}ms)`); |
| console.log(` Expected MCP: ${result.expectedMCP}`); |
| console.log(` CV Detected: ${result.cvDetected ? '✅' : '❌'} (${result.cvModel})`); |
| console.log(` RAG Detected: ${result.ragDetected ? '✅' : '❌'}`); |
| console.log(` CSDL Detected: ${result.csdlDetected ? '✅' : '❌'}`); |
| console.log(` Hospital Detected: ${result.hospitalDetected ? '✅' : '❌'}`); |
| if (result.hospitalDetected) { |
| console.log(` Hospital: ${result.hospitalName || 'N/A'} (${result.hospitalDistance ? result.hospitalDistance + 'km' : 'N/A'})`); |
| } |
| console.log(` Triage Level: ${result.triageLevel || 'N/A'}`); |
| console.log(` Suspected Conditions: ${result.hasSuspectedConditions ? 'Yes' : 'No'}`); |
| console.log(` Response Length: ${result.responseLength} chars`); |
| if (result.sessionId) { |
| console.log(` Session ID: ${result.sessionId.substring(0, 8)}...`); |
| } |
| if (validations.length > 0) { |
| console.log(` ⚠️ Warnings: ${validations.join(', ')}`); |
| } |
|
|
| } catch (error) { |
| failed++; |
| const result = { |
| id: useCase.id, |
| name: useCase.name, |
| status: 'FAIL', |
| error: error.response?.data?.message || error.message, |
| statusCode: error.response?.status || 'N/A' |
| }; |
| results.push(result); |
| |
| console.log(`❌ FAIL: ${result.error}`); |
| if (error.response?.data) { |
| console.log(` Response: ${JSON.stringify(error.response.data).substring(0, 200)}...`); |
| } |
| } |
| } |
|
|
| |
| console.log('\n' + '='.repeat(80)); |
| console.log('📊 TEST SUMMARY REPORT'); |
| console.log('='.repeat(80)); |
| console.log(`Total Use Cases: ${useCases.length}`); |
| console.log(`✅ Passed: ${passed}`); |
| console.log(`⚠️ Warnings: ${warnings}`); |
| console.log(`❌ Failed: ${failed}`); |
| console.log(`📈 Success Rate: ${((passed / useCases.length) * 100).toFixed(1)}%`); |
| |
| console.log('\n--- Detailed Results (Short) ---'); |
| results.forEach(r => { |
| const icon = r.status === 'PASS' ? '✅' : r.status === 'WARNING' ? '⚠️' : '❌'; |
| const mcpUsed = []; |
| if (r.cvDetected) mcpUsed.push('CV'); |
| if (r.ragDetected) mcpUsed.push('RAG'); |
| if (r.csdlDetected) mcpUsed.push('CSDL'); |
| if (r.hospitalDetected) mcpUsed.push('HOSPITAL'); |
| |
| console.log(`${icon} UC${r.id}: ${r.name}`); |
| if (r.status === 'PASS' || r.status === 'WARNING') { |
| console.log(` Expected: ${r.expectedMCP} | Used: ${mcpUsed.join('+') || 'None'} | Triage: ${r.triageLevel}`); |
| if (r.hospitalDetected) { |
| console.log(` 🏥 Hospital: ${r.hospitalName} (${r.hospitalDistance}km)`); |
| } |
| if (r.validations && r.validations.length > 0) { |
| console.log(` ⚠️ ${r.validations.join(', ')}`); |
| } |
| } else { |
| console.log(` ❌ Error: ${r.error}`); |
| } |
| }); |
|
|
| console.log('\n--- Performance Metrics ---'); |
| const durations = results |
| .filter(r => r.duration) |
| .map(r => parseInt(r.duration.replace('ms', ''))); |
| if (durations.length > 0) { |
| const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length; |
| const minDuration = Math.min(...durations); |
| const maxDuration = Math.max(...durations); |
| console.log(` Average Response Time: ${avgDuration.toFixed(0)}ms`); |
| console.log(` Min: ${minDuration}ms | Max: ${maxDuration}ms`); |
| } |
|
|
| console.log('\n--- MCP Usage Statistics ---'); |
| let ragCount = 0; |
| let csdlCount = 0; |
| let cvCount = 0; |
| let hospitalCount = 0; |
| let bothCount = 0; |
| let allThreeCount = 0; |
| let noneCount = 0; |
| |
| results.forEach(r => { |
| if (r.status === 'PASS' || r.status === 'WARNING') { |
| if (r.ragDetected) ragCount++; |
| if (r.csdlDetected) csdlCount++; |
| if (r.cvDetected) cvCount++; |
| if (r.hospitalDetected) hospitalCount++; |
| |
| if (r.ragDetected && r.csdlDetected && r.hospitalDetected) { |
| allThreeCount++; |
| } else if (r.ragDetected && r.csdlDetected) { |
| bothCount++; |
| } else if (!r.ragDetected && !r.csdlDetected && !r.cvDetected && !r.hospitalDetected) { |
| noneCount++; |
| } |
| } |
| }); |
| |
| console.log(` CV: ${cvCount}`); |
| console.log(` RAG: ${ragCount}`); |
| console.log(` CSDL: ${csdlCount}`); |
| console.log(` Hospital: ${hospitalCount}`); |
| console.log(` RAG + CSDL: ${bothCount}`); |
| console.log(` RAG + CSDL + Hospital: ${allThreeCount}`); |
| console.log(` None: ${noneCount}`); |
|
|
| console.log('\n--- Expected vs Actual MCP Usage ---'); |
| const mcpStats = {}; |
| results.forEach(r => { |
| if (r.status === 'PASS' || r.status === 'WARNING') { |
| const key = r.expectedMCP; |
| if (!mcpStats[key]) { |
| mcpStats[key] = { total: 0, ragDetected: 0, csdlDetected: 0, both: 0, none: 0 }; |
| } |
| mcpStats[key].total++; |
| if (r.ragDetected && r.csdlDetected) { |
| mcpStats[key].both++; |
| } else if (r.ragDetected) { |
| mcpStats[key].ragDetected++; |
| } else if (r.csdlDetected) { |
| mcpStats[key].csdlDetected++; |
| } else { |
| mcpStats[key].none++; |
| } |
| } |
| }); |
| |
| Object.entries(mcpStats).forEach(([mcp, stats]) => { |
| console.log(` ${mcp}:`); |
| console.log(` Total: ${stats.total} | RAG: ${stats.ragDetected} | CSDL: ${stats.csdlDetected} | Both: ${stats.both} | None: ${stats.none}`); |
| }); |
|
|
| console.log('\n--- Triage Level Distribution ---'); |
| const triageCounts = {}; |
| results.forEach(r => { |
| if (r.triageLevel) { |
| triageCounts[r.triageLevel] = (triageCounts[r.triageLevel] || 0) + 1; |
| } |
| }); |
| Object.entries(triageCounts).forEach(([level, count]) => { |
| console.log(` ${level}: ${count}`); |
| }); |
|
|
| console.log('\n' + '='.repeat(80)); |
| |
| if (failed === 0 && warnings === 0) { |
| console.log('🎉 All tests passed perfectly!'); |
| process.exit(0); |
| } else if (failed === 0) { |
| console.log('✅ All tests passed with some warnings'); |
| process.exit(0); |
| } else { |
| console.log('⚠️ Some tests failed. Please review the results above.'); |
| process.exit(1); |
| } |
| } |
|
|
| |
| async function checkServer() { |
| try { |
| await axios.get(HEALTH_CHECK_URL, { timeout: 10000 }); |
| return true; |
| } catch (error) { |
| console.error(`❌ Health check failed: ${error.message}`); |
| return false; |
| } |
| } |
|
|
| async function main() { |
| console.log(`🌐 Testing against: ${BASE_URL}`); |
| console.log(`🏥 Health check: ${HEALTH_CHECK_URL}\n`); |
| |
| const serverRunning = await checkServer(); |
| if (!serverRunning) { |
| console.error(`❌ Server is not responding at ${HEALTH_CHECK_URL}`); |
| console.error('Please check if the HuggingFace Space is running or set API_URL environment variable'); |
| console.error('Example: API_URL=https://your-space.hf.space/api/health-check npx tsx test_api_usecases.js'); |
| process.exit(1); |
| } |
| |
| console.log('✅ Server is responding!\n'); |
| await runTests(); |
| } |
|
|
| main().catch((error) => { |
| console.error('Fatal error:', error); |
| process.exit(1); |
| }); |
|
|
|
|