"""Shared helpers for the FablePool CLI/TUI test suite. Everything here drives the public CLI surface (``fablepool.cli.main``) exactly the way a user's terminal would, capturing stdout/stderr in-process so the suite runs fast and without subprocess overhead. """ from __future__ import annotations import contextlib import io import json from typing import List, Optional, Tuple from fablepool.cli import main as cli_main # Status vocabularies. The model layer uses one canonical word per state; # the small synonym sets keep assertions honest without overfitting. ACTIVE_STATUSES = {"active"} REFUTED_STATUSES = {"refuted"} INVALIDATED_STATUSES = {"invalidated", "stale"} CORRECTED_STATUSES = {"corrected", "superseded"} def run_cli(home, *args: str) -> Tuple[int, str, str]: """Run one CLI command in-process. Returns (exit_code, stdout, stderr).""" out, err = io.StringIO(), io.StringIO() with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err): try: code = cli_main(["--home", str(home), *[str(a) for a in args]]) except SystemExit as exc: # argparse error paths code = exc.code if isinstance(exc.code, int) else 1 return (0 if code is None else int(code)), out.getvalue(), err.getvalue() def run_ok(home, *args: str) -> str: """Run one CLI command and assert it succeeded; returns stdout.""" code, out, err = run_cli(home, *args) assert code == 0, ( f"fablepool {' '.join(str(a) for a in args)} failed with exit {code}:\n" f"{err or out}" ) return out def claim_list(payload) -> List[dict]: """Normalize ``claims --json`` output to a list of claim dicts.""" data = json.loads(payload) if isinstance(payload, str) else payload if isinstance(data, dict): for key in ("claims", "items", "results"): if key in data and isinstance(data[key], list): data = data[key] break assert isinstance(data, list), f"unexpected claims JSON shape: {type(data)}" return data def get_claims(home, *extra: str) -> List[dict]: return claim_list(run_ok(home, "claims", "--json", *extra)) def get_claim(home, claim_id: str) -> dict: data = json.loads(run_ok(home, "show", str(claim_id), "--json")) if isinstance(data, dict) and isinstance(data.get("claim"), dict): return data["claim"] assert isinstance(data, dict) return data def claim_inputs(claim: dict) -> List[str]: refs = claim.get("inputs") or claim.get("derived_from") or [] return [str(r) for r in refs] def find_dependency_pair(claims: List[dict]) -> Optional[Tuple[dict, dict]]: """Find (upstream, downstream) where downstream is derived from upstream.""" by_id = {str(c["id"]): c for c in claims} for c in claims: cid = str(c["id"]) for ref in claim_inputs(c): if ref in by_id and ref != cid: return by_id[ref], c return None def op_count(home) -> int: """Number of operations currently in the node's log.""" text = run_ok(home, "log", "--json", "--limit", "1000000") data = json.loads(text) if isinstance(data, dict): for key in ("ops", "operations", "entries", "log"): if key in data and isinstance(data[key], list): data = data[key] break assert isinstance(data, list), "unexpected log --json shape" return len(data)