// Run the SHIPPED `connectToGroup` against a fake transport and a fake clock. // // Nothing here is a paraphrase of search-page.js: the function and the two // constants that govern its deadline are lifted out of the file as text and // executed. What is modelled is the *environment* — a clock the test advances, // a hub that lists nodes, and a transport that reports progress and resolves // whenever the scenario says it does. // // A real clock would make these scenarios a minute each and would hide the // thing worth asserting, which is the moment the deadline fires rather than // roughly how long it took. // // Usage: node search_connect_harness.mjs import { readFileSync } from 'fs'; const src = readFileSync(process.argv[2], 'utf8'); const cfg = JSON.parse(process.argv[3] || '{}'); const { // When the transport reports progress, in ms from the start of the attempt. progressAt = [], // When connect() resolves. null: never. resolveAt = null, // How many nodes the hub lists for the group. nodes = 1, // How far to run the clock before giving up on the harness itself. runUntil = 300000, } = cfg; // ── The clock ──────────────────────────────────────────────────────────────── let now = 0; let nextId = 1; let timers = []; globalThis.setTimeout = (fn, ms) => { const t = { at: now + (ms || 0), fn, id: nextId++ }; timers.push(t); return t.id; }; globalThis.clearTimeout = (id) => { timers = timers.filter((t) => t.id !== id); }; // Promise jobs queued by the code under test have to run between one timer and // the next, or a `.then` that schedules a timer would be seen after the clock // has already passed it. const flush = async () => { for (let i = 0; i < 50; i++) await Promise.resolve(); }; async function run(until) { await flush(); while (timers.length) { const due = timers.reduce((a, b) => (b.at < a.at ? b : a)); if (due.at > until) break; timers = timers.filter((t) => t !== due); now = due.at; due.fn(); await flush(); } now = until; } // ── The environment ────────────────────────────────────────────────────────── let closed = 0; class FakeTransport { constructor() { this.onConnectProgress = null; this.onNeedToken = null; } connect() { const started = now; for (const at of progressAt) { setTimeout(() => { // Exactly what transport.js does: it never lets the caller's callback // break the connection it is reporting on. if (this.onConnectProgress) { try { this.onConnectProgress('test'); } catch { /* caller's problem */ } } }, at); } return new Promise((resolve) => { if (resolveAt !== null) { setTimeout(() => resolve({ ok: true, at: now - started }), resolveAt); } }); } close() { closed += 1; } } globalThis.window = { MeshBayTransport: FakeTransport }; const hubFetch = async () => ({ nodes: Array.from({ length: nodes }, (_, i) => ({ node_id: `node-${i}` })), }); const ensureFreshToken = async () => 'fresh-token'; // ── The code under test, as text ───────────────────────────────────────────── const constant = (name) => { const m = new RegExp(`^const ${name} = (\\d+);`, 'm').exec(src); if (!m) throw new Error(`${name} is gone from search-page.js`); return `const ${name} = ${m[1]};`; }; const fnStart = src.indexOf('async function connectToGroup('); if (fnStart < 0) throw new Error('connectToGroup is gone from search-page.js'); const fnEnd = src.indexOf('\n}\n', fnStart) + 3; const fnText = src.slice(fnStart, fnEnd); const make = new Function( 'hubFetch', 'ensureFreshToken', 'window', `${constant('SEARCH_STALL_MS')} ${constant('SEARCH_MAX_MS')} ${fnText} return connectToGroup;`, ); const connectToGroup = make(hubFetch, ensureFreshToken, globalThis.window); // ── The scenario ───────────────────────────────────────────────────────────── let outcome = null; connectToGroup('https://hub.example', 'g1', 'tok', null, 'alice', 'u1') .then(() => { outcome = { result: 'connected', at: now }; }) .catch((e) => { outcome = { result: 'failed', at: now, error: String(e.message) }; }); await run(runUntil); console.log(JSON.stringify({ ...(outcome || { result: 'still-waiting', at: now }), closed, }));