summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_packaging_units.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-18 09:42:34 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-18 09:42:34 +0200
commit30e855f55f1d920b25da0bdd8e538c249d3c0c26 (patch)
tree587dcad0beb8a120352613be8aa751a97040015c /packages/meshbay-node/tests/test_packaging_units.py
parent768e07046368819b8a8f15c8b21e5a8bbfcdf282 (diff)
downloadmeshbay-30e855f55f1d920b25da0bdd8e538c249d3c0c26.tar.gz
feat(client): the platform seam, and an Electron shell that has never been run
Stage D, and the honest half of it. D1 — the seam (done, and verified) ---------------------------------- `static/platform.js`. `HUB` becomes `platform.hubBase()` and the transport is built with the same base, so one address has one source. In a browser it returns '' and every path stays relative to the origin that served the page — the acceptance criterion for this split was "the browser SPA behaves identically", and it does. `platform.js` joins `_ASSETS`, or a change to it would not move the content hash and a cached browser would never ask for it. D2 — the shell (written, never launched) ----------------------------------------- **There is no npm on this machine. Electron was never installed and `packages/meshbay-client/` has not been run once.** That is stated here rather than discovered later. What is there: a main process serving the packaged interface over a privileged `app://` scheme (`secure` and `standard` are not cosmetic — without them the service worker refuses to register and streamed downloads break silently), a preload exposing an enumerated bridge that never passes a filesystem path, a window with `sandbox`, `contextIsolation` and no node integration, navigation away from the package refused, and a CSP where the hub is reachable over connect-src and is not a script source. The hub address arrives as a process argument because `platform.hubBase()` runs before anything can await. `test_desktop_shell.py` pins each of those by reading the source — the treatment `test_downloads.py` already gives the three browser save paths. It catches a property being removed and proves nothing about the application running. Two were checked by breaking them. The interface is *copied* into the package by `build/sync-ui.js` from the hub's static directory, and `ui/` is gitignored: a silent fork is the only real way to end up maintaining the interface twice. D3 — partial ------------ The bridge, and the part worth having now: safeStorage's backend is reported rather than assumed. On Linux it falls back to a fixed key when no keyring is running, silently — someone who believes the OS is holding their keys is told when it is not. The native key lifecycle belongs with D4 and needs a running application to mean anything. D8 — partial, and a real defect found -------------------------------------- `meshbay-node.spec` installed the SYSTEM template — the one carrying `User=%i` — into `%{_userunitdir}`. A user unit already runs as its owner and cannot carry `User=`; systemd refuses the file, so the packaged unit could never have started. Nothing noticed because nobody had built and installed the RPM. Two units now: the template to `%{_unitdir}`, and a new `meshbay-node-user.service` that a person enables themselves without a password — which is what lets the desktop client install a node without asking for one. It carries ExecReload, so `meshbay-node reload` does not have to stop a service somebody is streaming from, and documents the drop-in for a drive outside the home, RequiresMountsFor included. 798 tests pass; e2e.py still passes end to end. Nothing here was built or launched: no npm, no rpmbuild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_packaging_units.py')
-rw-r--r--packages/meshbay-node/tests/test_packaging_units.py108
1 files changed, 108 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_packaging_units.py b/packages/meshbay-node/tests/test_packaging_units.py
new file mode 100644
index 0000000..bbfb536
--- /dev/null
+++ b/packages/meshbay-node/tests/test_packaging_units.py
@@ -0,0 +1,108 @@
+"""
+The systemd units, and which directory each belongs in.
+
+`meshbay-node.spec` installed the SYSTEM template — the one carrying `User=%i` —
+into the *user* unit directory. A user unit already runs as its owner and cannot
+carry `User=`; systemd refuses the file, so the packaged unit could never have
+started. Nothing caught it because nothing had built and installed the RPM.
+
+These read the files rather than installing them: no rpmbuild here. Weak
+evidence, and enough for this defect, which is a file in the wrong place.
+"""
+
+from pathlib import Path
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[3]
+SYSTEMD = ROOT / "packaging" / "systemd"
+SPEC = ROOT / "packaging" / "rpm" / "meshbay-node.spec"
+
+pytestmark = pytest.mark.skipif(not SPEC.exists(), reason="packaging not present")
+
+
+def _system_unit() -> str:
+ return (SYSTEMD / "meshbay-node.service").read_text(encoding="utf-8")
+
+
+def _user_unit() -> str:
+ return (SYSTEMD / "meshbay-node-user.service").read_text(encoding="utf-8")
+
+
+def _directives(unit: str) -> list[str]:
+ """
+ The lines systemd acts on — comments dropped.
+
+ Searching the whole file finds the comment explaining why a directive is
+ absent, and calls that the directive. The same mistake as reading a CSP out
+ of the HTML comment above the meta tag.
+ """
+ return [line.strip() for line in unit.splitlines()
+ if line.strip() and not line.strip().startswith("#")]
+
+
+def test_the_system_unit_is_a_template_that_names_its_user():
+ directives = _directives(_system_unit())
+ assert any(d == "User=%i" for d in directives), (
+ "the system template must run as the instance name")
+ assert any("%h" in d for d in directives), "it reads the instance's own home"
+
+
+def test_the_user_unit_names_no_user():
+ """
+ It already runs as its owner. `User=` in a user unit is not ignored —
+ systemd refuses to load the file at all.
+ """
+ directives = _directives(_user_unit())
+ assert not any(d.startswith("User=") for d in directives)
+ assert not any(d.startswith("Group=") for d in directives)
+
+
+def test_each_unit_is_installed_where_it_can_run():
+ spec = SPEC.read_text(encoding="utf-8")
+ install = spec.split("%files")[0]
+
+ # The template goes to the system directory, instantiated per person.
+ assert "%{_unitdir}/meshbay-node@.service" in install
+ # The user unit goes to the user directory, enabled without a password.
+ assert "%{_userunitdir}/meshbay-node.service" in install
+
+ system_line = next(l for l in install.splitlines()
+ if "meshbay-node-user.service" in l)
+ idx = install.splitlines().index(system_line)
+ destination = install.splitlines()[idx + 1]
+ assert "_userunitdir" in destination, (
+ "the per-user unit is installed as a system unit")
+
+
+def test_both_units_are_listed_in_files():
+ files = SPEC.read_text(encoding="utf-8").split("%files")[1]
+ assert "%{_unitdir}/meshbay-node@.service" in files
+ assert "%{_userunitdir}/meshbay-node.service" in files
+
+
+def test_the_user_unit_can_be_reloaded_without_dropping_anyone():
+ """
+ `meshbay-node reload` sends SIGHUP so a group's directories can change
+ without restarting. Without ExecReload the desktop client's reload would
+ have to stop the service, which drops whoever is watching a film.
+ """
+ directives = _directives(_user_unit())
+ reload_line = next((d for d in directives if d.startswith("ExecReload=")), "")
+ assert reload_line, "no ExecReload"
+ assert "HUP" in reload_line
+
+
+def test_the_user_unit_documents_how_a_drive_outside_home_is_added():
+ """
+ ProtectSystem=strict hides it, and a volume mounted after the service
+ started is invisible inside the unit's mount namespace — so the drop-in
+ needs RequiresMountsFor as well as ReadWritePaths. Written down where
+ somebody debugging an empty directory will find it.
+ """
+ unit = _user_unit()
+ assert "ProtectSystem=strict" in _directives(unit)
+ # These two belong in the comment: they are what an operator has to write in
+ # a drop-in, not what this file declares.
+ assert "RequiresMountsFor" in unit
+ assert "meshbay-node.service.d" in unit