diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/group-settings.js | 51 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_settings_node_detect.py | 87 |
2 files changed, 125 insertions, 13 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js index 3ffa524..1a367be 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/group-settings.js @@ -14,6 +14,36 @@ import { inviteLinkHere, nodePkForLink } from './invite-link.js'; // ALLOWED_PREF_KEYS must list it, or every toggle snaps back. export const INVITE_EMAIL_PREF = 'invite_email'; +// How many times the settings page asks the node on this machine before it +// decides there is none. One ask is a 3 s `node:detect`, and a node busy +// indexing a large root can miss it: a single miss used to hide "Add a +// directory" until the page was reloaded. Found live, by a tester adding a +// sixth directory while the others were still indexing. +const NODE_CHECK_ATTEMPTS = 4; +const NODE_CHECK_DELAY_MS = 2000; + +/** + * What the node on this machine says about `groupId`: `{ group }`, with + * `group` null when that node does not host it, or null when no node answers. + * A miss is asked again; a machine with no node set up at all is not. + */ +async function readLocalNode(node, groupId, attempts = NODE_CHECK_ATTEMPTS, + delayMs = NODE_CHECK_DELAY_MS) { + for (let i = 0; i < attempts; i++) { + if (i) await new Promise(r => setTimeout(r, delayMs)); + try { + const detect = await node.detect(); + if (!detect.detected) { + if (!detect.configured) return null; + continue; + } + const data = await node.op('groups'); + return { group: (data.groups || []).find(g => g.id === groupId) || null }; + } catch { /* busy, or mid-reload: ask again */ } + } + return null; +} + // ── Shared Directories Table ──────────────────────────────────────────── /** @@ -438,19 +468,14 @@ function GroupSettingsPanel({ groupId, group, token, transportRef, gekRef, const loadNodeInfo = useCallback(async () => { if (!platform.node.available) return; - try { - const detect = await platform.node.detect(); - if (!detect.detected) { setNodeDetected(false); return; } - setNodeDetected(true); - const data = await platform.node.op('groups'); - const groups = data.groups || []; - const ng = groups.find(g => g.id === groupId); - setNodeHostsGroup(Boolean(ng)); - if (ng) { - setNodeRoots(ng.roots || []); - setNodeGroupName(ng.name || ''); - } - } catch { setNodeDetected(false); } + const found = await readLocalNode(platform.node, groupId); + if (!found) { setNodeDetected(false); return; } + setNodeDetected(true); + setNodeHostsGroup(Boolean(found.group)); + if (found.group) { + setNodeRoots(found.group.roots || []); + setNodeGroupName(found.group.name || ''); + } }, [groupId]); useEffect(() => { loadNodeInfo(); }, [loadNodeInfo]); diff --git a/packages/meshbay-hub/tests/test_settings_node_detect.py b/packages/meshbay-hub/tests/test_settings_node_detect.py new file mode 100644 index 0000000..96e3cc0 --- /dev/null +++ b/packages/meshbay-hub/tests/test_settings_node_detect.py @@ -0,0 +1,87 @@ +""" +The settings page asks the node on this machine again before deciding there is +none. + +`readLocalNode` (group-settings.js) is lifted out and run under node against a +stub of `platform.node`. One missed 3 s check used to set "no node here", which +hides "Add a directory", until the page was reloaded: found by a tester adding a +directory while the node was busy indexing the others. +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node is not available") + + +def _run(tmp_path, stub): + src = (STATIC / "group-settings.js").read_text(encoding="utf-8") + fn = src[src.index("async function readLocalNode"):] + fn = fn[:fn.index("\n}\n") + 2] + script = tmp_path / "detect.mjs" + script.write_text(fn + stub + """ +const out = { result: await readLocalNode(node, 'g1', 4, 1), detects: node.detects }; +console.log(JSON.stringify(out)); +""", encoding="utf-8") + proc = subprocess.run(["node", str(script)], capture_output=True, text=True, + encoding="utf-8") + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_a_node_that_misses_a_check_is_asked_again(tmp_path): + out = _run(tmp_path, """ +const node = { + detects: 0, + async detect() { + this.detects++; + if (this.detects === 1) return { detected: false, configured: true }; + return { detected: true, configured: true }; + }, + async op() { + if (this.detects === 2) throw new Error('mid-reload'); + return { groups: [{ id: 'g1', name: 'G', roots: [{ name: 'a' }] }] }; + }, +}; +""") + assert out["result"] == {"group": {"id": "g1", "name": "G", "roots": [{"name": "a"}]}} + assert out["detects"] == 3 + + +def test_a_machine_with_no_node_set_up_is_not_asked_again(tmp_path): + out = _run(tmp_path, """ +const node = { + detects: 0, + async detect() { this.detects++; return { detected: false, configured: false }; }, + async op() { throw new Error('unreachable'); }, +}; +""") + assert out == {"result": None, "detects": 1} + + +def test_a_node_that_never_answers_is_given_up_on(tmp_path): + out = _run(tmp_path, """ +const node = { + detects: 0, + async detect() { this.detects++; return { detected: false, configured: true }; }, + async op() { throw new Error('unreachable'); }, +}; +""") + assert out == {"result": None, "detects": 4} + + +def test_a_node_that_does_not_host_the_group_still_counts_as_a_node(tmp_path): + out = _run(tmp_path, """ +const node = { + detects: 0, + async detect() { this.detects++; return { detected: true, configured: true }; }, + async op() { return { groups: [{ id: 'other' }] }; }, +}; +""") + assert out == {"result": {"group": None}, "detects": 1} |