diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:15:16 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-12 16:36:54 +0200 |
| commit | c5fff4ce8366b08669c0c8b6d30b99b94b9fefca (patch) | |
| tree | a4ab1bf93c63cdf5eb96f1c7e98b2845f6382953 /packages/meshbay-hub/tests/test_packaging_hub_unit.py | |
| parent | 2114a54eb6335f97b0c276c4f1f224d45f46fd1a (diff) | |
| download | meshbay-c5fff4ce8366b08669c0c8b6d30b99b94b9fefca.tar.gz | |
fix(packaging): the hub unit can start, and nothing carries the migration path
`ExecStartPre` ran `alembic -c /opt/meshbay-hub/migrations/alembic.ini upgrade
head`. The build does stage that file, so the path existed and the contents
were wrong: `alembic.ini` resolves `script_location` with `%(here)s`, so the
copy pointed at `/opt/meshbay-hub/migrations/src/meshbay_hub/db/migrations` —
which nothing installs, because the migrations ship inside `meshbay_hub`, in
the shared venv.
`ExecStartPre` failing stops the unit. A hub installed from the RPM or the DEB
could not start at all, and nothing noticed because the one live deployment
was assembled by hand — the same shape as the node unit that carried `User=`
into the user unit directory.
The same `%(here)s` trap was already found once on the server, where a stray
`alembic.ini` resolved to a month-old snapshot of the tree. Twice is a trap
rather than an accident, so the fix is that the path is no longer written down
anywhere: `meshbay-hub migrate` asks the installed package where its own
migrations are, which is correct for the RPM, the DEB, a venv and a checkout.
The build stages no `alembic.ini`; the repo keeps its own for `alembic
revision` and for deploy scripts that already work.
`env.py` now prefers a URL the caller resolved over re-reading the environment
itself, so `migrate --config` connects with exactly the string the server
will — one resolution, not two that agree until they do not.
Six tests, three of which fail against the unit as it was. They read the
directives rather than the file, because searching the whole thing finds the
comment explaining a directive and calls that the directive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4YmK41VsEURWFdop4EEeT
Diffstat (limited to 'packages/meshbay-hub/tests/test_packaging_hub_unit.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_packaging_hub_unit.py | 101 |
1 files changed, 101 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_packaging_hub_unit.py b/packages/meshbay-hub/tests/test_packaging_hub_unit.py new file mode 100644 index 0000000..7d8f5d2 --- /dev/null +++ b/packages/meshbay-hub/tests/test_packaging_hub_unit.py @@ -0,0 +1,101 @@ +""" +The hub's systemd unit, and the migration step it runs before starting. + +`ExecStartPre` was `alembic -c /opt/meshbay-hub/migrations/alembic.ini upgrade +head`. The file was staged there, so the path existed — but `alembic.ini` +resolves `script_location` with `%(here)s`, so a copy of it is only correct +where it was copied *from*. Staged into `/opt/meshbay-hub/migrations/` it +pointed at `/opt/meshbay-hub/migrations/src/meshbay_hub/db/migrations`, which +nothing installs: the migrations ship inside `meshbay_hub`, in the shared venv. + +`ExecStartPre` failing stops the unit, so **a hub installed from the RPM or the +DEB could not start at all**, and nothing noticed because the reference +deployment was assembled by hand. Exactly the shape of the node unit defect +recorded in `test_packaging_units.py`: a packaged file nobody had installed. + +The same `%(here)s` trap had already been found once on the server, where a +stray `alembic.ini` resolved to a month-old snapshot of the tree +(`QE/server-state/meshbay.org.md`). Twice is a trap, not an accident: the fix +is that nothing carries the path any more. `meshbay-hub migrate` asks the +installed package where its own migrations are. +""" + +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[3] +UNIT = ROOT / "packaging" / "systemd" / "meshbay-hub.service" +BUILD = ROOT / "packaging" / "build" / "build-hub.sh" + +pytestmark = pytest.mark.skipif( + not UNIT.exists(), reason="packaging not present") + + +def _directives() -> 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; `test_packaging_units.py` records + that mistake being made. + """ + return [line.strip() for line in UNIT.read_text(encoding="utf-8").splitlines() + if line.strip() and not line.strip().startswith("#")] + + +def _exec_start_pre() -> list[str]: + return [d for d in _directives() if d.startswith("ExecStartPre=")] + + +def test_the_migration_step_runs_the_hubs_own_command(): + pre = _exec_start_pre() + assert len(pre) == 1, f"expected one ExecStartPre, got {pre}" + assert pre[0].endswith("meshbay-hub migrate --config /etc/meshbay/hub.toml"), pre[0] + + +def test_no_directive_names_an_alembic_config_by_path(): + """A path to `alembic.ini` in a unit is the defect itself: the file is only + correct where it was written, and a package moves it.""" + offenders = [d for d in _directives() if "alembic" in d] + assert not offenders, ( + "a unit that names alembic.ini carries a path that the packaging " + f"relocates: {offenders}") + + +def test_the_build_stages_no_alembic_config(): + """Staging a copy is what made the path exist and the contents wrong.""" + staged = [line.strip() for line in BUILD.read_text(encoding="utf-8").splitlines() + if "alembic.ini" in line and not line.strip().startswith("#")] + assert not staged, f"build-hub.sh still stages alembic.ini: {staged}" + + +def test_the_migrations_are_where_the_command_looks_for_them(): + """The other half. The unit is right only if the package carries them — + they are `.py` files inside `meshbay_hub`, so a wheel does, but nothing + said so and nothing would notice if that changed.""" + from meshbay_hub.daemon import migrations_dir + + scripts = migrations_dir() + assert (scripts / "env.py").is_file(), scripts + revisions = list((scripts / "versions").glob("*.py")) + assert revisions, f"no revisions under {scripts}" + + +def test_the_command_resolves_from_the_installed_package_not_the_checkout(): + """Derived from `meshbay_hub.__file__`, so it is correct in a venv, an RPM + and a checkout alike — which is the whole point of not writing it down.""" + import meshbay_hub + + from meshbay_hub.daemon import migrations_dir + + assert migrations_dir() == ( + Path(meshbay_hub.__file__).resolve().parent / "db" / "migrations") + + +def test_the_hub_runs_as_the_service_account_under_the_hardening(): + """Read once while here, because an ExecStartPre that cannot run is not the + only way a unit fails to start.""" + directives = _directives() + for required in ("User=meshbay", "Group=meshbay", + "NoNewPrivileges=true", "ProtectSystem=strict"): + assert required in directives, f"{required} is not in the unit" |