aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_cli_golden.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-24 17:47:10 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-24 17:58:08 +0200
commita408a25af7b91abf729ec5fac7e5506437a86049 (patch)
tree675d96591ecfe3bebac41872892aaf7602f8bf13 /packages/meshbay-node/tests/test_cli_golden.py
parentc2dabdb870ecd8fd8ea6392e44258de02e9ad467 (diff)
downloadmeshbay-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.py138
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"