Spaces:
Paused
Paused
File size: 9,827 Bytes
b43bc6c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | #!/usr/bin/env node
/**
* OMNIA Agent β repeatable end-to-end smoke test (no secrets printed).
*
* Drives the live agent over its SSE endpoint with the customer prompts from the
* brief and asserts the assembled checkout / clarify / refusal. Also checks CORS
* for 127.0.0.1 and that signed-out marketplace bookings are rejected (401).
*
* Usage: node scripts/e2e-agent.mjs (needs the 4 backends running)
* npm run e2e
*
* Requires Node 18+ (global fetch). Reads no .env and prints no credentials.
*/
const AGENT = process.env.AGENT_URL ?? 'http://localhost:3000';
const STAYS = process.env.STAYS_URL ?? 'http://localhost:3001';
const EATS = process.env.EATS_URL ?? 'http://localhost:3002';
const RIDES = process.env.RIDES_URL ?? 'http://localhost:3003';
const CITY = {
Tanger: [35.7595, -5.834],
Rabat: [34.0209, -6.8416],
Marrakech: [31.6295, -7.9811],
Agadir: [30.4278, -9.5981],
};
let passed = 0;
let failed = 0;
const fails = [];
function check(name, cond, detail = '') {
if (cond) {
passed++;
console.log(` β ${name}`);
} else {
failed++;
fails.push(name);
console.log(` β ${name}${detail ? ` β ${detail}` : ''}`);
}
}
/** POST a prompt to the agent and collect the parsed SSE events. */
async function runAgent(prompt, city) {
const [lat, lon] = CITY[city] ?? CITY.Rabat;
const res = await fetch(`${AGENT}/agent/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt, history: [], location: { lat, lon, city, country: 'Morocco' } }),
});
const text = await res.text();
const events = [];
let tokens = '';
for (const line of text.split('\n')) {
if (!line.startsWith('data:')) continue;
const p = line.slice(5).trim();
if (!p || p === '[DONE]') continue;
try {
const e = JSON.parse(p);
events.push(e);
if (e.type === 'token') tokens += e.text;
} catch {
/* ignore non-JSON frames */
}
}
return {
tokens: tokens.trim(),
checkout: events.find((e) => e.type === 'checkout'),
clarify: events.find((e) => e.type === 'clarify'),
};
}
const itemNames = (d) => (d?.items ?? []).map((i) => i.name.toLowerCase()).join(' | ');
async function main() {
console.log(`\nOMNIA Agent E2E β agent=${AGENT}\n`);
// ββ 0) CORS: 127.0.0.1 must be allowed ββββββββββββββββββββββββββββββββββ
console.log('[CORS] 127.0.0.1 origin allowed');
try {
const r = await fetch(`${AGENT}/health`, { headers: { Origin: 'http://127.0.0.1:5180' } });
const acao = r.headers.get('access-control-allow-origin');
check('agent /health reflects 127.0.0.1 origin', acao === 'http://127.0.0.1:5180', `ACAO=${acao}`);
const body = await r.json();
check('public /health hides key-pool detail', body.groqKeys === undefined, JSON.stringify(Object.keys(body)));
const kr = await fetch(`${AGENT}/health/keys`);
check('gated /health/keys reachable in dev', kr.ok, `status=${kr.status}`);
} catch (e) {
check('agent reachable', false, String(e));
return finish();
}
// ββ 1) Eats: pepperoni pizza + coke in Tanger βββββββββββββββββββββββββββ
console.log('\n[1] "Order me a pepperoni pizza and a coke in Tanger"');
{
const r = await runAgent('Order me a pepperoni pizza and a coke in Tanger', 'Tanger');
const d = r.checkout?.drafts?.[0];
check('returns an eats checkout', d?.marketplace === 'eats', r.tokens.slice(0, 80));
check('order includes a pizza', /pizza|margher|pepperoni/.test(itemNames(d)), itemNames(d));
check('order total > 0', (d?.total ?? 0) > 0);
// Coke isn't in the seed β it must be substituted with a drink or flagged, never silently dropped.
const cokeFlagged = /coke/i.test(r.tokens) && /(isn'?t on|left (it|them) off|instead|added)/i.test(r.tokens);
const hasDrink = /juice|tea|soda|water|cola|lemonade|smoothie/i.test(itemNames(d));
check('unavailable coke is substituted or flagged (not silently dropped)', cokeFlagged || hasDrink, r.tokens.slice(0, 160));
}
// ββ 2) Ride: from X to Y, ~12 km (the parseEndpoints fix) βββββββββββββββ
console.log('\n[2] "Book me an economy ride from Rabat Agdal station to Rabat airport, about 12 km"');
{
const r = await runAgent('Book me an economy ride from Rabat Agdal station to Rabat airport, about 12 km', 'Rabat');
const d = r.checkout?.drafts?.[0];
check('returns a rides checkout', d?.marketplace === 'rides');
check('pickup = "Rabat Agdal station"', d?.pickup === 'Rabat Agdal station', `pickup=${d?.pickup}`);
check('dropoff = "Rabat airport"', d?.dropoff === 'Rabat airport', `dropoff=${d?.dropoff}`);
check('distance = 12 km', d?.distanceKm === 12, `km=${d?.distanceKm}`);
check('offers alternative ride tiers', (d?.rideOptions?.length ?? 0) > 1);
}
// ββ 3) Stay: riad in Marrakech, 2 guests, 3 nights, under 900/night βββββ
console.log('\n[3] "Book a riad in Marrakech for 2 guests for 3 nights under 900 MAD per night"');
{
const r = await runAgent('Book a riad in Marrakech for 2 guests for 3 nights under 900 MAD per night', 'Marrakech');
const d = r.checkout?.drafts?.[0];
check('returns a stays checkout', d?.marketplace === 'stays');
check('nights = 3', d?.nights === 3, `nights=${d?.nights}`);
check('guests = 2', d?.guests === 2, `guests=${d?.guests}`);
check('per-night within 900', d ? d.total / (d.nights || 1) <= 900 : false, `total=${d?.total}`);
}
// ββ 3b) Misspelled city in the PROMPT must still resolve to Marrakech ββββ
console.log('\n[3b] "Book a riad in marakech for 2 guests for 3 nights" (misspelled; location=Tanger)');
{
const r = await runAgent('Book a riad in marakech for 2 guests for 3 nights', 'Tanger');
const d = r.checkout?.drafts?.[0];
check('misspelled prompt city β stays checkout', d?.marketplace === 'stays', r.tokens.slice(0, 80));
check('resolves to Marrakech inventory', /marrakech/i.test(d?.subtitle ?? ''), `subtitle=${d?.subtitle}`);
}
// ββ 3c) Misspelled city in the LOCATION (prompt names no city) ββββββββββ
console.log('\n[3c] "Book me a riad for 2 guests for 3 nights" (location city="marakech")');
{
const r = await runAgent('Book me a riad for 2 guests for 3 nights', 'marakech');
const d = r.checkout?.drafts?.[0];
check('misspelled location city β stays checkout (not empty)', d?.marketplace === 'stays', r.tokens.slice(0, 80));
}
// ββ 4) Trip with no party size β must ASK guests first ββββββββββββββββββ
console.log('\n[4] "Plan my full trip to Agadir" (expects a clarifying question)');
{
const r = await runAgent('Plan my full trip to Agadir', 'Agadir');
check('asks a clarifying question', Boolean(r.clarify), r.tokens.slice(0, 80));
check('question is about guests', /guest/i.test(r.clarify?.question ?? ''), r.clarify?.question);
check('does not pre-book', !r.checkout);
}
// ββ 5) Full trip with guests + nights + budget β receipt + itinerary ββββ
console.log('\n[5] "Plan my full trip to Agadir for 2 guests for 3 nights under 3000 MAD"');
{
const r = await runAgent('Plan my full trip to Agadir for 2 guests for 3 nights under 3000 MAD', 'Agadir');
const c = r.checkout;
check('returns a multi-action checkout', (c?.drafts?.length ?? 0) >= 2, `drafts=${c?.drafts?.length}`);
check('includes a trip plan', Boolean(c?.trip));
check('itinerary has days', (c?.trip?.days?.length ?? 0) >= 3, `days=${c?.trip?.days?.length}`);
check('budget cap recorded = 3000', c?.trip?.budget?.budget === 3000, `budget=${c?.trip?.budget?.budget}`);
check('budget breakdown present', c?.trip?.budget && ['stay', 'ride', 'food', 'total'].every((k) => typeof c.trip.budget[k] === 'number'));
}
// ββ 6) Unsupported city β polite refusal, no booking ββββββββββββββββββββ
console.log('\n[6] "Book me a hotel and dinner in Paris for 2 nights" (expects refusal)');
{
const r = await runAgent('Book me a hotel and dinner in Paris for 2 nights', 'Rabat');
check("refuses (doesn't operate in Paris)", /doesn'?t operate in Paris/i.test(r.tokens), r.tokens.slice(0, 100));
check('does not book', !r.checkout);
}
// ββ 7) Signed-out bookings must be rejected (401), never silent βββββββββ
console.log('\n[7] Signed-out Buy Now β marketplace APIs reject without a token (401)');
{
const post = (url, body) =>
fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then((r) => r.status);
check('Eats /orders β 401', (await post(`${EATS}/orders`, { vendorId: 'x', items: [] })) === 401);
check('Stays /bookings β 401', (await post(`${STAYS}/bookings`, { listingId: 'x', nights: 1 })) === 401);
check('Rides /trips β 401', (await post(`${RIDES}/trips`, { rideClassId: 'x' })) === 401);
}
// ββ 8) Signed-in Buy Now β needs a real Clerk session (manual) ββββββββββ
console.log('\n[8] Signed-in Buy Now');
console.log(' ~ SKIPPED: requires a real Clerk session token (sign in with Google in the UI, then tap Buy now).');
finish();
}
function finish() {
console.log(`\nββββββββββββββββββββββββββββ\n${passed} passed, ${failed} failed`);
if (fails.length) console.log(`Failed: ${fails.join('; ')}`);
process.exit(failed ? 1 : 0);
}
main().catch((e) => {
console.error('E2E runner crashed:', e);
process.exit(1);
});
|