From 7e01b7e8c2ce2ef9fce8b6c053c3b7210b64f9bf Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Sat, 17 Oct 2026 18:51:35 +0200 Subject: fix(spa): search names the groups refused the local network Search gives up before ICE does, so the refusal is read from the connection then; the walk moves on to the group's other nodes, and the page lists the refused groups with a link to each. Co-Authored-By: Claude Opus 5.5 --- .../meshbay-hub/tests/harness/local_network_harness.mjs | 9 ++++++++- .../tests/harness/search_connect_harness.mjs | 15 +++++++++++++-- .../meshbay-hub/tests/harness/search_fanout_harness.mjs | 17 ++++++++++++++--- 3 files changed, 35 insertions(+), 6 deletions(-) (limited to 'packages/meshbay-hub/tests/harness') diff --git a/packages/meshbay-hub/tests/harness/local_network_harness.mjs b/packages/meshbay-hub/tests/harness/local_network_harness.mjs index 9afdf59e..3c86cf77 100644 --- a/packages/meshbay-hub/tests/harness/local_network_harness.mjs +++ b/packages/meshbay-hub/tests/harness/local_network_harness.mjs @@ -151,6 +151,13 @@ if (cfg.classify) { undefined, undefined, '', 'pk') .then(() => { outcome = 'connected'; }, (err) => { outcome = err.message; reason = err.reason || null; main.close(); }); + // What a caller that stops waiting early reads (connection-pool.js): at + // `probeAt` ms, while ICE is still checking, or after it connected. + let early = null; + if (cfg.probeAt) { + await run(cfg.probeAt); + early = await main.localNetworkRefused(); + } await run(60000); - process.stdout.write(JSON.stringify({ outcome, reason, statsCalls })); + process.stdout.write(JSON.stringify({ outcome, reason, statsCalls, early })); } diff --git a/packages/meshbay-hub/tests/harness/search_connect_harness.mjs b/packages/meshbay-hub/tests/harness/search_connect_harness.mjs index 60cea2ba..71c7e8c4 100644 --- a/packages/meshbay-hub/tests/harness/search_connect_harness.mjs +++ b/packages/meshbay-hub/tests/harness/search_connect_harness.mjs @@ -26,6 +26,11 @@ const { resolveAt = null, // How many nodes the hub lists for the group. nodes = 1, + // Which of them answer (resolveAt applies to these). null: all. + answering = null, + // Which of them this device refuses to reach on the local network: what the + // transport reads from its connection when search gives up on it. + lanRefused = [], // How far to run the clock before giving up on the harness itself. runUntil = 300000, } = cfg; @@ -65,13 +70,17 @@ async function run(until) { // ── The environment ────────────────────────────────────────────────────────── let closed = 0; +let made = 0; class FakeTransport { constructor() { this.onConnectProgress = null; this.onNeedToken = null; + this.index = made++; } + async localNetworkRefused() { return lanRefused.includes(this.index); } + connect() { const started = now; for (const at of progressAt) { @@ -84,7 +93,7 @@ class FakeTransport { }, at); } return new Promise((resolve) => { - if (resolveAt !== null) { + if (resolveAt !== null && (answering === null || answering.includes(this.index))) { setTimeout(() => resolve({ ok: true, at: now - started }), resolveAt); } }); @@ -127,7 +136,9 @@ const connectToGroup = make(hubFetch, ensureFreshToken, globalThis.window); 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) }; }); + .catch((e) => { + outcome = { result: 'failed', at: now, error: String(e.message), reason: e.reason || null }; + }); await run(runUntil); diff --git a/packages/meshbay-hub/tests/harness/search_fanout_harness.mjs b/packages/meshbay-hub/tests/harness/search_fanout_harness.mjs index f84d90bc..27ad8757 100644 --- a/packages/meshbay-hub/tests/harness/search_fanout_harness.mjs +++ b/packages/meshbay-hub/tests/harness/search_fanout_harness.mjs @@ -23,6 +23,8 @@ const { // Indexes (0-based, in the hub's listing order) of the groups whose node is // down. They take `deadMs`; every other group answers in `liveMs`. dead = [], + // Of those, the ones whose node this device refused on the local network. + lanRefused = [], liveMs = 200, deadMs = 10000, // Group indexes the browser remembers as silent from a previous sweep. @@ -87,8 +89,12 @@ const fetchGroupIndex = (_pool, groupId) => new Promise((resolve, reject) => { const n = Number(String(groupId).split('-')[1]); const isDead = dead.includes(n); setTimeout( - () => (isDead ? reject(new Error('Connection timeout')) - : resolve({ entries: [{ id: `e${n}` }], roots: {} })), + () => { + if (!isDead) return resolve({ entries: [{ id: `e${n}` }], roots: {} }); + const err = new Error('Connection timeout'); + if (lanRefused.includes(n)) err.reason = 'local_network_blocked'; + return reject(err); + }, isDead ? deadMs : liveMs, ); }); @@ -125,11 +131,15 @@ const fetchAllIndexes = make( const list = Array.from({ length: groups }, (_, i) => ({ id: `g-${i}`, name: `G${i}` })); let finishedAt = null; +let outcome = null; fetchAllIndexes( null, list, 'tok', 'alice', 'u1', () => {}, // progress counters only (results) => { shown.push({ at: now, groups: results.size }); }, -).then(() => { finishedAt = now; }); +).then((out) => { + finishedAt = now; + outcome = { unreachable: out.unreachable, lanBlocked: out.lanBlocked.map((g) => g.name) }; +}); await run(runUntil); @@ -140,6 +150,7 @@ console.log(JSON.stringify({ // Every render, so a test can see whether live groups waited on dead ones. renders: shown, finishedAt, + outcome, inFlight: Number(ceiling[1]), // What the browser will remember for next time. remembered: written === null ? null : JSON.parse(written), -- cgit v1.2.3