mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
feat: add AI flow test script — validates auth, lead lookup, patient, doctor, appointments, calls, activities as Rekha
This commit is contained in:
252
scripts/test-ai-flow.ts
Normal file
252
scripts/test-ai-flow.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Test script — simulates a CC agent (Rekha) receiving a call and using the AI assistant.
|
||||
* Tests: auth → lead lookup → patient data → doctor data → appointments → calls → activities → AI response
|
||||
*
|
||||
* Run: cd helix-engage && npx tsx scripts/test-ai-flow.ts
|
||||
*/
|
||||
|
||||
const GQL = 'http://localhost:4000/graphql';
|
||||
const SUB = 'fortytwo-dev';
|
||||
const ORIGIN = 'http://fortytwo-dev.localhost:4010';
|
||||
|
||||
// Rekha's credentials
|
||||
const AGENT_EMAIL = 'rekha.cc@globalhospital.com';
|
||||
const AGENT_PASSWORD = 'Global@123';
|
||||
|
||||
// Simulated incoming call — Priya's phone
|
||||
const CALLER_PHONE = '9949879837';
|
||||
|
||||
let token = '';
|
||||
let failures = 0;
|
||||
|
||||
async function gql(query: string, variables?: any) {
|
||||
const h: Record<string, string> = { 'Content-Type': 'application/json', 'X-Workspace-Subdomain': SUB };
|
||||
if (token) h['Authorization'] = `Bearer ${token}`;
|
||||
const r = await fetch(GQL, { method: 'POST', headers: h, body: JSON.stringify({ query, variables }) });
|
||||
const d: any = await r.json();
|
||||
if (d.errors) throw new Error(d.errors[0].message);
|
||||
return d.data;
|
||||
}
|
||||
|
||||
function assert(condition: boolean, label: string, detail?: string) {
|
||||
if (condition) {
|
||||
console.log(` ✅ ${label}${detail ? ` — ${detail}` : ''}`);
|
||||
} else {
|
||||
console.log(` ❌ ${label}${detail ? ` — ${detail}` : ''}`);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🧪 Helix Engage — AI Flow Test\n');
|
||||
console.log(`Agent: ${AGENT_EMAIL}`);
|
||||
console.log(`Incoming call from: ${CALLER_PHONE}\n`);
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 1: Login as Rekha
|
||||
// ═══════════════════════════════════════
|
||||
console.log('1️⃣ LOGIN');
|
||||
const d1 = await gql(`mutation { getLoginTokenFromCredentials(email: "${AGENT_EMAIL}", password: "${AGENT_PASSWORD}", origin: "${ORIGIN}") { loginToken { token } } }`);
|
||||
const lt = d1.getLoginTokenFromCredentials.loginToken.token;
|
||||
const d2 = await gql(`mutation { getAuthTokensFromLoginToken(loginToken: "${lt}", origin: "${ORIGIN}") { tokens { accessOrWorkspaceAgnosticToken { token } } } }`);
|
||||
token = d2.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken.token;
|
||||
assert(!!token, 'Authenticated as Rekha');
|
||||
|
||||
// Verify role
|
||||
const profile = await gql('{ currentUser { email workspaceMember { name { firstName lastName } roles { label } } } }');
|
||||
const roles = profile.currentUser.workspaceMember.roles.map((r: any) => r.label);
|
||||
const name = `${profile.currentUser.workspaceMember.name.firstName} ${profile.currentUser.workspaceMember.name.lastName}`;
|
||||
assert(roles.includes('HelixEngage User'), `Role: ${roles.join(', ')}`);
|
||||
assert(name.includes('Rekha'), `Name: ${name}`);
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 2: Look up caller by phone number
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n2️⃣ LEAD LOOKUP (by phone: 9949879837)');
|
||||
const leadsData = await gql(`{ leads(first: 50) { edges { node {
|
||||
id name contactName { firstName lastName }
|
||||
contactPhone { primaryPhoneNumber }
|
||||
contactEmail { primaryEmail }
|
||||
source status interestedService assignedAgent
|
||||
leadScore contactAttempts firstContacted lastContacted
|
||||
aiSummary aiSuggestedAction
|
||||
patientId campaignId
|
||||
} } } }`);
|
||||
|
||||
const leads = leadsData.leads.edges.map((e: any) => e.node);
|
||||
assert(leads.length > 0, `Found ${leads.length} leads total`);
|
||||
|
||||
// Match by phone
|
||||
const matchedLead = leads.find((l: any) => {
|
||||
const phone = l.contactPhone?.primaryPhoneNumber ?? '';
|
||||
return phone.includes(CALLER_PHONE) || CALLER_PHONE.includes(phone);
|
||||
});
|
||||
|
||||
assert(!!matchedLead, 'Matched lead by phone', matchedLead?.name);
|
||||
assert(matchedLead?.contactName?.firstName === 'Priya', `First name: ${matchedLead?.contactName?.firstName}`);
|
||||
assert(matchedLead?.status === 'APPOINTMENT_SET', `Status: ${matchedLead?.status}`);
|
||||
assert(!!matchedLead?.aiSummary, 'Has AI summary', matchedLead?.aiSummary?.substring(0, 80) + '...');
|
||||
assert(!!matchedLead?.aiSuggestedAction, 'Has AI suggested action', matchedLead?.aiSuggestedAction);
|
||||
assert(!!matchedLead?.patientId, 'Linked to patient', matchedLead?.patientId);
|
||||
assert(!!matchedLead?.campaignId, 'Linked to campaign', matchedLead?.campaignId);
|
||||
|
||||
const leadId = matchedLead?.id;
|
||||
const patientId = matchedLead?.patientId;
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 3: Fetch patient record
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n3️⃣ PATIENT RECORD');
|
||||
if (patientId) {
|
||||
const patientData = await gql(`{ patients(first: 1, filter: { id: { eq: "${patientId}" } }) { edges { node {
|
||||
id name fullName { firstName lastName }
|
||||
phones { primaryPhoneNumber }
|
||||
emails { primaryEmail }
|
||||
dateOfBirth gender patientType
|
||||
} } } }`);
|
||||
const patient = patientData.patients.edges[0]?.node;
|
||||
assert(!!patient, 'Patient record found', patient?.name);
|
||||
assert(patient?.gender === 'FEMALE', `Gender: ${patient?.gender}`);
|
||||
assert(patient?.patientType === 'RETURNING', `Type: ${patient?.patientType}`);
|
||||
assert(!!patient?.dateOfBirth, `DOB: ${patient?.dateOfBirth}`);
|
||||
} else {
|
||||
assert(false, 'No patient ID to look up');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 4: Fetch appointments for this patient
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n4️⃣ APPOINTMENTS');
|
||||
if (patientId) {
|
||||
const apptData = await gql(`{ appointments(first: 20, filter: { patientId: { eq: "${patientId}" } }) { edges { node {
|
||||
id name scheduledAt durationMin appointmentType status
|
||||
doctorName department reasonForVisit
|
||||
doctorId
|
||||
} } } }`);
|
||||
const appts = apptData.appointments.edges.map((e: any) => e.node);
|
||||
assert(appts.length >= 2, `Found ${appts.length} appointments`);
|
||||
|
||||
const completed = appts.filter((a: any) => a.status === 'COMPLETED');
|
||||
const scheduled = appts.filter((a: any) => a.status === 'SCHEDULED');
|
||||
assert(completed.length >= 1, `${completed.length} completed`);
|
||||
assert(scheduled.length >= 1, `${scheduled.length} upcoming`);
|
||||
|
||||
for (const a of appts) {
|
||||
const status = a.status === 'COMPLETED' ? '✓' : '📅';
|
||||
assert(!!a.doctorId, `${status} ${a.name} — doctor linked`, a.doctorId);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 5: Fetch doctor record (Dr. Patel)
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n5️⃣ DOCTOR LOOKUP');
|
||||
const doctorsData = await gql(`{ doctors(first: 10) { edges { node {
|
||||
id name fullName { firstName lastName }
|
||||
department specialty qualifications yearsOfExperience
|
||||
branchClinic visitingHours
|
||||
consultationFeeNew { amountMicros currencyCode }
|
||||
consultationFeeFollowUp { amountMicros currencyCode }
|
||||
active registrationNumber
|
||||
portalUserId
|
||||
} } } }`);
|
||||
const doctors = doctorsData.doctors.edges.map((e: any) => e.node);
|
||||
assert(doctors.length === 5, `Found ${doctors.length} doctors`);
|
||||
|
||||
const drPatel = doctors.find((d: any) => d.name?.includes('Patel'));
|
||||
assert(!!drPatel, 'Found Dr. Patel', drPatel?.specialty);
|
||||
assert(!!drPatel?.visitingHours, `Hours: ${drPatel?.visitingHours}`);
|
||||
assert(!!drPatel?.consultationFeeNew, `Fee (new): ₹${(drPatel?.consultationFeeNew?.amountMicros ?? 0) / 1_000_000}`);
|
||||
assert(!!drPatel?.portalUserId, 'Linked to workspace member', drPatel?.portalUserId);
|
||||
|
||||
const drSharma = doctors.find((d: any) => d.name?.includes('Sharma'));
|
||||
assert(!!drSharma, 'Found Dr. Sharma', drSharma?.specialty);
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 6: Fetch call history for this lead
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n6️⃣ CALL HISTORY');
|
||||
if (leadId) {
|
||||
const callsData = await gql(`{ calls(first: 20, filter: { leadId: { eq: "${leadId}" } }) { edges { node {
|
||||
id name direction callStatus agentName
|
||||
startedAt durationSec disposition
|
||||
} } } }`);
|
||||
const calls = callsData.calls.edges.map((e: any) => e.node);
|
||||
assert(calls.length >= 3, `Found ${calls.length} calls for Priya`);
|
||||
|
||||
for (const c of calls) {
|
||||
console.log(` ${c.direction} | ${c.disposition ?? c.callStatus} | ${c.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 7: Fetch lead activities
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n7️⃣ LEAD ACTIVITIES');
|
||||
if (leadId) {
|
||||
const activitiesData = await gql(`{ leadActivities(first: 30, filter: { leadId: { eq: "${leadId}" } }, orderBy: [{ occurredAt: DescNullsLast }]) { edges { node {
|
||||
id activityType summary occurredAt performedBy channel
|
||||
} } } }`);
|
||||
const activities = activitiesData.leadActivities.edges.map((e: any) => e.node);
|
||||
assert(activities.length >= 8, `Found ${activities.length} activities for Priya`);
|
||||
|
||||
for (const a of activities.slice(0, 5)) {
|
||||
console.log(` ${a.activityType} | ${a.summary}`);
|
||||
}
|
||||
if (activities.length > 5) console.log(` ... and ${activities.length - 5} more`);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 8: Simulate AI context assembly
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n8️⃣ AI CONTEXT ASSEMBLY');
|
||||
console.log(' What the AI assistant would receive:\n');
|
||||
console.log(` Caller: ${matchedLead?.contactName?.firstName} ${matchedLead?.contactName?.lastName}`);
|
||||
console.log(` Phone: ${CALLER_PHONE}`);
|
||||
console.log(` Status: ${matchedLead?.status}`);
|
||||
console.log(` Service: ${matchedLead?.interestedService}`);
|
||||
console.log(` AI Summary: ${matchedLead?.aiSummary}`);
|
||||
console.log(` Suggested Action: ${matchedLead?.aiSuggestedAction}`);
|
||||
console.log(` Doctor: Dr. Patel — ${drPatel?.specialty}`);
|
||||
console.log(` Next Appointment: ${drPatel?.visitingHours} at ${drPatel?.branchClinic}`);
|
||||
console.log(` Fee: ₹${(drPatel?.consultationFeeNew?.amountMicros ?? 0) / 1_000_000} (new) / ₹${(drPatel?.consultationFeeFollowUp?.amountMicros ?? 0) / 1_000_000} (follow-up)`);
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// STEP 9: Test AI chat endpoint (if sidecar is running)
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n9️⃣ AI CHAT (via sidecar — skipped if not running)');
|
||||
try {
|
||||
const aiRes = await fetch('http://localhost:4100/api/ai/chat', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify({
|
||||
message: 'Tell me about the patient on the line. When is their next appointment?',
|
||||
context: { callerPhone: CALLER_PHONE, leadId, leadName: `${matchedLead?.contactName?.firstName} ${matchedLead?.contactName?.lastName}` },
|
||||
}),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (aiRes.ok) {
|
||||
const aiData = await aiRes.json();
|
||||
assert(!!aiData.reply, 'AI responded');
|
||||
console.log(`\n 🤖 AI says:\n "${aiData.reply}"\n`);
|
||||
assert(aiData.sources?.length > 0, `Sources: ${aiData.sources?.join(', ')}`);
|
||||
} else {
|
||||
console.log(` ⏭️ Sidecar returned ${aiRes.status} — skipping AI test`);
|
||||
}
|
||||
} catch {
|
||||
console.log(' ⏭️ Sidecar not running — AI chat test skipped');
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════
|
||||
// RESULTS
|
||||
// ═══════════════════════════════════════
|
||||
console.log('\n' + '═'.repeat(50));
|
||||
if (failures === 0) {
|
||||
console.log('🎉 ALL TESTS PASSED');
|
||||
} else {
|
||||
console.log(`⚠️ ${failures} test(s) failed`);
|
||||
}
|
||||
console.log('═'.repeat(50));
|
||||
}
|
||||
|
||||
main().catch(e => { console.error('💥', e.message); process.exit(1); });
|
||||
Reference in New Issue
Block a user