/** * orchestrator/test-gridsearch-ipc.mjs * * E2E test: spawn the C++ worker, send a gridsearch request * matching `npm run gridsearch:enrich` defaults, collect IPC events, * and verify the full event sequence. * * Run: node orchestrator/test-gridsearch-ipc.mjs * Needs: npm run build-debug (or npm run build) */ import { spawnWorker } from './spawn.mjs'; import { resolve, dirname } from 'node:path'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import fs from 'node:fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const IS_WIN = process.platform === 'win32'; const EXE_NAME = IS_WIN ? 'polymech-cli.exe' : 'polymech-cli'; const EXE = resolve(__dirname, '..', 'dist', EXE_NAME); if (!fs.existsSync(EXE)) { console.error(`❌ No ${EXE_NAME} found in dist. Run npm run build first.`); process.exit(1); } console.log(`Binary: ${EXE}\n`); // Load the sample settings (same as gridsearch:enrich) const sampleConfig = JSON.parse( readFileSync(resolve(__dirname, '..', 'config', 'gridsearch-sample.json'), 'utf8') ); let passed = 0; let failed = 0; function assert(condition, label) { if (condition) { console.log(` ✅ ${label}`); passed++; } else { console.error(` ❌ ${label}`); failed++; } } // ── Event collector ───────────────────────────────────────────────────────── const EXPECTED_EVENTS = [ 'grid-ready', 'waypoint-start', 'area', 'location', 'enrich-start', 'node', 'nodePage', // 'node-error' — may or may not occur, depends on network ]; function createCollector() { const events = {}; for (const t of ['grid-ready', 'waypoint-start', 'area', 'location', 'enrich-start', 'node', 'node-error', 'nodePage']) { events[t] = []; } return { events, handler(msg) { const t = msg.type; if (events[t]) { events[t].push(msg); } else { events[t] = [msg]; } // Live progress indicator const d = msg.payload ?? {}; if (t === 'waypoint-start') { process.stdout.write(`\r 🔍 Searching waypoint ${(d.index ?? 0) + 1}/${d.total ?? '?'}...`); } else if (t === 'node') { process.stdout.write(`\r 📧 Enriched: ${d.title?.substring(0, 40) ?? ''} `); } else if (t === 'node-error') { process.stdout.write(`\r ⚠️ Error: ${d.node?.title?.substring(0, 40) ?? ''} `); } }, }; } // ── Main test ─────────────────────────────────────────────────────────────── async function run() { console.log('🧪 Gridsearch IPC E2E Test\n'); // ── 1. Spawn worker ─────────────────────────────────────────────────── console.log('1. Spawn worker in daemon mode'); const worker = spawnWorker(EXE, ['worker', '--daemon', '--user-uid', '3bb4cfbf-318b-44d3-a9d3-35680e738421']); const readyMsg = await worker.ready; assert(readyMsg.type === 'ready', 'Worker sends ready signal'); // ── 2. Register event collector ─────────────────────────────────────── const collector = createCollector(); worker.onEvent(collector.handler); // ── 3. Send gridsearch request (matching gridsearch:enrich) ──────────── console.log('2. Send gridsearch request (Aruba / recycling / --enrich)'); const t0 = Date.now(); // Very long timeout — enrichment can take minutes const result = await worker.request( { type: 'gridsearch', payload: { ...sampleConfig, enrich: true, }, }, 5 * 60 * 1000 // 5 min timeout ); const elapsed = ((Date.now() - t0) / 1000).toFixed(1); console.log(`\n\n ⏱️ Completed in ${elapsed}s\n`); // ── 4. Verify final result ──────────────────────────────────────────── console.log('3. Verify job_result'); assert(result.type === 'job_result', `Response type is "job_result" (got "${result.type}")`); const summary = result.payload ?? null; assert(summary !== null, 'job_result payload is present'); if (summary) { assert(typeof summary.totalMs === 'number', `totalMs is number (${summary.totalMs})`); assert(typeof summary.searchMs === 'number', `searchMs is number (${summary.searchMs})`); assert(typeof summary.enrichMs === 'number', `enrichMs is number (${summary.enrichMs})`); assert(typeof summary.freshApiCalls === 'number', `freshApiCalls is number (${summary.freshApiCalls})`); assert(typeof summary.waypointCount === 'number', `waypointCount is number (${summary.waypointCount})`); assert(summary.gridStats && typeof summary.gridStats.validCells === 'number', 'gridStats.validCells present'); assert(summary.searchStats && typeof summary.searchStats.totalResults === 'number', 'searchStats.totalResults present'); assert(typeof summary.enrichedOk === 'number', `enrichedOk is number (${summary.enrichedOk})`); assert(typeof summary.enrichedTotal === 'number', `enrichedTotal is number (${summary.enrichedTotal})`); } // ── 5. Verify event sequence ────────────────────────────────────────── console.log('4. Verify event stream'); const e = collector.events; assert(e['grid-ready'].length === 1, `Exactly 1 grid-ready event (got ${e['grid-ready'].length})`); assert(e['waypoint-start'].length > 0, `At least 1 waypoint-start event (got ${e['waypoint-start'].length})`); assert(e['area'].length > 0, `At least 1 area event (got ${e['area'].length})`); assert(e['waypoint-start'].length === e['area'].length, `waypoint-start count (${e['waypoint-start'].length}) === area count (${e['area'].length})`); assert(e['enrich-start'].length === 1, `Exactly 1 enrich-start event (got ${e['enrich-start'].length})`); const totalNodes = e['node'].length + e['node-error'].length; assert(totalNodes > 0, `At least 1 node event (got ${totalNodes}: ${e['node'].length} ok, ${e['node-error'].length} errors)`); // Validate grid-ready payload if (e['grid-ready'].length > 0) { const gr = e['grid-ready'][0].payload ?? {}; assert(Array.isArray(gr.areas), 'grid-ready.areas is array'); assert(typeof gr.total === 'number' && gr.total > 0, `grid-ready.total > 0 (${gr.total})`); } // Validate location events have required fields if (e['location'].length > 0) { const loc = e['location'][0].payload ?? {}; assert(loc.location && typeof loc.location.title === 'string', 'location event has location.title'); assert(loc.location && typeof loc.location.place_id === 'string', 'location event has location.place_id'); assert(typeof loc.areaName === 'string', 'location event has areaName'); } assert(e['location'].length > 0, `At least 1 location event (got ${e['location'].length})`); // Validate node payloads if (e['node'].length > 0) { const nd = e['node'][0].payload ?? {}; assert(typeof nd.placeId === 'string', 'node event has placeId'); assert(typeof nd.title === 'string', 'node event has title'); assert(Array.isArray(nd.emails), 'node event has emails array'); assert(typeof nd.status === 'string', 'node event has status'); } // ── 6. Print event summary ──────────────────────────────────────────── console.log('\n5. Event summary'); for (const [type, arr] of Object.entries(e)) { if (arr.length > 0) console.log(` ${type}: ${arr.length}`); } // ── 7. Shutdown ─────────────────────────────────────────────────────── console.log('\n6. Graceful shutdown'); const shutdownRes = await worker.shutdown(); assert(shutdownRes.type === 'shutdown_ack', 'Shutdown acknowledged'); await new Promise(r => setTimeout(r, 500)); assert(worker.process.exitCode === 0, `Worker exited with code 0 (got ${worker.process.exitCode})`); // ── Summary ─────────────────────────────────────────────────────────── console.log(`\n────────────────────────────────`); console.log(` Passed: ${passed} Failed: ${failed}`); console.log(`────────────────────────────────\n`); process.exit(failed > 0 ? 1 : 0); } run().catch((err) => { console.error('Test runner error:', err); process.exit(1); });