"""Hardening tests for ``muse read``. Covers gaps identified during SUPERCHARGE review: 1. ``duration_ms`` + ``exit_code`` present in success JSON (TestElapsedAndExitCode) 2. commit-not-found error with ``--json`` emits structured JSON to stdout — no duplicate plain-text to stderr (TestErrorJson) 3. Unknown flag exits non-zero 4. Error JSON carries ``duration_ms`` and ``exit_code`` (TestErrorJson) 5. ``TestJsonSchema`` REQUIRED_KEYS updated to include ``duration_ms``/``exit_code`` """ from __future__ import annotations from collections.abc import Mapping import json import os import pathlib import pytest from tests.cli_test_helper import CliRunner, InvokeResult runner = CliRunner() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult: saved = os.getcwd() try: os.chdir(repo) return runner.invoke(None, args) finally: os.chdir(saved) def _show(repo: pathlib.Path, *extra: str) -> InvokeResult: return _invoke(repo, ["read", *extra]) def _commit(repo: pathlib.Path, *extra: str) -> InvokeResult: return _invoke(repo, ["commit", *extra]) @pytest.fixture() def repo(tmp_path: pathlib.Path) -> pathlib.Path: """Initialised repo with one tracked file and one commit.""" saved = os.getcwd() try: os.chdir(tmp_path) runner.invoke(None, ["init"]) finally: os.chdir(saved) (tmp_path / "a.py").write_text("x = 1\n") _commit(tmp_path, "-m", "initial commit") return tmp_path def _assert_error_json(result: InvokeResult) -> Mapping[str, object]: """Assert result has non-zero exit code and parseable error JSON on stdout.""" assert result.exit_code != 0 d = json.loads(result.output) # must be on stdout, not stderr assert "error" in d, f"'error' key missing: {d}" assert "duration_ms" in d, f"'duration_ms' key missing: {d}" assert "exit_code" in d, f"'exit_code' key missing: {d}" assert d["exit_code"] != 0 return d # --------------------------------------------------------------------------- # TestElapsedAndExitCode — success JSON must carry envelope fields # --------------------------------------------------------------------------- class TestElapsedAndExitCode: def test_success_json_has_duration_ms(self, repo: pathlib.Path) -> None: result = _show(repo, "--json") assert result.exit_code == 0 data = json.loads(result.output) assert "duration_ms" in data, f"'duration_ms' missing from: {list(data)}" def test_success_json_duration_ms_is_float(self, repo: pathlib.Path) -> None: result = _show(repo, "--json") data = json.loads(result.output) assert isinstance(data["duration_ms"], float), ( f"duration_ms should be float, got {type(data['duration_ms'])}" ) def test_success_json_duration_ms_non_negative(self, repo: pathlib.Path) -> None: result = _show(repo, "--json") data = json.loads(result.output) assert data["duration_ms"] >= 0.0 def test_success_json_has_exit_code(self, repo: pathlib.Path) -> None: result = _show(repo, "--json") data = json.loads(result.output) assert "exit_code" in data, f"'exit_code' missing from: {list(data)}" def test_success_json_exit_code_is_zero(self, repo: pathlib.Path) -> None: result = _show(repo, "--json") data = json.loads(result.output) assert data["exit_code"] == 0 def test_no_delta_json_has_envelope(self, repo: pathlib.Path) -> None: result = _show(repo, "--json", "--no-delta") data = json.loads(result.output) assert "duration_ms" in data assert "exit_code" in data assert data["exit_code"] == 0 def test_manifest_json_has_envelope(self, repo: pathlib.Path) -> None: result = _show(repo, "--json", "--manifest") data = json.loads(result.output) assert "duration_ms" in data assert "exit_code" in data assert data["exit_code"] == 0 def test_no_stat_json_has_envelope(self, repo: pathlib.Path) -> None: result = _show(repo, "--json", "--no-stat") data = json.loads(result.output) assert "duration_ms" in data assert "exit_code" in data assert data["exit_code"] == 0 # --------------------------------------------------------------------------- # TestErrorJson — error paths emit structured JSON to stdout # --------------------------------------------------------------------------- class TestErrorJson: def test_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None: """commit-not-found with --json emits JSON to stdout, not stderr.""" result = _show(repo, "--json", "nonexistent-branch-xyz") d = _assert_error_json(result) assert d["error"] == "commit_not_found" def test_commit_not_found_json_has_ref_key(self, repo: pathlib.Path) -> None: result = _show(repo, "--json", "nonexistent-branch-xyz") d = json.loads(result.output) assert "ref" in d def test_commit_not_found_no_duplicate_stderr(self, repo: pathlib.Path) -> None: """When --json, the plain-text ❌ line must NOT also appear on stderr.""" result = _show(repo, "--json", "nonexistent-ref") # stderr should be empty (or at most the ❌ line must NOT be present) stderr = result.stderr or "" assert "not found" not in stderr.lower(), ( f"Plain-text error leaked to stderr: {stderr!r}" ) def test_commit_not_found_error_json_has_duration_ms( self, repo: pathlib.Path ) -> None: result = _show(repo, "--json", "nonexistent") d = _assert_error_json(result) assert isinstance(d["duration_ms"], float) def test_commit_not_found_error_json_exit_code_nonzero( self, repo: pathlib.Path ) -> None: result = _show(repo, "--json", "nonexistent") d = _assert_error_json(result) assert d["exit_code"] == 1 def test_unknown_flag_exits_nonzero(self, repo: pathlib.Path) -> None: result = _show(repo, "--format", "xml") assert result.exit_code != 0 # --------------------------------------------------------------------------- # TestRequiredKeysUpdated — existing TestJsonSchema REQUIRED_KEYS check # --------------------------------------------------------------------------- class TestRequiredKeysUpdated: """duration_ms and exit_code must be in the success JSON schema.""" REQUIRED_KEYS = { "commit_id", "branch", "message", "author", "agent_id", "committed_at", "snapshot_id", "parent_commit_id", "parent2_commit_id", "sem_ver_bump", "breaking_changes", "metadata", "files_added", "files_removed", "files_modified", "duration_ms", "exit_code", } def test_all_required_keys_present(self, repo: pathlib.Path) -> None: result = _show(repo, "--json") assert result.exit_code == 0 data = json.loads(result.output) missing = self.REQUIRED_KEYS - set(data) assert not missing, f"Missing JSON keys: {missing}"