""" A node that cannot serve the signed-in account says so, and the account can let go of a node from anywhere. Signing in on a desktop links this machine's node to the account (main.js `ensureNode`), but never over a key already linked -- that would cut off the user's other machine -- nor a node set up for another account. Found on a real install (2026-10-10): the node read "Running" while the hub refused it in a loop, and nothing said why. And the only way to unlink was the linked machine's own Node page, of no use once that machine is gone. """ import json import re import shutil import subprocess from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" NODE_PAGE = STATIC / "node-page.js" PROFILE_PAGE = STATIC / "profile-page.js" def _src(path: Path) -> str: return path.read_text(encoding="utf-8") @pytest.mark.skipif(shutil.which("node") is None, reason="node is not available") def test_the_problem_is_worked_out_from_the_node_and_the_hub(tmp_path): m = re.search(r"^export function nodeLinkProblem\(.*?^\}", _src(NODE_PAGE), re.M | re.S) assert m, "node-page.js no longer has nodeLinkProblem" mine = {"username": "alice", "hub_url": "https://hub.example/", "pk_node_ed25519": "K1"} cases = [ [mine, "K1", "alice", "https://hub.example"], # linked here: fine [mine, None, "alice", "https://hub.example"], # nothing linked: sign-in links it [mine, "K2", "alice", "https://hub.example"], # another node's key [{**mine, "username": "bob"}, "K1", "alice", "https://hub.example"], [mine, "K1", "alice", "https://other.example"], # set up for another hub [mine, "K2", "alice", ""], # browser: same-origin hub [None, "K2", "alice", "https://hub.example"], # node not answering ] script = tmp_path / "case.js" script.write_text( m.group(0).replace("export ", "") + f"\nconsole.log(JSON.stringify({json.dumps(cases)}" ".map(c => nodeLinkProblem(...c))));", encoding="utf-8") out = subprocess.run(["node", str(script)], capture_output=True, encoding="utf-8", check=True) assert json.loads(out.stdout) == [ None, None, "other_node", "other_account", "other_account", "other_node", None] def test_the_node_page_shows_the_banner_and_offers_the_link(): src = _src(NODE_PAGE) page = src.split("export function NodePage(", 1)[1] assert "<${NodeLinkBanner} info=${nodeInfo} token=${token} username=${username}" in page banner = src.split("function NodeLinkBanner(", 1)[1].split("\nfunction ", 1)[0] # Asked of the hub, not remembered from sign-in. assert "hubFetch(`/v1/users/${encodeURIComponent(username)}/pubkeys`" in banner assert "nodeLinkProblem(info, linkedKey, username, HUB)" in banner take_over = banner.split("const takeOver = async () => {", 1)[1].split("\n };", 1)[0] # Replacing another machine's link is asked first, then this node's key # replaces it directly: node:start left a node answering "running" alone, # and a node signed in before the account was linked elsewhere does # (found clicking it on a real install). For another account node:start # asks itself before switching, and takeOver gets past "running". assert "problem === 'other_node' && !await ask(t('node.link_here_confirm'))" in take_over assert "method: 'PUT', token, body: { pk_node_ed25519: nodeKey }" in take_over assert "platform.node.start({ hubUrl: HUB, username, token, takeOver: true })" in take_over for key in ("node.link_other_node", "node.link_other_account", "node.link_here", "node.link_take_over", "node.link_linking"): assert f"'{key}'" in banner, key def test_node_start_switches_a_running_node_when_asked_to_take_it_over(): main_js = (Path(__file__).resolve().parents[3] / "packages" / "meshbay-client" / "src" / "main.js").read_text(encoding="utf-8") start = main_js.split("handle('node:start', async (_e, opts) => {", 1)[1] start = start.split("\n });\n", 1)[0] assert "already.status === 'running' && !(opts && opts.takeOver)" in start def test_the_profile_page_can_unlink_the_node(): src = _src(PROFILE_PAGE) unlink = src.split("const unlinkNodeKey = useCallback(async () => {", 1)[1].split("}, [", 1)[0] assert "if (!await ask(t('settings.node_key_unlink_confirm'))) return;" in unlink assert "hubFetch('/v1/users/me/node_key', { method: 'DELETE', token: user.token })" in unlink assert "setCurrentNodeKey(null)" in unlink # Offered only where there is a key to let go of. shown = src.split("${currentNodeKey && html`", 1)[1].split("`}", 1)[0] assert "onClick=${unlinkNodeKey}" in shown