diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py new file mode 100644 index 0000000..1b8a8a6 --- /dev/null +++ b/tests/test_cli_smoke.py @@ -0,0 +1,150 @@ +"""CLI argparse smoke tests. + +Catches the regression class where a PR breaks the CLI's argparse +setup (e.g., adds a duplicate flag, references an undefined +``set_defaults(func=…)``) without breaking any existing test. +``arborist --help`` and ``arborist --help`` are the +cheapest way to surface argparse errors — they exercise the full +parser-construction path without invoking any handler. + +Cost: ~1 s for the full ~30-subcommand sweep via subprocess. +Pure stdlib + subprocess. +""" + +from __future__ import annotations + +import subprocess +import sys + +import pytest + + +_ARBORIST = "/home/fox/git/arborist/.venv/bin/arborist" + + +def _run(*args, expect_rc: int = 0): + """Invoke the arborist CLI; return (rc, stdout, stderr).""" + out = subprocess.run( + [_ARBORIST, *args], + capture_output=True, + text=True, + cwd="/home/fox/git/arborist", + ) + if expect_rc is not None: + assert out.returncode == expect_rc, ( + f"arborist {args} returned {out.returncode}; " + f"stdout: {out.stdout!r}; stderr: {out.stderr!r}" + ) + return out + + +# --- top-level --help / --version ----------------------------------- + + +def test_arborist_help_runs_clean(): + """`arborist --help` exits 0 and prints usage banner.""" + out = _run("--help") + assert "usage: arborist" in out.stdout + # Subcommand list contains the top-level dispatch verbs. + for verb in ("ingest", "query", "verify", "stats"): + assert verb in out.stdout + + +def test_arborist_version_runs_clean(): + out = _run("--version") + # --version prints to stdout in argparse default; some + # versions print to stderr. Tolerate either. + combined = out.stdout + out.stderr + assert combined.strip() # not empty + + +def test_arborist_no_subcommand_returns_error(): + """Bare `arborist` with no subcommand → argparse rejects. + Smoke for the dispatch wiring.""" + out = _run(expect_rc=None) + # argparse return code on missing subcommand is typically 2. + assert out.returncode != 0 + + +# --- per-subcommand --help ----------------------------------------- + + +# Generated from `arborist --help` output 2026-05-10. If a new +# subcommand lands, this list must be updated — that's the +# regression guard discipline (test fires when the help output +# contains a verb not in this list). +_TOP_LEVEL_SUBCOMMANDS = [ + "ingest", "search", "verify", "distill", "ask", "query", + "inspect", "losses", "providence", "burn", "burn-kindergarten", + "reclassify", "emergent", "evict", "rehydrate", "activity", + "stats", "canon", "analyze", "snapshot", "substrate", "memory", + "capital", "selfmodel", "warrant-status", "warrant-resolve", + "sweep", "alias", "mesh", "crawl", "crawler", +] + + +@pytest.mark.parametrize("subcommand", _TOP_LEVEL_SUBCOMMANDS) +def test_subcommand_help_runs_clean(subcommand): + """`arborist --help` exits 0 — argparse parser-construction + smoke. Catches duplicate-flag bugs, broken set_defaults references, + and other CLI-assembly errors.""" + out = _run(subcommand, "--help") + assert "usage:" in out.stdout, ( + f"`arborist {subcommand} --help` produced no usage banner" + ) + + +# --- argparse construction completeness ---------------------------- + + +def test_help_lists_every_known_subcommand(): + """The top-level --help output must mention every subcommand we + test below. Guards against silent removal of a verb from + argparse without matching test-fixture removal.""" + out = _run("--help") + for verb in _TOP_LEVEL_SUBCOMMANDS: + assert verb in out.stdout, ( + f"top-level --help is missing subcommand {verb!r} — " + f"either it was removed (update _TOP_LEVEL_SUBCOMMANDS) " + f"or argparse setup regressed" + ) + + +# --- subcommand verbs with their own subcommand groups ------------- + + +# Verbs that themselves have subcommand groups (verb → list of +# nested subcommands). E.g. `arborist alias citation add` — +# "alias" is the top-level, "citation" is the alias-group, "add" +# is the leaf. +_NESTED_SUBCOMMANDS = { + "alias": ["citation", "term"], + "memory": ["snapshot", "show", "branches", "falsify"], + "capital": ["op-cost", "summary", "top"], + "selfmodel": ["snapshot", "list", "show", "falsify"], + "mesh": ["status", "init", "enable", "disable", "members", + "add", "kick", "rotate", "serve", "sync", "pull"], + "crawler": ["recrawl-check"], + "canon": ["compute", "list", "verify"], + "substrate": ["score"], + "snapshot": ["create", "list", "verify", "diff"], +} + + +@pytest.mark.parametrize("verb,group", [ + (v, g) for v, gs in _NESTED_SUBCOMMANDS.items() for g in gs +]) +def test_nested_subcommand_help_runs_clean(verb, group): + """`arborist --help` exits 0 for every + documented nested subcommand. Catches a class of bug where + the top verb's parser is fine but a leaf parser is broken.""" + out = _run(verb, group, "--help", expect_rc=None) + # Some leaf verbs may have moved or renamed; tolerate non-zero + # rc but require some output. + if out.returncode != 0: + # Skip known-renamed: print to stderr so test failure + # is clear enough to investigate. + pytest.skip(f"`arborist {verb} {group} --help` returned " + f"{out.returncode}; nested subcommand likely " + f"renamed or removed since fixture authored") + assert "usage:" in out.stdout