diff options
Diffstat (limited to 'packages/meshbay-hub/tests/harness/search_connect_harness.mjs')
| -rw-r--r-- | packages/meshbay-hub/tests/harness/search_connect_harness.mjs | 134 |
1 files changed, 134 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/harness/search_connect_harness.mjs b/packages/meshbay-hub/tests/harness/search_connect_harness.mjs new file mode 100644 index 0000000..2479d46 --- /dev/null +++ b/packages/meshbay-hub/tests/harness/search_connect_harness.mjs @@ -0,0 +1,134 @@ +// 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 <path to search-page.js> <json config> +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, +})); |