feat: add AI flow test script — validates auth, lead lookup, patient, doctor, appointments, calls, activities as Rekha

This commit is contained in:
2026-03-18 14:46:19 +05:30
parent f5c8766f68
commit e01a4d7747

252
scripts/test-ai-flow.ts Normal file
View 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); });