summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-24 17:42:10 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-24 17:58:08 +0200
commitef942b93d1e9bfc28bf607dfaf501cc7d9685e7b (patch)
tree644ffc1884d506036a279f7f19a2d302f45d9064 /packages/meshbay-node/tests
parentdf6fafa76b0eec167fa1fa18a40632c2e95181ff (diff)
downloadmeshbay-ef942b93d1e9bfc28bf607dfaf501cc7d9685e7b.tar.gz
test(node): read the daemon's source wherever it is split
Tests that read daemon.py now take their text from node_source (daemon.py, NodeDaemon's bases, the cli package once it exists). The CLI tests that sliced main() run the CLI instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/node_source.py59
-rw-r--r--packages/meshbay-node/tests/test_audit.py4
-rw-r--r--packages/meshbay-node/tests/test_cli_dispatch.py51
-rw-r--r--packages/meshbay-node/tests/test_root_paths_are_operator_only.py44
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py12
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py8
-rw-r--r--packages/meshbay-node/tests/test_stream_capacity_config.py6
-rw-r--r--packages/meshbay-node/tests/test_transport_wire_parity.py6
-rw-r--r--packages/meshbay-node/tests/test_upload_size_cap.py7
9 files changed, 138 insertions, 59 deletions
diff --git a/packages/meshbay-node/tests/node_source.py b/packages/meshbay-node/tests/node_source.py
index df63773..918cf05 100644
--- a/packages/meshbay-node/tests/node_source.py
+++ b/packages/meshbay-node/tests/node_source.py
@@ -9,6 +9,7 @@ file sets below are derived from the tree rather than listed, so that a module
added later is read without anybody having to remember it.
"""
+import ast
import inspect
import textwrap
from pathlib import Path
@@ -50,3 +51,61 @@ def session_method(name: str) -> str:
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
return textwrap.dedent(inspect.getsource(getattr(WebRTCPeerSession, name)))
+
+
+# The daemon is `daemon.py`, every class NodeDaemon is built from, and the CLI
+# once `main()` is split out into a package of its own.
+CLI_PACKAGE = SRC / "cli"
+
+
+def daemon_files() -> list[Path]:
+ """Every module of the daemon and its CLI, however they are split up."""
+ from meshbay_node.daemon import NodeDaemon
+
+ files = [SRC / "daemon.py"]
+ for klass in NodeDaemon.__mro__:
+ if klass.__module__.startswith("meshbay_node."):
+ where = Path(inspect.getsourcefile(klass)).resolve()
+ if where not in files:
+ files.append(where)
+ if CLI_PACKAGE.is_dir():
+ files += sorted(p for p in CLI_PACKAGE.rglob("*.py")
+ if "__pycache__" not in p.parts)
+ return files
+
+
+def daemon_source() -> str:
+ return "\n".join(p.read_text(encoding="utf-8") for p in daemon_files())
+
+
+def daemon_class_source() -> str:
+ """The body of NodeDaemon, including every class it is built from."""
+ from meshbay_node.daemon import NodeDaemon
+
+ return "\n".join(inspect.getsource(k) for k in NodeDaemon.__mro__
+ if k.__module__.startswith("meshbay_node."))
+
+
+def cli_source() -> str:
+ """`meshbay-node`'s entry point and whatever it hands the work to."""
+ from meshbay_node import daemon
+
+ files = []
+ if CLI_PACKAGE.is_dir():
+ files = sorted(p for p in CLI_PACKAGE.rglob("*.py")
+ if "__pycache__" not in p.parts)
+ return "\n".join([inspect.getsource(daemon.main)]
+ + [p.read_text(encoding="utf-8") for p in files])
+
+
+def daemon_call(name: str) -> str:
+ """The one call to `name(...)` in the daemon, found by name, not position."""
+ found = []
+ for path in daemon_files():
+ text = path.read_text(encoding="utf-8")
+ for node in ast.walk(ast.parse(text)):
+ if (isinstance(node, ast.Call)
+ and getattr(node.func, "id", getattr(node.func, "attr", None)) == name):
+ found.append(ast.get_source_segment(text, node))
+ assert len(found) == 1, f"{len(found)} calls to {name} in the daemon — re-read this test"
+ return found[0]
diff --git a/packages/meshbay-node/tests/test_audit.py b/packages/meshbay-node/tests/test_audit.py
index b5b98b5..1a685ff 100644
--- a/packages/meshbay-node/tests/test_audit.py
+++ b/packages/meshbay-node/tests/test_audit.py
@@ -1,13 +1,13 @@
"""Tests for the node audit store (legal compliance IP/action logging)."""
import asyncio
-import inspect
import time
import pytest
from meshbay_node.audit import AuditStore
from meshbay_node.config import Config, HubConfig, KeystoreConfig, NodeConfig
from meshbay_node.daemon import NodeDaemon
+from node_source import daemon_class_source
@pytest.fixture
@@ -132,5 +132,5 @@ async def test_daemon_purges_the_audit_log(audit, tmp_path):
def test_daemon_starts_the_purge():
- source = inspect.getsource(NodeDaemon)
+ source = daemon_class_source()
assert "create_task(self._purge_audit_log())" in source
diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py
index a0b2a64..d73dfcf 100644
--- a/packages/meshbay-node/tests/test_cli_dispatch.py
+++ b/packages/meshbay-node/tests/test_cli_dispatch.py
@@ -13,10 +13,12 @@ is tested in `test_ops.py` and `test_roster_pairing.py`. What this catches is a
branch nobody ever ran.
"""
+import re
import sys
import pytest
from meshbay_node import daemon as daemon_mod
+from node_source import cli_source
# Each verb, with the arguments that reach its branch. `--yes` where the command
# would otherwise stop for a confirmation nobody can type in a test.
@@ -151,18 +153,22 @@ def test_every_verb_reaches_its_branch(argv, stub_daemon, monkeypatch, capsys):
assert out.out or out.err, f"{' '.join(argv)} printed nothing at all"
-def test_the_verb_list_here_matches_the_parser():
+def test_the_verb_list_here_matches_the_parser(monkeypatch, capsys):
"""
A verb added to the parser and not to this file would go untested, which is
exactly how `reload` shipped broken.
- """
- import inspect
- source = inspect.getsource(daemon_mod.main)
- start = source.index('choices=[') + len('choices=[')
- end = source.index(']', start)
- declared = {c.strip().strip('"\'') for c in source[start:end].split(',')
- if c.strip()}
+ The verbs are read from what `--help` prints, not from the source, so this
+ holds wherever the parser is built.
+ """
+ monkeypatch.setattr(sys, "argv", ["meshbay-node", "--help"])
+ with pytest.raises(SystemExit):
+ daemon_mod.main()
+ usage = capsys.readouterr().out
+ groups = [g for g in re.findall(r"\{([\w,-]+)\}", usage)
+ if "status" in g.split(",")]
+ assert groups, "--help no longer lists the verbs as {a,b,…}"
+ declared = set(groups[0].split(","))
exercised = {argv[0] for argv in VERBS}
# `init` writes a config file and `calibrate-argon2` burns CPU for seconds;
@@ -262,7 +268,7 @@ def test_a_bare_invocation_with_no_config_yet_exits_cleanly(monkeypatch, tmp_pat
"a fresh, unprovisioned start must not create anything on disk")
-def test_a_removed_verb_says_what_replaced_it():
+def test_a_removed_verb_says_what_replaced_it(stub_daemon, monkeypatch, capsys):
"""
`member upload` used to set a group-wide switch that no longer exists. It
reached the usage line for the *other* member verbs — "usage: meshbay-node
@@ -272,20 +278,20 @@ def test_a_removed_verb_says_what_replaced_it():
Naming it costs three lines and is the difference between an operator
finding `root set --writable` and concluding the CLI is broken.
"""
- import inspect
- source = inspect.getsource(daemon_mod.main)
- start = source.index('if args.command == "member":')
- block = source[start:source.index('if args.command == "group":', start)]
+ monkeypatch.setattr(sys, "argv", ["meshbay-node", "member", "upload"])
+ with pytest.raises(SystemExit) as exc:
+ daemon_mod.main()
+ assert exc.value.code == 1
+ guidance = capsys.readouterr().out
- assert 'sub == "upload"' in block, (
+ assert "usage: meshbay-node member upload" not in guidance, (
"`member upload` falls through to the generic usage line")
- guidance = block[block.index('sub == "upload"'):]
- guidance = guidance[:guidance.index("sys.exit")]
assert "root set" in guidance and "--writable" in guidance, (
"the message does not name what replaced it")
-def test_there_is_no_way_to_create_a_group_in_the_old_shape():
+def test_there_is_no_way_to_create_a_group_in_the_old_shape(
+ stub_daemon, monkeypatch, capsys):
"""
`--upload-dir` is gone, and documenting it as deprecated was the wrong
answer — which is what it got at first.
@@ -301,9 +307,16 @@ def test_there_is_no_way_to_create_a_group_in_the_old_shape():
only legitimate use. Writing it does not.
"""
import inspect
- source = inspect.getsource(daemon_mod.main)
- assert "--upload-dir" not in source, (
+ assert "--upload-dir" not in cli_source(), (
"the CLI can still create a group in the pre-RO/RW shape")
+ # With the daemon stubbed: a parser that accepted the flag would otherwise
+ # go on and add the group on whatever node answers the loopback API.
+ monkeypatch.setattr(sys, "argv", ["meshbay-node", "group", "add", "g",
+ "--dir", "/tmp/a", "--upload-dir", "/tmp/b"])
+ with pytest.raises(SystemExit) as exc:
+ daemon_mod.main()
+ assert exc.value.code == 2, "the parser accepts --upload-dir"
+ assert "--upload-dir" in capsys.readouterr().err
from meshbay_node import ops
params = inspect.signature(ops.attach_group).parameters
diff --git a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
index 0e89afa..a18e9fa 100644
--- a/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
+++ b/packages/meshbay-node/tests/test_root_paths_are_operator_only.py
@@ -19,6 +19,7 @@ them in the member payload too.
import inspect
import re
+import sys
from pathlib import Path
from meshbay_node import daemon as daemon_mod
@@ -85,21 +86,16 @@ def test_the_loopback_api_asks_for_paths():
"list_groups uses the member form, so every path it reports is missing")
-def test_the_cli_only_reads_fields_the_payload_carries():
+def test_the_cli_only_reads_fields_the_payload_carries(monkeypatch, tmp_path, capsys):
"""
The gap this whole file exists for. The CLI reads a dict and the API
returns a dict; nothing between them says which keys are owed, so a name
that is simply absent prints as a placeholder and looks like a node
problem.
- """
- source = inspect.getsource(daemon_mod.main)
- start = source.index('if args.command == "root":')
- block = source[start:source.index('if args.command == "operator":', start)]
-
- read = set(re.findall(r"r\.get\(['\"](\w+)['\"]", block))
- read |= set(re.findall(r"r\[['\"](\w+)['\"]\]", block))
- assert read, "the root CLI no longer reads the payload this way"
+ `root list` is run against a payload that records every key it is asked
+ for, so this holds wherever the CLI's code lives.
+ """
class _Any:
path = Path("/tmp/x")
name = "x"
@@ -107,7 +103,31 @@ def test_the_cli_only_reads_fields_the_payload_carries():
writable = removable = ejected = False
available = True
- offered = set(RootSet(roots=[_Any()]).describe(with_paths=True)[0])
- assert read <= offered, (
+ offered = RootSet(roots=[_Any()]).describe(with_paths=True)[0]
+ read: set[str] = set()
+
+ class _Recording(dict):
+ def __getitem__(self, key):
+ read.add(key)
+ return super().__getitem__(key)
+
+ def get(self, key, default=None):
+ read.add(key)
+ return super().get(key, default)
+
+ gid = "g" * 32
+ monkeypatch.setattr(daemon_mod, "_daemon_api", lambda cfg, path, **kw: {
+ "groups": [{"id": gid, "roots": [_Recording(offered)]}]})
+ monkeypatch.setattr(daemon_mod, "_resolve_group", lambda cfg, g: gid)
+ conf = tmp_path / "node.toml"
+ conf.write_text('[hub]\nurl = "https://example.invalid"\n')
+ monkeypatch.setattr(daemon_mod, "DEFAULT_CONFIG_PATH", conf)
+ monkeypatch.setattr(sys, "argv", ["meshbay-node", "root", "list"])
+
+ daemon_mod.main()
+
+ assert read, "the root CLI no longer reads the payload this way"
+ assert "/tmp/x" in capsys.readouterr().out
+ assert read <= set(offered), (
f"the `root` CLI reads keys the loopback payload does not carry: "
- f"{sorted(read - offered)}")
+ f"{sorted(read - set(offered))}")
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 038f12c..ba16317 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -23,7 +23,7 @@ from meshbay_common.join import ROLE_MEMBER, ROLE_OPERATOR, join_transcript
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roster import Roster, hash_code, normalize_code
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
-from node_source import session_method, webrtc_source
+from node_source import daemon_source, session_method, webrtc_source
from conftest import one_root
@@ -528,9 +528,7 @@ def test_join_policy_is_carried_from_node_config():
The policy reaches the transport from node.toml. If it ever came from the hub
instead, a hub could declare any group open and be handed its key.
"""
- daemon_src = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "daemon.py").read_text(encoding="utf-8")
- assert '"join_policy": group_cfg.join_policy' in daemon_src
+ assert '"join_policy": group_cfg.join_policy' in daemon_source()
config_src = (Path(__file__).parent.parent
/ "src" / "meshbay_node" / "config.py").read_text(encoding="utf-8")
@@ -883,8 +881,7 @@ def test_daemon_does_not_auto_pin_keystore_key():
Authority now comes from the roster, or from an explicit node.toml value.
"""
- source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "daemon.py").read_text(encoding="utf-8")
+ source = daemon_source()
assert "Auto-pinning admin key" not in source
assert "_resolve_admin_pk" not in source, (
"the auto-pin resolver is back — node authority must be established "
@@ -896,7 +893,6 @@ def test_admin_authority_is_never_fetched_from_the_hub():
The fix M3 invites: ask the hub which key belongs to the operator. That would
hand a malicious hub the node — the same substitution as H3, one level deeper.
"""
- src = Path(__file__).parent.parent / "src" / "meshbay_node"
body = session_method("_verify_admin_sig")
assert "operator_pks" in body, "the roster is where authority comes from"
@@ -908,7 +904,7 @@ def test_admin_authority_is_never_fetched_from_the_hub():
f"_verify_admin_sig mentions {forbidden!r} — authority must come from "
"the local roster and nothing else")
- daemon = (src / "daemon.py").read_text(encoding="utf-8")
+ daemon = daemon_source()
assert "has_operator()" in daemon, "the daemon reads authority from the roster"
assert "admin_pk_ed25519" not in daemon, (
"the node.toml operator key is gone; it must not come back as a second "
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 5e8badf..8e90332 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -18,7 +18,7 @@ from meshbay_common.crypto import generate_gek
from meshbay_node.indexer.group_index import GroupIndex
from meshbay_node.roots import RootSet
from meshbay_node.transport.webrtc_server import WebRTCPeerSession
-from node_source import TRANSPORT, webrtc_files, webrtc_source
+from node_source import TRANSPORT, daemon_source, webrtc_files, webrtc_source
from conftest import one_root, opened_ack, sealed_upload
@@ -434,8 +434,7 @@ def test_chat_store_and_peers_are_per_group(tmp_path):
def test_daemon_sets_no_global_chat_store(tmp_path):
"""H1: the daemon must not hoist one group's chat store onto the transport."""
- source = (Path(__file__).parent.parent
- / "src" / "meshbay_node" / "daemon.py").read_text(encoding="utf-8")
+ source = daemon_source()
assert '_ctx["chat_store"]' not in source, (
"daemon must not assign a transport-wide chat_store — it leaks chat "
"across groups (H1)"
@@ -610,8 +609,7 @@ def test_swarm_registration_skips_private_groups():
mis-mounted route, so fixing the route without this filter would have turned a
dormant leak into a live one.
"""
- source = (Path(__file__).parent.parent / "src" / "meshbay_node"
- / "daemon.py").read_text(encoding="utf-8")
+ source = daemon_source()
assert 'visibility' in source and '_register_swarm' in source
# Both registration sites must gate on public visibility.
for marker in ['gctx.get("visibility") == "public"',
diff --git a/packages/meshbay-node/tests/test_stream_capacity_config.py b/packages/meshbay-node/tests/test_stream_capacity_config.py
index 5514712..a40cbc0 100644
--- a/packages/meshbay-node/tests/test_stream_capacity_config.py
+++ b/packages/meshbay-node/tests/test_stream_capacity_config.py
@@ -27,6 +27,7 @@ from meshbay_node.config import load_config
from meshbay_node.roots import RootSet
from meshbay_node.transport.webrtc.apps.streaming import MAX_CONCURRENT_TRANSCODES
from meshbay_node.transport.webrtc_server import WebRTCPeerSession, WebRTCTransport
+from node_source import daemon_call
def _cfg(tmp_path: Path, body: str):
@@ -133,10 +134,7 @@ def test_the_daemon_passes_it( ):
`self.cfg` parses and imports perfectly well; it raises AttributeError the
first time somebody plays a video, which is not where anyone would look.
"""
- daemon = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
- / "daemon.py").read_text(encoding="utf-8")
- i = daemon.index("WebRTCTransport(")
- call = daemon[i:daemon.index(")", daemon.index("denylist=denylist", i))]
+ call = daemon_call("WebRTCTransport")
assert "max_concurrent_streams=" in call, (
"the daemon builds the transport without the operator's setting, so "
"node.toml is read and then ignored")
diff --git a/packages/meshbay-node/tests/test_transport_wire_parity.py b/packages/meshbay-node/tests/test_transport_wire_parity.py
index 3250f7f..c7008c8 100644
--- a/packages/meshbay-node/tests/test_transport_wire_parity.py
+++ b/packages/meshbay-node/tests/test_transport_wire_parity.py
@@ -24,7 +24,7 @@ from meshbay_node.indexer import DirectoryIndexer
from meshbay_node.transport import quic_server
from meshbay_node.transport.webrtc import disk
from meshbay_node.transport.wire import index_sync_message
-from node_source import webrtc_source
+from node_source import daemon_source, webrtc_source
from conftest import one_root
@@ -88,9 +88,7 @@ def test_the_daemon_does_not_build_an_index_message_itself():
site for an index message, and the one that would have kept sending cleartext
while the other two were sealed.
"""
- from meshbay_node import daemon
-
- source = inspect.getsource(daemon)
+ source = daemon_source()
assert "index_delta_message" in source and "index_sync_message" in source
assert '"type": MNP.INDEX_DELTA' not in source, (
"the daemon builds index_delta by hand again")
diff --git a/packages/meshbay-node/tests/test_upload_size_cap.py b/packages/meshbay-node/tests/test_upload_size_cap.py
index 7675f32..d2deb03 100644
--- a/packages/meshbay-node/tests/test_upload_size_cap.py
+++ b/packages/meshbay-node/tests/test_upload_size_cap.py
@@ -21,7 +21,7 @@ from meshbay_node.config import load_config
from meshbay_node.roots import RootSet
from meshbay_node.transport.webrtc.upload_handlers import GB_BYTES, MAX_UPLOAD_BYTES
from meshbay_node.transport.webrtc_server import WebRTCPeerSession, WebRTCTransport
-from node_source import webrtc_source
+from node_source import daemon_call, webrtc_source
def _cfg(tmp_path: Path, body: str):
@@ -138,10 +138,7 @@ def test_zero_is_refused_at_the_transport_too():
def test_the_daemon_passes_it():
"""The join that syntax checking cannot see."""
- daemon = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
- / "daemon.py").read_text(encoding="utf-8")
- i = daemon.index("WebRTCTransport(")
- call = daemon[i:daemon.index(")", daemon.index("denylist=denylist", i))]
+ call = daemon_call("WebRTCTransport")
assert "max_upload_gb=self._config.node.max_upload_gb" in call, (
"the daemon builds the transport without the operator's ceiling, so "
"node.toml is read and then ignored")