aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_boot_guard.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-17 03:40:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-17 03:40:52 +0200
commitf4fd6db8faa15bf02f38a14b652cc46936f8d6bf (patch)
tree396cbe7fe188fb565231be937ccfa6419384814f /packages/meshbay-hub/tests/test_boot_guard.py
parent929bfcccf8928a30d86d48a8b0137db0dd4a1ee0 (diff)
downloadmeshbay-f4fd6db8faa15bf02f38a14b652cc46936f8d6bf.tar.gz
spa: a blank page can never be silent again
boot-guard.js is a classic script loaded before the module graph, so it survives the graph failing to link. If #app is still empty after ten seconds it names what failed and offers a reset of this origin — cache, storage, databases and the service worker, which clearing the cache does not touch. Two real defects found building it: openDB never settled when an upgrade was blocked by another tab, and a connection it gave up on stayed open and squatted the database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_boot_guard.py')
-rw-r--r--packages/meshbay-hub/tests/test_boot_guard.py122
1 files changed, 122 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_boot_guard.py b/packages/meshbay-hub/tests/test_boot_guard.py
new file mode 100644
index 0000000..d802a03
--- /dev/null
+++ b/packages/meshbay-hub/tests/test_boot_guard.py
@@ -0,0 +1,122 @@
+"""
+The page cannot end up silently blank, and there is a way out of it.
+
+A reader spent an evening on a white screen. The shell arrived on every reload,
+every module was served from the browser's own store so not one request reached
+the hub, and nothing rendered: no message, no error, nothing in the server log
+to look at. The same account worked in a private window and in another browser
+— the signature of something wrong in this origin's stored state rather than in
+what was deployed. Clearing the site's data fixed it; clearing the *cache*,
+three times, had not, because a service worker and IndexedDB are not the cache.
+
+What is measured here is the silence, not its cause — that evidence was
+destroyed by the fix, necessarily. A blank page is a bug report nobody can
+write, and on a phone there is no console to open.
+
+`boot-guard.js` is a classic script, loaded before the module graph, because the
+failure it guards against includes the graph never linking: one bad module and
+no module code runs at all, so a guard inside `app.js` would be part of what
+failed.
+"""
+
+import json
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+import pytest
+
+HARNESS = Path(__file__).parent / "harness" / "boot_guard_probe.py"
+
+
+@pytest.fixture(scope="module")
+def cases():
+ if shutil.which("google-chrome") is None:
+ pytest.skip("Chrome is not available")
+ proc = subprocess.run([sys.executable, str(HARNESS)],
+ capture_output=True, text=True, timeout=300)
+ data = json.loads(proc.stdout)
+ assert "error" not in data, f"probe failed: {proc.stdout}{proc.stderr}"
+ return {c["case"]: c for c in data["cases"]}
+
+
+def test_the_guard_says_nothing_when_the_application_mounts(cases):
+ # The ordinary case, and the one that matters most: a guard that draws over
+ # a working application would be a worse bug than the one it is for.
+ c = cases["the application mounted"]
+ assert c["buttons"] == [], f"the guard drew over a mounted application: {c}"
+ assert "application" in c["text"]
+
+
+def test_a_graph_that_does_not_link_shows_a_message_not_a_blank_page(cases):
+ c = cases["the module graph did not link"]
+ assert len(c["text"]) > 40, f"still effectively blank: {c['text']!r}"
+
+
+def test_the_failure_is_named_on_screen(cases):
+ # Verbatim, on screen, because a phone has no console and this is the first
+ # thing anybody diagnosing it will ask for.
+ c = cases["the module graph did not link"]
+ assert "__case.js" in c["text"], (
+ f"the module that failed is not named anywhere: {c['text']!r}")
+
+
+def test_a_way_out_is_offered(cases):
+ c = cases["the module graph did not link"]
+ assert len(c["buttons"]) == 2, c["buttons"]
+ joined = " ".join(c["buttons"]).lower()
+ assert "essayer" in joined or "try" in joined
+ assert "initialis" in joined or "reset" in joined
+
+
+def test_the_reset_button_really_empties_this_origin(cases):
+ """A button that claims to clear and does not would be worse than none.
+
+ So it is clicked, for real, against a seeded `localStorage` and a seeded
+ database — and what it leaves behind is what is read back.
+ """
+ c = cases["the reset button empties this origin"]
+ assert c["clicked"], "no reset button to click"
+ assert c["after"]["auth"] is None, (
+ f"localStorage survived the reset: {c['after']}")
+ assert c["after"]["dbs"] == [], (
+ f"a database survived the reset: {c['after']}")
+
+
+def test_a_reset_another_tab_is_blocking_says_so(cases):
+ """Deleting a database waits for every other connection to close — silently.
+
+ Unwatched, `deleteDatabase` neither fails nor completes, so a reset that
+ reloads regardless comes back to exactly the state it claimed to clear. The
+ reader would then have tried the one thing that works, watched it appear to
+ work, and still be staring at the same page.
+ """
+ c = cases["a reset another tab is blocking says so"]
+ assert c["clicked"], "no reset button to click"
+ assert c["told"] == "meshbay", (
+ f"the blocked database was not named on screen: {c}")
+ assert not c["reloadedAnyway"], "it reloaded into the state it had not cleared"
+ assert c["stillOffersReset"], "the reader is left with no way to try again"
+
+
+def test_a_blocked_database_upgrade_gives_up_instead_of_hanging(cases):
+ """`indexedDB.open` fires neither `success` nor `error` when an upgrade is
+ blocked by another connection — it fires `blocked`, and unhandled that
+ leaves the promise unsettled for ever.
+
+ Version 2 arrived with the playlists, so every browser that had used this
+ site before it has a version 1 to upgrade, and a second tab holding one open
+ is all it takes.
+ """
+ c = cases["a blocked database upgrade gives up instead of hanging"]
+ assert c["result"] != "never settled", c
+ assert c["result"]["held"], "the fixture did not hold a connection open"
+ assert c["result"]["outcome"].startswith("rejected:"), c["result"]
+ # `blocked` fires the moment the upgrade is attempted, so this is immediate.
+ # The deadline behind it is a backstop for the case where even `blocked`
+ # never arrives — tolerating five seconds here would let the backstop stand
+ # in for the handler and the handler could be deleted unnoticed.
+ assert "blocked" in c["result"]["outcome"], (
+ f"gave up on a timeout rather than on the blocked event: {c['result']}")
+ assert c["result"]["ms"] < 1000, f"it took {c['result']['ms']}ms to give up"