aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness/search_connect_harness.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/harness/search_connect_harness.mjs')
-rw-r--r--packages/meshbay-hub/tests/harness/search_connect_harness.mjs134
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,
+}));