CognxSafeTrack
chore: execute Sprint 38 technical debt resolution (Type Safety, Zod validation, Vitest, Mock LLM extracted)
d9879cf | import axios from 'axios'; | |
| import crypto from 'crypto'; | |
| import { PrismaClient } from '@repo/database'; | |
| const prisma = new PrismaClient(); | |
| const API_URL = process.env.API_URL || 'http://localhost:8080/v1'; | |
| const ADMIN_API_KEY = process.env.ADMIN_API_KEY || 'default_admin_key'; // Update accordingly | |
| const WHATSAPP_APP_SECRET = process.env.WHATSAPP_APP_SECRET || ''; // If set, needed for webhook payload signing | |
| const TEST_PHONE = '221770000000'; // Fake test number | |
| // Helper to sign webhook payload | |
| function generateSignature(payload: string): string { | |
| if (!WHATSAPP_APP_SECRET) return ''; | |
| const hash = crypto.createHmac('sha256', WHATSAPP_APP_SECRET).update(payload).digest('hex'); | |
| return `sha256=${hash}`; | |
| } | |
| // Helper to simulate incoming WhatsApp Text | |
| async function simulateWhatsAppMessage(phone: string, text: string) { | |
| const payload = JSON.stringify({ | |
| object: 'whatsapp_business_account', | |
| entry: [{ | |
| id: 'test_entry', | |
| changes: [{ | |
| value: { | |
| messaging_product: 'whatsapp', | |
| metadata: { phone_number_id: 'test_phone_id' }, | |
| messages: [{ | |
| from: phone, | |
| id: `msg_${Date.now()}`, | |
| timestamp: Date.now().toString(), | |
| type: 'text', | |
| text: { body: text } | |
| }] | |
| }, | |
| field: 'messages' | |
| }] | |
| }] | |
| }); | |
| const headers: Record<string, string> = { 'Content-Type': 'application/json' }; | |
| const signature = generateSignature(payload); | |
| if (signature) headers['x-hub-signature-256'] = signature; | |
| try { | |
| await axios.post(`${API_URL}/whatsapp/webhook`, payload, { headers }); | |
| console.log(`[E2E] Simulated WhatsApp Message from ${phone}: "${text}"`); | |
| } catch (e: unknown) { | |
| console.error(`[E2E] Failed to simulate message: ${(e as any).response?.data?.error || (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`); | |
| } | |
| } | |
| // Helper to sleep for async DB updates | |
| const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); | |
| async function runTests() { | |
| console.log('🚀 Starting XAMLÉ E2E Journey Test (Sprints 22 & 23)'); | |
| try { | |
| // 1. Cleanup previous test state | |
| await prisma.userProgress.deleteMany({ where: { user: { phone: TEST_PHONE } } }); | |
| await prisma.response.deleteMany({ where: { user: { phone: TEST_PHONE } } }); | |
| await prisma.enrollment.deleteMany({ where: { user: { phone: TEST_PHONE } } }); | |
| await prisma.message.deleteMany({ where: { user: { phone: TEST_PHONE } } }); | |
| await prisma.businessProfile.deleteMany({ where: { user: { phone: TEST_PHONE } } }); | |
| await prisma.user.deleteMany({ where: { phone: TEST_PHONE } }); | |
| console.log('[E2E] Cleanup complete.'); | |
| // Initialize User directly for speed, or via onboarding. | |
| // We'll create the user directly in DB to simulate the exact WOLOF Couture state | |
| const track = await prisma.track.findFirst({ where: { title: { contains: 'Comprendre son business' } } }); | |
| if (!track) throw new Error("Could not find T1 Track in DB."); | |
| const user = await prisma.user.create({ | |
| data: { | |
| phone: TEST_PHONE, | |
| language: 'WOLOF', | |
| activity: 'Couture', | |
| } | |
| }); | |
| const enrollment = await prisma.enrollment.create({ | |
| data: { | |
| userId: user.id, | |
| trackId: track.id, | |
| currentDay: 1, | |
| status: 'ACTIVE' | |
| } | |
| }); | |
| console.log(`[E2E] Created Test User | ID: ${user.id} | Track: ${track.id} | CurrentDay: 1`); | |
| // --- TEST 1: ANTI-SAUT --- | |
| console.log('\n--- TEST 1: ANTI-SAUT ---'); | |
| // Simulate sending a response to Day 1 | |
| console.log(`[E2E] Sending Day 1 Answer...`); | |
| await simulateWhatsAppMessage(TEST_PHONE, "Couture answer for day 1"); | |
| await sleep(3000); // give time for queue & worker to process AI eval | |
| // User should normally receive Bravo and then reply 'SUITE'. Let's simulate 'SUITE'. | |
| console.log(`[E2E] Sending 'SUITE' keyword...`); | |
| await simulateWhatsAppMessage(TEST_PHONE, "SUITE"); | |
| await sleep(2000); | |
| const updatedEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } }); | |
| if (updatedEnrollment?.currentDay === 2) { | |
| console.log(`✅ [Anti-Saut Test Passed] currentDay est exactement 2.`); | |
| } else { | |
| console.error(`❌ [Anti-Saut Test Failed] currentDay is ${updatedEnrollment?.currentDay} instead of 2.`); | |
| } | |
| // --- TEST 2: HUMAN-IN-THE-LOOP --- | |
| console.log('\n--- TEST 2: HUMAN-IN-THE-LOOP (WOLOF AUDIO) ---'); | |
| // Since we can't easily mock an audio upload to Meta right now, we will manually insert a `PENDING_REVIEW` | |
| // into the tracking flow. Actually, we can trigger the Worker indirectly, but inserting the userProgress | |
| // to mimic Wolof interception is simpler for an E2E API test. | |
| console.log(`[E2E] Simulating WOLOF Interception (Setting PENDING_REVIEW)...`); | |
| await prisma.userProgress.upsert({ | |
| where: { userId_trackId: { userId: user.id, trackId: track.id } }, | |
| update: { exerciseStatus: 'PENDING_REVIEW' as any }, | |
| create: { userId: user.id, trackId: track.id, exerciseStatus: 'PENDING_REVIEW' as any } | |
| }); | |
| const preReview = await prisma.userProgress.findUnique({ where: { userId_trackId: { userId: user.id, trackId: track.id } } }); | |
| if ((preReview?.exerciseStatus as string) === 'PENDING_REVIEW') { | |
| console.log(`✅ [Human-in-the-Loop] Status correctly set to PENDING_REVIEW.`); | |
| } | |
| console.log(`[E2E] Admin calling POST /admin/override-feedback...`); | |
| try { | |
| const adminRes = await axios.post(`${API_URL}/admin/override-feedback`, { | |
| userId: user.id, | |
| trackId: track.id, | |
| transcription: 'Traduction manuelle validée de la couture.', | |
| overrideAudioUrl: 'https://fake-s3-url.com/audio.webm', | |
| adminId: 'E2E_ADMIN' | |
| }, { | |
| headers: { 'Authorization': `Bearer ${ADMIN_API_KEY}` } | |
| }); | |
| console.log(`[E2E] Admin Overdrive Response: ${adminRes.status}`); | |
| // Wait for BullMQ to process the overdrive logic and send SUITE automatically | |
| await sleep(4000); | |
| const postReview = await prisma.userProgress.findUnique({ where: { userId_trackId: { userId: user.id, trackId: track.id } } }); | |
| const progressEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } }); | |
| if ((postReview?.exerciseStatus as string) === 'COMPLETED' && progressEnrollment?.currentDay === 3) { | |
| console.log(`✅ [Human-in-the-Loop Test Passed] User unlocked & progressed to Day 3.`); | |
| } else { | |
| console.log(`⚠️ [Human-in-the-Loop Note] exerciseStatus: ${postReview?.exerciseStatus}, currentDay: ${progressEnrollment?.currentDay}`); | |
| } | |
| } catch (e: unknown) { | |
| console.error(`❌ [Human-in-the-Loop Test Failed] API Error: ${(e as any).response?.data?.error || (e instanceof Error ? (e instanceof Error ? e.message : String(e)) : String(e))}`); | |
| } | |
| // --- TEST 3: BADGE GUARD --- | |
| console.log('\n--- TEST 3: BADGE GUARD VERIFICATION ---'); | |
| // Inject a REPRISE badge and force currentDay to 3.5 (Fractional) | |
| await prisma.userProgress.update({ | |
| where: { userId_trackId: { userId: user.id, trackId: track.id } }, | |
| data: { badges: ['CLARTÉ', 'REPRISE'] } | |
| }); | |
| await prisma.enrollment.update({ | |
| where: { id: enrollment.id }, | |
| data: { currentDay: 3.5 } | |
| }); | |
| console.log(`[E2E] User has Badges: ['CLARTÉ', 'REPRISE'] on Day 3.5`); | |
| // Simulating the pedagogy send logic. If we were to send SUITE now, it would increment to 4 and trigger pedagogy.ts. | |
| console.log(`[E2E] Sending 'SUITE' to trigger Day 4 (which should NOT show REPRISE badge)...`); | |
| await simulateWhatsAppMessage(TEST_PHONE, "SUITE"); | |
| await sleep(3000); | |
| const finalEnrollment = await prisma.enrollment.findFirst({ where: { userId: user.id, trackId: track.id } }); | |
| console.log(`[E2E] Final Day is now: ${finalEnrollment?.currentDay}. Check server logs above for [Badge Guard] isVisible: false.`); | |
| console.log('\n🎉 All specified E2E simulations completed.'); | |
| process.exit(0); | |
| } catch (error) { | |
| console.error('❌ E2E Test Execution Failed:', error); | |
| process.exit(1); | |
| } | |
| } | |
| runTests(); | |