Files
helix-engage/scripts/test-ai-flow.ts
saridsa2 44bd221108 feat: deploy to Hostinger VPS, switch to global_healthx Ozonetel account
- Add helix.svg and PNG favicon (generated via nano-banana)
- Update page title to "Helix Engage" with proper meta tags
- Make seed scripts configurable via SEED_GQL/SEED_ORIGIN env vars
- Support remote workspace member IDs in seed-data.ts
- Dynamic doctor-to-clinic linking in seed-new-entities.ts (fetch IDs from platform)
- Remove deprecated branchClinic field from seed data
- Fix TypeScript errors: callNotes null vs undefined, Lead type casting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 10:27:25 +05:30

254 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 = process.env.SEED_GQL ?? 'http://localhost:4000/graphql';
const SUB = 'fortytwo-dev';
const ORIGIN = process.env.SEED_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
visitingHours
consultationFeeNew { amountMicros currencyCode }
consultationFeeFollowUp { amountMicros currencyCode }
active registrationNumber
portalUserId
clinic { id clinicName }
} } } }`);
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?.clinic?.clinicName ?? 'N/A'}`);
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); });