aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_hub_address_seam.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-18 12:54:29 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-18 12:54:29 +0200
commit06062d2e8352a205fa634970d260ab0f5de97f04 (patch)
tree0b10ee28d0de9d9af5fea0febfb404658177e1d5 /packages/meshbay-hub/tests/test_hub_address_seam.py
parent7c46b4e7dc2893974a37d6a701123a95803fcb95 (diff)
downloadmeshbay-06062d2e8352a205fa634970d260ab0f5de97f04.tar.gz
fix(client): three defects a real desktop found in ten minutes
All three came from the operator running the application on Ubuntu GNOME. None would have been found by anything already in the suite. **A second copy of the hub address.** `keyderive.js` carried `const HUB = '' // same origin` — true of a page the hub served, false of one loaded from a package, where the origin is `app://meshbay` and `/v1/users/register` resolves against the application's own protocol handler. **Sign-up and sign-in, the first two things anybody does, failed with "Not found."** The seam was changed in `app.js` and in the signalling call and this was missed: the same shape as the duplicate `MNP_VERSION` in `protocol.py`, a second copy of a constant that is harmless until the context changes. `test_hub_address_seam.py` refuses any file that decides where the hub is, and any `fetch('/v1/…')` relative to the page origin. **A window handler reading a variable another path reassigns.** Changing the hub closes one window and opens another; `closed` arrives *after* the replacement is assigned, so the outgoing window nulled the reference to the incoming one and its `ready-to-show` crashed on it — a modal "A JavaScript error occurred in the main process". Every handler now belongs to the window it was created with. The CDP test wrote `config.json` in advance, so it never took the one path that creates a second window; it does now, starting from an empty user-data dir. **A first run that could not be undone.** The hub address was accepted on anything URL-shaped and there was no way to change it afterwards — the prompt only appears when none is set, so a typo meant editing JSON by hand. `https` typed at a hub speaking `http` produced `TypeError: fetch failed`, which names nothing. Now: the address is probed before being written, failures say which URL and why ("does not speak https. If this hub is on your own machine, it is probably http"), Settings can change it, and Electron's "Error invoking remote method" wrapper is stripped from what a person reads. Verified on the operator's desktop: **safeStorage really uses the GNOME keyring** — Settings reports `gnome-libsecret`, and `secrets.bin` is written 0600 with Chromium's `v11` prefix, the marker for keyring-backed encryption (the fixed-key fallback writes `v10`). Headless, the same code reports `unavailable` and refuses to store rather than downgrading in silence, which is now explained in Settings instead of shown as a bare word. Unrelated but found while testing: `test_locales.py` assigned to `globalThis.navigator`, which is read-only from Node 22. The client's build already requires Node 22+, so the first CI machine configured for it would have failed these tests for no visible reason. 809 tests pass on Node 18 and Node 24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_hub_address_seam.py')
-rw-r--r--packages/meshbay-hub/tests/test_hub_address_seam.py79
1 files changed, 79 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_hub_address_seam.py b/packages/meshbay-hub/tests/test_hub_address_seam.py
new file mode 100644
index 0000000..392a21c
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_hub_address_seam.py
@@ -0,0 +1,79 @@
+"""
+One address for the hub, resolved in one place.
+
+`keyderive.js` carried its own `const HUB = '' // same origin`. True of a page
+the hub served; false of one loaded from a package, where the origin is
+`app://meshbay` — so `/v1/users/register` resolved against that and the
+application's own protocol handler answered 404. **Registration and sign-in, the
+first two things anybody does, failed with "Not found".**
+
+It was found by a person clicking Register, not by anything here, and it is the
+same shape as the duplicate `MNP_VERSION` in `protocol.py`: a second copy of a
+constant, harmless until something changes underneath it.
+
+So: no file that talks to the hub may decide for itself where the hub is.
+"""
+
+import re
+from pathlib import Path
+
+import pytest
+
+STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
+
+# Files that issue hub requests. `app.js` gets its base from `platform.hubBase()`
+# and the rest reach the adapter through the global it publishes.
+CALLERS = ["app.js", "keyderive.js", "transport.js", "crypto.js"]
+
+
+def _source(name: str) -> str:
+ return (STATIC / name).read_text(encoding="utf-8")
+
+
+@pytest.mark.parametrize("name", CALLERS)
+def test_no_file_decides_where_the_hub_is(name):
+ """
+ A literal empty base means "same origin", which is an assumption about how
+ the page was loaded — and it is wrong in the application.
+ """
+ for line in _source(name).splitlines():
+ stripped = line.strip()
+ if stripped.startswith("*") or stripped.startswith("//"):
+ continue # prose about the fix, not the fix
+ assert not re.match(r"const HUB\s*=\s*['\"]{2}\s*;", stripped), (
+ f"{name} hard-codes the hub as the current origin")
+
+
+@pytest.mark.parametrize("name", CALLERS)
+def test_hub_paths_are_never_fetched_against_the_page_origin(name):
+ """
+ `fetch('/v1/...')` resolves against whatever served the page. In a browser
+ that is the hub; in the application it is the package, and the request never
+ leaves the machine.
+ """
+ source = _source(name)
+ bad = re.findall(r"""\bfetch\(\s*['"`]/v1/""", source)
+ assert not bad, (
+ f"{name} fetches a hub path relative to the page origin — "
+ f"{len(bad)} site(s)")
+
+
+def test_the_adapter_is_reachable_from_a_classic_script():
+ """
+ `keyderive.js` and `transport.js` load before the module graph and cannot
+ import. The adapter therefore publishes a global, and they read it when a
+ call is made rather than when they load — by which time it exists.
+ """
+ platform = _source("platform.js")
+ assert "window.MeshBayPlatform" in platform
+
+ for name in ("keyderive.js", "transport.js"):
+ source = _source(name)
+ assert "MeshBayPlatform" in source, (
+ f"{name} does not reach the adapter, so it has an answer of its own")
+
+
+def test_the_adapter_is_the_only_thing_that_answers_where():
+ """One implementation, so a second cannot drift from it."""
+ platform = _source("platform.js")
+ assert platform.count("export function hubBase()") == 1