From 422647e7898b8df67cd11f8fb060aec23dec84aa Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 14 Oct 2026 22:20:15 +0200 Subject: feat(cli): meshbay-cli, a command-line client for members Signs in to the hub, lists the account's groups and their folders, and downloads and uploads files directly with each group's node, end-to-end encrypted. With no argument it opens a two-pane interactive mode (Textual). Session and device keys are kept encrypted under the system's key storage, or a 0600 key file where there is none; node keys are pinned at first contact; a new machine links as a device. Packaged on its own on Linux (deb and rpm, on meshbay-common's venv, with a man page generated from the parser) and frozen next to meshbay-node in the full Windows installer, not the light one, with a self-test the build runs. client/keys.py ports keyderive.js and is LGPL like it. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-cli/tests/test_manpage.py | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 packages/meshbay-cli/tests/test_manpage.py (limited to 'packages/meshbay-cli/tests/test_manpage.py') diff --git a/packages/meshbay-cli/tests/test_manpage.py b/packages/meshbay-cli/tests/test_manpage.py new file mode 100644 index 0000000..ebd71c8 --- /dev/null +++ b/packages/meshbay-cli/tests/test_manpage.py @@ -0,0 +1,70 @@ +""" +The man page is generated from the parser; these keep the generator honest. + +An option added to an action must reach the page, every exit code must be +explained, and troff must read the result without a warning. +""" + +import argparse +import os +import shutil +import subprocess + +import pytest +from meshbay_cli import __main__ as cli +from meshbay_cli import errors, manpage + + +def _section(page: str, start: str) -> str: + return page.split(start + "\n", 1)[1].split("\n.S", 1)[0] + + +def _entry(option: str) -> str: + """How an option opens its entry: `.TP`, then the option in bold.""" + return f"\\fB{manpage._roff(option)}\\fR" + + +def test_every_action_and_option_is_in_its_section(): + page = manpage.render() + parser = cli.build_parser() + glob = _section(page, ".SH GLOBAL OPTIONS") + for a in parser._actions: + for option in a.option_strings: + assert _entry(option) in glob, f"global {option} missing" + sub = next(a for a in parser._actions if isinstance(a, argparse._SubParsersAction)) + assert list(sub.choices) == list(cli.ACTIONS) + for name, sp in sub.choices.items(): + own = _section(page, f".SS {name}") + for a in sp._actions: + if isinstance(a, argparse._HelpAction): + continue + if a.option_strings and _entry(a.option_strings[0]) in glob: + continue # a global one, listed once + if a.option_strings: + for option in a.option_strings: + assert _entry(option) in own, f"{name} {option} missing" + elif a.choices: + for choice in a.choices: + assert _entry(choice) in own, f"{name} {choice} missing" + else: + meta = a.metavar or a.dest.upper() + assert f"\\fI{manpage._roff(meta)}\\fR" in own, f"{name} {meta} missing" + + +def test_every_exit_code_is_explained(): + codes = {v for k, v in vars(errors).items() if k.isupper() and isinstance(v, int)} + assert codes | {0} == set(manpage.EXIT_CODES) + + +def test_names_and_text_cannot_break_the_page(): + assert manpage._roff(".hidden") == "\\&.hidden" + assert manpage._roff("a\\b — c-d") == "a\\eb \\[u2014] c\\-d" + + +@pytest.mark.skipif(not shutil.which("groff"), reason="groff not installed") +def test_troff_reads_it_without_a_warning(tmp_path): + page = tmp_path / "meshbay-cli.1" + page.write_text(manpage.render(), encoding="ascii") # nothing past ASCII left + r = subprocess.run(["groff", "-man", "-Tutf8", "-ww", "-z", str(page)], + capture_output=True, text=True, env={**os.environ, "LC_ALL": "C"}) + assert r.returncode == 0 and not r.stderr, r.stderr -- cgit v1.2.3