aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_cli_dispatch.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_cli_dispatch.py')
-rw-r--r--packages/meshbay-node/tests/test_cli_dispatch.py129
1 files changed, 129 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py
new file mode 100644
index 0000000..faca0ce
--- /dev/null
+++ b/packages/meshbay-node/tests/test_cli_dispatch.py
@@ -0,0 +1,129 @@
+"""
+Every CLI verb reaches its own code without falling over on a name.
+
+`meshbay-node reload` shipped with `subprocess` unimported and crashed with a
+NameError the first time it was typed. Python compiles that file happily —
+`node --check validates syntax, not names` is already in CLAUDE.md about the
+SPA, and it is the same class here: nothing in the module is wrong until the
+branch runs.
+
+So this walks every documented verb with the daemon stubbed out, and asserts
+that the branch executes. It is deliberately shallow — what each command *does*
+is tested in `test_ops.py` and `test_roster_pairing.py`. What this catches is a
+branch nobody ever ran.
+"""
+
+import sys
+from pathlib import Path
+
+import pytest
+
+from meshbay_node import daemon as daemon_mod
+
+# 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.
+VERBS = [
+ ["status"],
+ ["ui"],
+ ["group", "list"],
+ ["group", "add"], # missing --dir: usage, then exit
+ ["gek", "init"],
+ ["gek", "rotate", "--yes"],
+ ["gek-init"],
+ ["member", "list"],
+ ["member", "invite", "bob"],
+ ["member", "revoke", "bob"],
+ ["member", "unpin", "bob"],
+ ["operator", "pair"],
+ ["file", "list"],
+ ["file", "rm", "abc", "--yes"],
+ ["denylist", "show"],
+ ["denylist", "clear", "--yes"],
+ ["reload"],
+]
+
+
+@pytest.fixture
+def stub_daemon(monkeypatch, tmp_path):
+ """
+ Answer every loopback call with an empty-ish payload.
+
+ The point is to reach the branch, not to exercise the daemon: a command that
+ only crashes when the node is running is still a command that crashes.
+ """
+ calls: list[tuple] = []
+
+ def fake_api(cfg, path, method="GET", timeout=30, body=None):
+ calls.append((method, path))
+ return {
+ "groups": [], "files": [], "identities": [], "members": [],
+ "invites": [], "users": [], "jtis": [], "count": 0, "removed": 0,
+ "subject": "all", "code": "TEST-CODE", "expires_at": "",
+ "user_id": "u", "authorized_members": 0, "errors": [],
+ "name": "g", "group_id": "g", "shared_dir": str(tmp_path),
+ "config": str(tmp_path / "node.toml"),
+ }
+
+ monkeypatch.setattr(daemon_mod, "_daemon_api", fake_api)
+ monkeypatch.setattr(daemon_mod, "_resolve_group", lambda cfg, g: "g" * 32)
+
+ conf = tmp_path / "node.toml"
+ conf.write_text('[hub]\nurl = "https://example.invalid"\n')
+ monkeypatch.setattr(daemon_mod, "DEFAULT_CONFIG_PATH", conf)
+
+ # No interactive prompt left to hang on.
+ monkeypatch.setattr("builtins.input", lambda *a: "n")
+
+ # `reload` looks for a real daemon and signals it. Without this the test
+ # SIGHUPs whatever node happens to be running on the machine — which it did,
+ # once, before this was added. A test must not reach outside itself.
+ import os
+ import subprocess
+
+ signalled: list[int] = []
+ monkeypatch.setattr(
+ subprocess, "run",
+ lambda *a, **k: subprocess.CompletedProcess(a[0], 0, stdout="4242\n",
+ stderr=""))
+ monkeypatch.setattr(os, "kill", lambda pid, sig: signalled.append(pid))
+ calls.append(("_signalled", signalled))
+ return calls
+
+
+@pytest.mark.parametrize("argv", VERBS, ids=lambda a: "-".join(a))
+def test_every_verb_reaches_its_branch(argv, stub_daemon, monkeypatch, capsys):
+ monkeypatch.setattr(sys, "argv", ["meshbay-node", *argv])
+ try:
+ daemon_mod.main()
+ except SystemExit:
+ # A usage message and exit(1) is a branch that ran, which is what this
+ # asserts. A NameError or AttributeError is not.
+ pass
+ except (NameError, AttributeError) as e: # pragma: no cover
+ pytest.fail(f"{' '.join(argv)} crashed on a name: {e}")
+
+ out = capsys.readouterr()
+ assert out.out or out.err, f"{' '.join(argv)} printed nothing at all"
+
+
+def test_the_verb_list_here_matches_the_parser():
+ """
+ A verb added to the parser and not to this file would go untested, which is
+ exactly how `reload` shipped broken.
+ """
+ import argparse
+ 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()}
+
+ exercised = {argv[0] for argv in VERBS}
+ # `init` writes a config file and `calibrate-argon2` burns CPU for seconds;
+ # both are excluded on purpose rather than by omission.
+ untested = declared - exercised - {"init", "calibrate-argon2"}
+ assert not untested, (
+ f"CLI verbs with no dispatch test: {sorted(untested)} — add them to "
+ f"VERBS above")