205 lines
8.9 KiB
JavaScript
205 lines
8.9 KiB
JavaScript
/**
|
|
* 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);
|
|
});
|