diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-24 17:47:10 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-24 17:58:08 +0200 |
| commit | a408a25af7b91abf729ec5fac7e5506437a86049 (patch) | |
| tree | 675d96591ecfe3bebac41872892aaf7602f8bf13 /packages/meshbay-node/tests/test_cli_golden.py | |
| parent | c2dabdb870ecd8fd8ea6392e44258de02e9ad467 (diff) | |
| download | meshbay-a408a25af7b91abf729ec5fac7e5506437a86049.tar.gz | |
test(node): record what every CLI verb prints
A golden master of `meshbay-node`: help text, every verb, the prompts
answered "no", each verb's usage fallback. Exit code, output, loopback
calls with their bodies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests/test_cli_golden.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_cli_golden.py | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_cli_golden.py b/packages/meshbay-node/tests/test_cli_golden.py new file mode 100644 index 0000000..ddaa82f --- /dev/null +++ b/packages/meshbay-node/tests/test_cli_golden.py @@ -0,0 +1,138 @@ +""" +What `meshbay-node` prints and asks for, verb by verb, recorded once and held. + +A characterisation test, like test_dispatch_golden.py: it states no rule of its +own, it pins the CLI as it is, so that `main()` can be taken apart underneath +it. Every verb test_cli_dispatch.py walks, the ones that ask for confirmation +without --yes, each verb with a subcommand it does not have, `--help` and a +verb that does not exist, is run with the daemon stubbed out, and what is +recorded is the exit code, stdout, stderr, each loopback call with its body, +each question asked and each `systemctl` run. The help text is in there whole: +the user guide quotes it. + +The recording is `golden/cli.json`. When a change to this behaviour is +intended, regenerate it with MESHBAY_GOLDEN_WRITE=1 and read the diff. +""" + +import builtins +import getpass +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest +from meshbay_node import config as config_mod +from meshbay_node import daemon as daemon_mod +from test_cli_dispatch import VERBS + +GOLDEN = Path(__file__).parent / "golden" / "cli.json" +# The verbs that ask first, asked and answered "no": test_cli_dispatch.py +# passes --yes to all of them. +UNCONFIRMED = [ + ["root", "remove", "media"], + ["gek", "rotate"], + ["file", "rm", "abc"], + ["video", "rematch"], + ["chat", "encrypt-history"], + ["denylist", "clear"], +] +# Each verb's last resort, its own usage line. +UNKNOWN_SUB = [[verb, "no-such-sub"] for verb in ( + "gek", "operator", "member", "group", "root", "file", "video", "chat", + "denylist", "stun", "transfers", "autostart", "service")] +CASES = [["--help"], ["no-such-verb"], *VERBS, *UNCONFIRMED, *UNKNOWN_SUB] + + +def _run(argv: list[str], tmp_path: Path, capsys) -> dict: + api: list = [] + asked: list = [] + systemctl: list = [] + + def fake_api(cfg, path, method="GET", timeout=30, body=None): + api.append([method, path, body]) + return { + "groups": [], "files": [], "identities": [], "members": [], + "invites": [], "users": [], "jtis": [], "count": 0, "removed": 0, + "subject": "all", "code": "TEST-CODE", "expires_at": "", + "link": "https://example.invalid/#/invite?v=1", "invite_id": "ab" * 16, + "user_id": "u", "authorized_members": 0, "errors": [], + "name": "g", "group_id": "g", "shared_dir": str(tmp_path), + "config": str(tmp_path / "node.toml"), + "enabled": False, "epoch": 1, "converted": 0, + "backup": str(tmp_path / "chat.db.bak"), + "encrypted": False, "plaintext_messages": 0, + "encrypted_messages": 0, "max_age_days": 30, + } + + def fake_input(prompt=""): + asked.append(prompt) + return "n" + + def fake_run(args, **kw): + systemctl.append(args) + return subprocess.CompletedProcess(args, 0, stdout="", stderr="") + + conf = tmp_path / "node.toml" + conf.write_text('[hub]\nurl = "https://example.invalid"\n') + code = 0 + with pytest.MonkeyPatch.context() as mp: + mp.setattr(daemon_mod, "_daemon_api", fake_api) + mp.setattr(daemon_mod, "_resolve_group", lambda cfg, g: "g" * 32) + mp.setattr(daemon_mod, "DEFAULT_CONFIG_PATH", conf) + # The data directory and the keystore's default path come from these; + # left real, `status` would record the developer's own node. + mp.setenv("HOME", str(tmp_path / "home")) + mp.setattr(config_mod, "DEFAULT_CONFIG_PATH", conf) + mp.setattr(builtins, "input", fake_input) + mp.setattr(getpass, "getpass", lambda *a, **kw: "test-password") + mp.setattr(subprocess, "run", fake_run) + # argparse wraps the help to the terminal's width. + mp.setenv("COLUMNS", "100") + mp.setattr(sys, "argv", ["meshbay-node", *argv]) + try: + daemon_mod.main() + except SystemExit as e: + code = e.code + out = capsys.readouterr() + + def mask(text: str) -> str: + return text.replace(str(tmp_path), "<tmp>") + + return json.loads(mask(json.dumps({ + "exit": code, "stdout": out.out, "stderr": out.err, + "api": api, "asked": asked, "systemctl": systemctl}))) + + +def _record(tmp_path: Path, capsys) -> dict: + return {" ".join(argv): _run(argv, tmp_path, capsys) for argv in CASES} + + +def test_every_verb_prints_as_recorded(tmp_path, capsys): + if os.environ.get("MESHBAY_GOLDEN_WRITE") == "1": + GOLDEN.write_text(json.dumps(_record(tmp_path, capsys), indent=1, + sort_keys=True, ensure_ascii=False) + "\n", + encoding="utf-8") + golden = json.loads(GOLDEN.read_text(encoding="utf-8")) + now = _record(tmp_path, capsys) + + assert sorted(now) == sorted(golden), ( + "the set of verbs changed; regenerate on purpose if it should") + differ = [k for k in golden if now[k] != golden[k]] + detail = "\n".join( + f" {k}\n" + "\n".join(f" {f}: was {golden[k][f]!r}\n {f}: now {now[k][f]!r}" + for f in golden[k] if golden[k][f] != now[k][f]) + for k in differ[:5]) + assert not differ, f"{len(differ)} verb(s) behave differently:\n{detail}" + + +def test_the_recording_exercises_the_cli(): + """A recording of nothing would hold nothing.""" + golden = json.loads(GOLDEN.read_text(encoding="utf-8")) + assert "usage: meshbay-node" in golden["--help"]["stdout"] + assert golden["no-such-verb"]["exit"] == 2 + assert sum(1 for c in golden.values() if c["api"]) > 30, "the daemon was never asked" + assert sum(1 for c in golden.values() if c["exit"] not in (0, None)) > 5, ( + "no usage line was ever reached") + assert any(c["asked"] for c in golden.values()), "no confirmation was ever asked" |