gabriel / muse public
test_cmd_show_hardening.py python
208 lines 7.3 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Hardening tests for ``muse read``.
2
3 Covers gaps identified during SUPERCHARGE review:
4
5 1. ``duration_ms`` + ``exit_code`` present in success JSON (TestElapsedAndExitCode)
6 2. commit-not-found error with ``--json`` emits structured JSON to stdout — no
7 duplicate plain-text to stderr (TestErrorJson)
8 3. Unknown flag exits non-zero
9 4. Error JSON carries ``duration_ms`` and ``exit_code`` (TestErrorJson)
10 5. ``TestJsonSchema`` REQUIRED_KEYS updated to include ``duration_ms``/``exit_code``
11 """
12
13 from __future__ import annotations
14 from collections.abc import Mapping
15
16 import json
17 import os
18 import pathlib
19
20 import pytest
21
22 from tests.cli_test_helper import CliRunner, InvokeResult
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31
32 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
33 saved = os.getcwd()
34 try:
35 os.chdir(repo)
36 return runner.invoke(None, args)
37 finally:
38 os.chdir(saved)
39
40
41 def _show(repo: pathlib.Path, *extra: str) -> InvokeResult:
42 return _invoke(repo, ["read", *extra])
43
44
45 def _commit(repo: pathlib.Path, *extra: str) -> InvokeResult:
46 return _invoke(repo, ["commit", *extra])
47
48
49 @pytest.fixture()
50 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
51 """Initialised repo with one tracked file and one commit."""
52 saved = os.getcwd()
53 try:
54 os.chdir(tmp_path)
55 runner.invoke(None, ["init"])
56 finally:
57 os.chdir(saved)
58 (tmp_path / "a.py").write_text("x = 1\n")
59 _commit(tmp_path, "-m", "initial commit")
60 return tmp_path
61
62
63 def _assert_error_json(result: InvokeResult) -> Mapping[str, object]:
64 """Assert result has non-zero exit code and parseable error JSON on stdout."""
65 assert result.exit_code != 0
66 d = json.loads(result.output) # must be on stdout, not stderr
67 assert "error" in d, f"'error' key missing: {d}"
68 assert "duration_ms" in d, f"'duration_ms' key missing: {d}"
69 assert "exit_code" in d, f"'exit_code' key missing: {d}"
70 assert d["exit_code"] != 0
71 return d
72
73
74 # ---------------------------------------------------------------------------
75 # TestElapsedAndExitCode — success JSON must carry envelope fields
76 # ---------------------------------------------------------------------------
77
78
79 class TestElapsedAndExitCode:
80 def test_success_json_has_duration_ms(self, repo: pathlib.Path) -> None:
81 result = _show(repo, "--json")
82 assert result.exit_code == 0
83 data = json.loads(result.output)
84 assert "duration_ms" in data, f"'duration_ms' missing from: {list(data)}"
85
86 def test_success_json_duration_ms_is_float(self, repo: pathlib.Path) -> None:
87 result = _show(repo, "--json")
88 data = json.loads(result.output)
89 assert isinstance(data["duration_ms"], float), (
90 f"duration_ms should be float, got {type(data['duration_ms'])}"
91 )
92
93 def test_success_json_duration_ms_non_negative(self, repo: pathlib.Path) -> None:
94 result = _show(repo, "--json")
95 data = json.loads(result.output)
96 assert data["duration_ms"] >= 0.0
97
98 def test_success_json_has_exit_code(self, repo: pathlib.Path) -> None:
99 result = _show(repo, "--json")
100 data = json.loads(result.output)
101 assert "exit_code" in data, f"'exit_code' missing from: {list(data)}"
102
103 def test_success_json_exit_code_is_zero(self, repo: pathlib.Path) -> None:
104 result = _show(repo, "--json")
105 data = json.loads(result.output)
106 assert data["exit_code"] == 0
107
108 def test_no_delta_json_has_envelope(self, repo: pathlib.Path) -> None:
109 result = _show(repo, "--json", "--no-delta")
110 data = json.loads(result.output)
111 assert "duration_ms" in data
112 assert "exit_code" in data
113 assert data["exit_code"] == 0
114
115 def test_manifest_json_has_envelope(self, repo: pathlib.Path) -> None:
116 result = _show(repo, "--json", "--manifest")
117 data = json.loads(result.output)
118 assert "duration_ms" in data
119 assert "exit_code" in data
120 assert data["exit_code"] == 0
121
122 def test_no_stat_json_has_envelope(self, repo: pathlib.Path) -> None:
123 result = _show(repo, "--json", "--no-stat")
124 data = json.loads(result.output)
125 assert "duration_ms" in data
126 assert "exit_code" in data
127 assert data["exit_code"] == 0
128
129
130 # ---------------------------------------------------------------------------
131 # TestErrorJson — error paths emit structured JSON to stdout
132 # ---------------------------------------------------------------------------
133
134
135 class TestErrorJson:
136 def test_commit_not_found_json_to_stdout(self, repo: pathlib.Path) -> None:
137 """commit-not-found with --json emits JSON to stdout, not stderr."""
138 result = _show(repo, "--json", "nonexistent-branch-xyz")
139 d = _assert_error_json(result)
140 assert d["error"] == "commit_not_found"
141
142 def test_commit_not_found_json_has_ref_key(self, repo: pathlib.Path) -> None:
143 result = _show(repo, "--json", "nonexistent-branch-xyz")
144 d = json.loads(result.output)
145 assert "ref" in d
146
147 def test_commit_not_found_no_duplicate_stderr(self, repo: pathlib.Path) -> None:
148 """When --json, the plain-text ❌ line must NOT also appear on stderr."""
149 result = _show(repo, "--json", "nonexistent-ref")
150 # stderr should be empty (or at most the ❌ line must NOT be present)
151 stderr = result.stderr or ""
152 assert "not found" not in stderr.lower(), (
153 f"Plain-text error leaked to stderr: {stderr!r}"
154 )
155
156 def test_commit_not_found_error_json_has_duration_ms(
157 self, repo: pathlib.Path
158 ) -> None:
159 result = _show(repo, "--json", "nonexistent")
160 d = _assert_error_json(result)
161 assert isinstance(d["duration_ms"], float)
162
163 def test_commit_not_found_error_json_exit_code_nonzero(
164 self, repo: pathlib.Path
165 ) -> None:
166 result = _show(repo, "--json", "nonexistent")
167 d = _assert_error_json(result)
168 assert d["exit_code"] == 1
169
170 def test_unknown_flag_exits_nonzero(self, repo: pathlib.Path) -> None:
171 result = _show(repo, "--format", "xml")
172 assert result.exit_code != 0
173
174
175 # ---------------------------------------------------------------------------
176 # TestRequiredKeysUpdated — existing TestJsonSchema REQUIRED_KEYS check
177 # ---------------------------------------------------------------------------
178
179
180 class TestRequiredKeysUpdated:
181 """duration_ms and exit_code must be in the success JSON schema."""
182
183 REQUIRED_KEYS = {
184 "commit_id",
185 "branch",
186 "message",
187 "author",
188 "agent_id",
189 "committed_at",
190 "snapshot_id",
191 "parent_commit_id",
192 # parent2_commit_id omitted for non-merge commits
193 "sem_ver_bump",
194 "breaking_changes",
195 # metadata omitted when empty
196 "files_added",
197 "files_removed",
198 "files_modified",
199 "duration_ms",
200 "exit_code",
201 }
202
203 def test_all_required_keys_present(self, repo: pathlib.Path) -> None:
204 result = _show(repo, "--json")
205 assert result.exit_code == 0
206 data = json.loads(result.output)
207 missing = self.REQUIRED_KEYS - set(data)
208 assert not missing, f"Missing JSON keys: {missing}"
File History 5 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c10a2ce474b3bb7ff2a3d628e8a3f2e028fd78ca652513496a03a498ae2267b3 chore: sweep all stale DirectoryRenameOp / directory_rename… Sonnet 4.6 minor 23 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago