"""TDD tests for ``muse trust --json`` output. All read subcommands (list, hub-list) must support --json for agent ergonomics. Mutation subcommands (add, remove, hub-reset) emit a JSON confirmation when --json is passed. Coverage: - trust list --json → {"trusted_dirs": [...]} - trust list --json with no dirs → {"trusted_dirs": []} - trust list --json with multiple dirs → all present - trust hub-list --json → {"fingerprints": {hostname: {...}}} - trust hub-list --json with no fingerprints → {"fingerprints": {}} - trust add --json → {"added": true, "path": "..."} - trust remove --json → {"removed": true/false, "path": "..."} - trust hub-reset --json → {"reset": true/false, "hostname": "..."} - trust list (text) still works (no regression) - trust hub-list (text) still works (no regression) """ from __future__ import annotations import json import pathlib import unittest.mock import pytest from tests.cli_test_helper import CliRunner runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _invoke(args: list[str], muse_home: pathlib.Path | None = None) -> tuple[int, str, str]: env: dict[str, str] = {} result = runner.invoke(None, args, env=env) return result.exit_code, result.stdout, result.stderr def _invoke_with_home(args: list[str], muse_home: pathlib.Path) -> tuple[int, str, str]: """Invoke with a patched MUSE_HOME so config is isolated.""" import muse.cli.config as cfg_mod import muse.core.hub_trust as ht_mod config_file = muse_home / "config.toml" hub_trust_file = muse_home / "hub_trust.toml" with ( unittest.mock.patch.object(cfg_mod, "_GLOBAL_CONFIG_FILE", config_file), unittest.mock.patch.object(ht_mod, "_HUB_TRUST_FILE", hub_trust_file), ): result = runner.invoke(None, args, env={}) return result.exit_code, result.stdout, result.stderr # --------------------------------------------------------------------------- # trust list --json # --------------------------------------------------------------------------- class TestTrustListJson: def test_json_is_object(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert isinstance(data, dict) def test_json_has_trusted_dirs_key(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert "trusted_dirs" in data def test_json_empty_when_no_dirs(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert data["trusted_dirs"] == [] def test_json_contains_added_dirs(self, tmp_path: pathlib.Path) -> None: _invoke_with_home(["trust", "add", "/some/path"], tmp_path) rc, out, err = _invoke_with_home(["trust", "list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert any("/some/path" in d for d in data["trusted_dirs"]) def test_json_multiple_dirs(self, tmp_path: pathlib.Path) -> None: _invoke_with_home(["trust", "add", "/path/a"], tmp_path) _invoke_with_home(["trust", "add", "/path/b"], tmp_path) rc, out, err = _invoke_with_home(["trust", "list", "--json"], tmp_path) data = json.loads(out.strip()) dirs = data["trusted_dirs"] assert len(dirs) == 2 def test_text_list_still_works(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "list"], tmp_path) assert rc == 0 # text output — should not start with "{" stripped = out.strip() assert not stripped.startswith("{") or stripped == "" # --------------------------------------------------------------------------- # trust hub-list --json # --------------------------------------------------------------------------- class TestTrustHubListJson: def test_json_is_object(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "hub-list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert isinstance(data, dict) def test_json_has_fingerprints_key(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "hub-list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert "fingerprints" in data def test_json_empty_when_no_fingerprints(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "hub-list", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert data["fingerprints"] == {} def test_json_fingerprints_is_dict(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "hub-list", "--json"], tmp_path) data = json.loads(out.strip()) assert isinstance(data["fingerprints"], dict) def test_text_hub_list_still_works(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "hub-list"], tmp_path) assert rc == 0 stripped = out.strip() assert not stripped.startswith("{") or stripped == "" # --------------------------------------------------------------------------- # trust add --json # --------------------------------------------------------------------------- class TestTrustAddJson: def test_json_exit_zero(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "add", "/test/repo", "--json"], tmp_path) assert rc == 0 def test_json_is_object(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "add", "/test/repo", "--json"], tmp_path) data = json.loads(out.strip()) assert isinstance(data, dict) def test_json_has_added_and_path(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "add", "/test/repo", "--json"], tmp_path) data = json.loads(out.strip()) assert "added" in data assert "path" in data def test_json_added_is_true(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "add", "/test/repo", "--json"], tmp_path) data = json.loads(out.strip()) assert data["added"] is True # --------------------------------------------------------------------------- # trust remove --json # --------------------------------------------------------------------------- class TestTrustRemoveJson: def test_json_removed_true_when_present(self, tmp_path: pathlib.Path) -> None: _invoke_with_home(["trust", "add", "/rm/this"], tmp_path) rc, out, err = _invoke_with_home(["trust", "remove", "/rm/this", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert data["removed"] is True assert "/rm/this" in data["path"] def test_json_removed_false_when_absent(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "remove", "/does/not/exist", "--json"], tmp_path) assert rc == 0 data = json.loads(out.strip()) assert data["removed"] is False def test_json_has_path_key(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home(["trust", "remove", "/some/path", "--json"], tmp_path) data = json.loads(out.strip()) assert "path" in data # --------------------------------------------------------------------------- # trust hub-reset --json # --------------------------------------------------------------------------- class TestTrustHubResetJson: def test_json_reset_false_when_not_pinned(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home( ["trust", "hub-reset", "notahost.example", "--json"], tmp_path ) assert rc == 0 data = json.loads(out.strip()) assert data["reset"] is False assert "hostname" in data def test_json_has_hostname_key(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home( ["trust", "hub-reset", "somehost:10003", "--json"], tmp_path ) data = json.loads(out.strip()) assert "hostname" in data def test_json_is_object(self, tmp_path: pathlib.Path) -> None: rc, out, err = _invoke_with_home( ["trust", "hub-reset", "h.test", "--json"], tmp_path ) data = json.loads(out.strip()) assert isinstance(data, dict) class TestRegisterFlags: def _parse(self, *args: str) -> "argparse.Namespace": import argparse from muse.cli.commands.trust import register p = argparse.ArgumentParser() sub = p.add_subparsers() register(sub) return p.parse_args(["trust", *args]) def test_default_json_out_is_false(self) -> None: ns = self._parse("list") assert ns.json_out is False def test_json_flag_sets_json_out(self) -> None: ns = self._parse("list", "--json") assert ns.json_out is True def test_j_shorthand_sets_json_out(self) -> None: ns = self._parse("list", "-j") assert ns.json_out is True