diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_cli_dispatch.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_cli_dispatch.py | 129 |
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") |