"""Hardening tests for ``muse show-ref``. Gaps closed ----------- 1. ``duration_ms`` + ``exit_code`` absent from ALL JSON output paths (listing, ``--head``, ``--count``, ``--verify``). 2. Format error wrote JSON to stderr — inconsistent with the agent error pattern: should be plain text to stderr (fmt is unknown, so we can't assume JSON was desired). 3. I/O error on listing wrote JSON to stderr — should use _emit_error(). 4. ``_ShowRefResult`` TypedDict missing ``duration_ms`` / ``exit_code``. 5. Module docstring missing envelope fields and error contract. """ from __future__ import annotations from collections.abc import Mapping import json import pathlib import pytest from muse.core.types import long_id from muse.core.paths import muse_dir, ref_path from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() _VALID_OID = long_id("a" * 64) # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path: repo = tmp_path / "repo" dot_muse = muse_dir(repo) (dot_muse / "objects").mkdir(parents=True) (dot_muse / "commits").mkdir(parents=True) (dot_muse / "snapshots").mkdir(parents=True) (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "HEAD").write_text("ref: refs/heads/main") (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "r1", "domain": "code"})) return repo def _write_ref(repo: pathlib.Path, branch: str, commit_id: str = _VALID_OID) -> None: (ref_path(repo, branch)).write_text(commit_id) def _sr(repo: pathlib.Path, *args: str) -> InvokeResult: from muse.cli.app import main as cli return runner.invoke(cli, ["show-ref", *args], env={"MUSE_REPO_ROOT": str(repo)}) def _assert_has_envelope(data: Mapping[str, object]) -> None: assert "duration_ms" in data, f"'duration_ms' missing: {list(data)}" assert "exit_code" in data, f"'exit_code' missing: {list(data)}" assert isinstance(data["duration_ms"], float) assert data["duration_ms"] >= 0.0 assert data["exit_code"] == 0 # --------------------------------------------------------------------------- # TestElapsedAndExitCode — every JSON output path must carry the envelope # --------------------------------------------------------------------------- class TestElapsedAndExitCode: def test_listing_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--json").output) assert "duration_ms" in data def test_listing_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--json").output) assert isinstance(data["duration_ms"], float) assert data["duration_ms"] >= 0.0 def test_listing_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--json").output) assert data["exit_code"] == 0 def test_listing_with_refs_has_envelope(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_ref(repo, "main") data = json.loads(_sr(repo, "--json").output) _assert_has_envelope(data) def test_count_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--count", "--json").output) assert "duration_ms" in data def test_count_json_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--count", "--json").output) assert data["exit_code"] == 0 def test_count_json_has_count_field(self, tmp_path: pathlib.Path) -> None: """Adding envelope must not drop the count field.""" repo = _make_repo(tmp_path) _write_ref(repo, "main") data = json.loads(_sr(repo, "--count", "--json").output) assert data["count"] == 1 def test_head_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_ref(repo, "main") data = json.loads(_sr(repo, "--head", "--json").output) assert "duration_ms" in data def test_head_json_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_ref(repo, "main") data = json.loads(_sr(repo, "--head", "--json").output) assert data["exit_code"] == 0 def test_head_null_json_has_envelope(self, tmp_path: pathlib.Path) -> None: """Even when HEAD has no commit, the envelope must be present.""" repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--head", "--json").output) assert "duration_ms" in data assert "exit_code" in data def test_verify_exists_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_ref(repo, "main") data = json.loads(_sr(repo, "--verify", "refs/heads/main", "--json").output) assert "duration_ms" in data def test_verify_exists_json_has_exit_code_zero(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) _write_ref(repo, "main") data = json.loads(_sr(repo, "--verify", "refs/heads/main", "--json").output) assert data["exit_code"] == 0 def test_verify_not_exists_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--verify", "refs/heads/ghost", "--json").output) assert "duration_ms" in data def test_verify_not_exists_json_has_exit_code_nonzero( self, tmp_path: pathlib.Path ) -> None: repo = _make_repo(tmp_path) result = _sr(repo, "--verify", "refs/heads/ghost", "--json") data = json.loads(result.output) assert data["exit_code"] == result.exit_code assert data["exit_code"] != 0 # --------------------------------------------------------------------------- # TestErrorJson — format error must be plain text to stderr, not JSON to stderr # --------------------------------------------------------------------------- class TestErrorJson: def test_bad_format_stdout_is_empty(self, tmp_path: pathlib.Path) -> None: """Format error must not bleed anything to stdout.""" repo = _make_repo(tmp_path) result = _sr(repo, "--format", "yaml") assert result.exit_code != 0 assert result.stdout_bytes == b"" def test_bad_format_stderr_has_message(self, tmp_path: pathlib.Path) -> None: """Format error message goes to stderr as plain text.""" repo = _make_repo(tmp_path) result = _sr(repo, "--format", "yaml") assert result.stderr # must not be empty assert "\x1b[" not in result.stderr # no ANSI escapes def test_bad_format_stderr_is_not_json(self, tmp_path: pathlib.Path) -> None: """Format error should NOT be a JSON blob on stderr.""" repo = _make_repo(tmp_path) result = _sr(repo, "--format", "yaml") # Plain-text error: stderr should not parse as JSON try: json.loads(result.stderr) is_json = True except (json.JSONDecodeError, ValueError): is_json = False assert not is_json, f"stderr should be plain text, got JSON: {result.stderr!r}" # --------------------------------------------------------------------------- # TestRequiredKeysUpdated — listing JSON schema includes envelope fields # --------------------------------------------------------------------------- class TestRequiredKeysUpdated: REQUIRED_KEYS = {"refs", "head", "count", "duration_ms", "exit_code"} def test_listing_schema_complete(self, tmp_path: pathlib.Path) -> None: repo = _make_repo(tmp_path) data = json.loads(_sr(repo, "--json").output) missing = self.REQUIRED_KEYS - set(data) assert not missing, f"Missing JSON keys: {missing}" # --------------------------------------------------------------------------- # TestValidOidFormat — refs written with sha256: prefix are listed correctly # --------------------------------------------------------------------------- class TestValidOidFormat: def test_sha256_prefixed_oid_appears_in_listing( self, tmp_path: pathlib.Path ) -> None: """Only sha256:-prefixed OIDs are valid; bare hex is rejected.""" repo = _make_repo(tmp_path) _write_ref(repo, "main", _VALID_OID) data = json.loads(_sr(repo, "--json").output) assert data["count"] == 1 assert data["refs"][0]["commit_id"] == _VALID_OID def test_bare_hex_oid_is_silently_skipped( self, tmp_path: pathlib.Path ) -> None: """Bare 64-hex-char OID without sha256: prefix fails validate_object_id.""" repo = _make_repo(tmp_path) _write_ref(repo, "bad", "a" * 64) # no sha256: prefix data = json.loads(_sr(repo, "--json").output) assert data["count"] == 0 # skipped silently def test_valid_and_invalid_oid_mixed(self, tmp_path: pathlib.Path) -> None: """Only the valid sha256:-prefixed ref is listed; bare hex is dropped.""" repo = _make_repo(tmp_path) _write_ref(repo, "valid", _VALID_OID) _write_ref(repo, "bare", "b" * 64) # intentionally bare hex — must be rejected data = json.loads(_sr(repo, "--json").output) assert data["count"] == 1 assert data["refs"][0]["ref"] == "refs/heads/valid" class TestRegisterFlags: def test_default_json_out_is_false(self) -> None: import argparse from muse.cli.commands.show_ref import register p = argparse.ArgumentParser() subs = p.add_subparsers() register(subs) args = p.parse_args(["show-ref"]) assert args.json_out is False def test_json_flag_sets_json_out(self) -> None: import argparse from muse.cli.commands.show_ref import register p = argparse.ArgumentParser() subs = p.add_subparsers() register(subs) args = p.parse_args(["show-ref", "--json"]) assert args.json_out is True def test_j_shorthand_sets_json_out(self) -> None: import argparse from muse.cli.commands.show_ref import register p = argparse.ArgumentParser() subs = p.add_subparsers() register(subs) args = p.parse_args(["show-ref", "-j"]) assert args.json_out is True