gabriel / muse public
test_maintenance_supercharge.py python
332 lines 13.5 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 140 days ago
1 """Supercharge tests for ``muse maintenance``.
2
3 Coverage tiers
4 --------------
5 - JSON envelope: status, error, exit_code, duration_ms always present on run/status/schedule
6 - Error payload: exactly {status, error, exit_code} — no prose to stdout in --json mode
7 - OID integrity: verify-objects failure list uses sha256:-prefixed IDs
8 - schedule --json: new mode; emits {status, enabled, period_hours, exit_code}
9 - TypedDicts: _MaintenanceRunJson, _MaintenanceStatusJson, _MaintenanceScheduleJson,
10 _MaintenanceErrorJson exist and are annotated
11 - Docstring: covers status, error, exit_code, duration_ms for all subcommands
12 - No-prose pollution: valid JSON on stdout, no emoji in JSON mode
13 """
14 from __future__ import annotations
15
16 import datetime
17 import hashlib
18 import json
19 import pathlib
20 from typing import get_type_hints
21
22 from muse.core.object_store import object_path, write_object
23 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
24 from muse.core.store import SnapshotRecord, write_snapshot
25 from muse.core._types import Manifest, long_id
26 from tests.cli_test_helper import CliRunner
27
28 runner = CliRunner()
29 _REPO_ID = "maintenance-sg"
30
31
32 # ---------------------------------------------------------------------------
33 # Helpers
34 # ---------------------------------------------------------------------------
35
36 def _sha(data: bytes) -> str:
37 return long_id(hashlib.sha256(data).hexdigest())
38
39
40 def _init_repo(path: pathlib.Path) -> pathlib.Path:
41 muse = path / ".muse"
42 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
43 (muse / d).mkdir(parents=True, exist_ok=True)
44 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
45 (muse / "repo.json").write_text(
46 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
47 )
48 return path
49
50
51 def _write_obj(repo: pathlib.Path, content: bytes) -> str:
52 oid = _sha(content)
53 write_object(repo, oid, content)
54 return oid
55
56
57 def _invoke(repo: pathlib.Path, *args: str):
58 from muse.cli.app import main as cli
59 return runner.invoke(cli, list(args), env={"MUSE_REPO_ROOT": str(repo)})
60
61
62 # ---------------------------------------------------------------------------
63 # run --json envelope
64 # ---------------------------------------------------------------------------
65
66 class TestRunJsonEnvelope:
67 """``maintenance run --json`` envelope has all required fields."""
68
69 _REQUIRED = {"status", "error", "tasks_run", "results", "dry_run", "duration_ms", "exit_code"}
70
71 def test_all_required_keys_present(self, tmp_path: pathlib.Path) -> None:
72 repo = _init_repo(tmp_path)
73 r = _invoke(repo, "maintenance", "run", "--json")
74 assert r.exit_code == 0
75 d = json.loads(r.output)
76 missing = self._REQUIRED - d.keys()
77 assert not missing, f"Missing keys: {missing}"
78
79 def test_status_ok_on_success(self, tmp_path: pathlib.Path) -> None:
80 repo = _init_repo(tmp_path)
81 r = _invoke(repo, "maintenance", "run", "--json")
82 assert json.loads(r.output)["status"] == "ok"
83
84 def test_error_empty_on_success(self, tmp_path: pathlib.Path) -> None:
85 repo = _init_repo(tmp_path)
86 r = _invoke(repo, "maintenance", "run", "--json")
87 assert json.loads(r.output)["error"] == ""
88
89 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
90 repo = _init_repo(tmp_path)
91 r = _invoke(repo, "maintenance", "run", "--json")
92 assert json.loads(r.output)["exit_code"] == 0
93
94 def test_duration_ms_is_nonneg_float(self, tmp_path: pathlib.Path) -> None:
95 repo = _init_repo(tmp_path)
96 r = _invoke(repo, "maintenance", "run", "--json")
97 d = json.loads(r.output)
98 assert isinstance(d["duration_ms"], float)
99 assert d["duration_ms"] >= 0.0
100
101 def test_no_elapsed_ms_key(self, tmp_path: pathlib.Path) -> None:
102 """Deprecated elapsed_ms replaced by duration_ms for consistency."""
103 repo = _init_repo(tmp_path)
104 r = _invoke(repo, "maintenance", "run", "--json")
105 d = json.loads(r.output)
106 assert "elapsed_ms" not in d
107
108 def test_dry_run_flag_reflected(self, tmp_path: pathlib.Path) -> None:
109 repo = _init_repo(tmp_path)
110 r = _invoke(repo, "maintenance", "run", "--dry-run", "--json")
111 d = json.loads(r.output)
112 assert d["dry_run"] is True
113
114 def test_tasks_run_is_list(self, tmp_path: pathlib.Path) -> None:
115 repo = _init_repo(tmp_path)
116 r = _invoke(repo, "maintenance", "run", "--task", "gc", "--json")
117 d = json.loads(r.output)
118 assert isinstance(d["tasks_run"], list)
119 assert "gc" in d["tasks_run"]
120
121
122 # ---------------------------------------------------------------------------
123 # status --json envelope
124 # ---------------------------------------------------------------------------
125
126 class TestStatusJsonEnvelope:
127 """``maintenance status --json`` envelope has all required fields."""
128
129 _REQUIRED = {"status", "error", "enabled", "period_hours", "tasks", "last_run", "exit_code"}
130
131 def test_all_required_keys_present(self, tmp_path: pathlib.Path) -> None:
132 repo = _init_repo(tmp_path)
133 r = _invoke(repo, "maintenance", "status", "--json")
134 assert r.exit_code == 0
135 d = json.loads(r.output)
136 missing = self._REQUIRED - d.keys()
137 assert not missing, f"Missing keys: {missing}"
138
139 def test_status_ok_on_success(self, tmp_path: pathlib.Path) -> None:
140 repo = _init_repo(tmp_path)
141 r = _invoke(repo, "maintenance", "status", "--json")
142 assert json.loads(r.output)["status"] == "ok"
143
144 def test_error_empty_on_success(self, tmp_path: pathlib.Path) -> None:
145 repo = _init_repo(tmp_path)
146 r = _invoke(repo, "maintenance", "status", "--json")
147 assert json.loads(r.output)["error"] == ""
148
149 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
150 repo = _init_repo(tmp_path)
151 r = _invoke(repo, "maintenance", "status", "--json")
152 assert json.loads(r.output)["exit_code"] == 0
153
154
155 # ---------------------------------------------------------------------------
156 # schedule --json
157 # ---------------------------------------------------------------------------
158
159 class TestScheduleJson:
160 """``maintenance schedule --json`` emits a structured result."""
161
162 _REQUIRED = {"status", "error", "enabled", "period_hours", "exit_code"}
163
164 def test_schedule_json_flag_accepted(self, tmp_path: pathlib.Path) -> None:
165 repo = _init_repo(tmp_path)
166 r = _invoke(repo, "maintenance", "schedule", "--period-hours", "48", "--json")
167 assert r.exit_code == 0
168
169 def test_schedule_json_all_required_keys(self, tmp_path: pathlib.Path) -> None:
170 repo = _init_repo(tmp_path)
171 r = _invoke(repo, "maintenance", "schedule", "--json")
172 assert r.exit_code == 0
173 d = json.loads(r.output)
174 missing = self._REQUIRED - d.keys()
175 assert not missing, f"Missing keys: {missing}"
176
177 def test_schedule_json_status_ok(self, tmp_path: pathlib.Path) -> None:
178 repo = _init_repo(tmp_path)
179 r = _invoke(repo, "maintenance", "schedule", "--json")
180 assert json.loads(r.output)["status"] == "ok"
181
182 def test_schedule_json_reflects_period_hours(self, tmp_path: pathlib.Path) -> None:
183 repo = _init_repo(tmp_path)
184 r = _invoke(repo, "maintenance", "schedule", "--period-hours", "72", "--json")
185 d = json.loads(r.output)
186 assert d["period_hours"] == 72
187
188 def test_schedule_json_reflects_enabled_true(self, tmp_path: pathlib.Path) -> None:
189 repo = _init_repo(tmp_path)
190 r = _invoke(repo, "maintenance", "schedule", "--enable", "--json")
191 d = json.loads(r.output)
192 assert d["enabled"] is True
193
194 def test_schedule_json_reflects_enabled_false(self, tmp_path: pathlib.Path) -> None:
195 repo = _init_repo(tmp_path)
196 r = _invoke(repo, "maintenance", "schedule", "--disable", "--json")
197 d = json.loads(r.output)
198 assert d["enabled"] is False
199
200 def test_schedule_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
201 repo = _init_repo(tmp_path)
202 r = _invoke(repo, "maintenance", "schedule", "--json")
203 assert json.loads(r.output)["exit_code"] == 0
204
205
206 # ---------------------------------------------------------------------------
207 # OID integrity — verify-objects failures list
208 # ---------------------------------------------------------------------------
209
210 class TestVerifyObjectsOidIntegrity:
211 """Failure OIDs in verify-objects results carry the sha256: prefix."""
212
213 def test_failures_list_uses_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
214 """When an object is corrupt, its ID in the failures list is sha256:-prefixed."""
215 repo = _init_repo(tmp_path)
216 oid = _write_obj(repo, b"original content")
217 obj_path = object_path(repo, oid)
218 obj_path.chmod(0o644)
219 obj_path.write_bytes(b"corrupted data")
220
221 r = _invoke(repo, "maintenance", "run", "--task", "verify-objects", "--json")
222 d = json.loads(r.output)
223 failures = d["results"]["verify-objects"]["failures"]
224 assert failures, "Expected at least one failure entry"
225 for fid in failures:
226 assert fid.startswith("sha256:"), f"Failure OID not prefixed: {fid!r}"
227
228 def test_clean_store_failures_list_empty(self, tmp_path: pathlib.Path) -> None:
229 repo = _init_repo(tmp_path)
230 for i in range(3):
231 _write_obj(repo, f"clean-{i}".encode())
232 r = _invoke(repo, "maintenance", "run", "--task", "verify-objects", "--json")
233 d = json.loads(r.output)
234 assert d["results"]["verify-objects"]["failures"] == []
235
236
237 # ---------------------------------------------------------------------------
238 # No-prose pollution
239 # ---------------------------------------------------------------------------
240
241 class TestNoProsePollution:
242 def test_run_json_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
243 repo = _init_repo(tmp_path)
244 r = _invoke(repo, "maintenance", "run", "--json")
245 json.loads(r.output) # must not raise
246
247 def test_status_json_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
248 repo = _init_repo(tmp_path)
249 r = _invoke(repo, "maintenance", "status", "--json")
250 json.loads(r.output) # must not raise
251
252 def test_schedule_json_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
253 repo = _init_repo(tmp_path)
254 r = _invoke(repo, "maintenance", "schedule", "--json")
255 json.loads(r.output) # must not raise
256
257 def test_no_emoji_in_run_json_stdout(self, tmp_path: pathlib.Path) -> None:
258 repo = _init_repo(tmp_path)
259 r = _invoke(repo, "maintenance", "run", "--json")
260 assert "❌" not in r.output
261 assert "✅" not in r.output
262
263 def test_no_traceback_in_run_json(self, tmp_path: pathlib.Path) -> None:
264 repo = _init_repo(tmp_path)
265 r = _invoke(repo, "maintenance", "run", "--json")
266 assert "Traceback" not in r.output
267
268
269 # ---------------------------------------------------------------------------
270 # TypedDicts
271 # ---------------------------------------------------------------------------
272
273 class TestTypedDicts:
274 def test_maintenance_run_json_exists(self) -> None:
275 from muse.cli.commands.maintenance import _MaintenanceRunJson
276 assert _MaintenanceRunJson is not None
277
278 def test_maintenance_status_json_exists(self) -> None:
279 from muse.cli.commands.maintenance import _MaintenanceStatusJson
280 assert _MaintenanceStatusJson is not None
281
282 def test_maintenance_schedule_json_exists(self) -> None:
283 from muse.cli.commands.maintenance import _MaintenanceScheduleJson
284 assert _MaintenanceScheduleJson is not None
285
286 def test_maintenance_error_json_exists(self) -> None:
287 from muse.cli.commands.maintenance import _MaintenanceErrorJson
288 assert _MaintenanceErrorJson is not None
289
290 def test_run_json_has_required_annotations(self) -> None:
291 from muse.cli.commands.maintenance import _MaintenanceRunJson
292 hints = get_type_hints(_MaintenanceRunJson)
293 for field in ("status", "error", "duration_ms", "exit_code"):
294 assert field in hints, f"Missing annotation: {field!r}"
295
296 def test_status_json_has_required_annotations(self) -> None:
297 from muse.cli.commands.maintenance import _MaintenanceStatusJson
298 hints = get_type_hints(_MaintenanceStatusJson)
299 for field in ("status", "error", "exit_code"):
300 assert field in hints, f"Missing annotation: {field!r}"
301
302 def test_schedule_json_has_required_annotations(self) -> None:
303 from muse.cli.commands.maintenance import _MaintenanceScheduleJson
304 hints = get_type_hints(_MaintenanceScheduleJson)
305 for field in ("status", "error", "enabled", "period_hours", "exit_code"):
306 assert field in hints, f"Missing annotation: {field!r}"
307
308
309 # ---------------------------------------------------------------------------
310 # Docstring coverage
311 # ---------------------------------------------------------------------------
312
313 class TestDocstring:
314 def _doc(self) -> str:
315 import muse.cli.commands.maintenance as mod
316 return mod.__doc__ or ""
317
318 def test_docstring_documents_status(self) -> None:
319 assert "status" in self._doc()
320
321 def test_docstring_documents_error(self) -> None:
322 assert "error" in self._doc()
323
324 def test_docstring_documents_exit_code(self) -> None:
325 assert "exit_code" in self._doc()
326
327 def test_docstring_documents_duration_ms(self) -> None:
328 assert "duration_ms" in self._doc()
329
330 def test_docstring_documents_error_schema(self) -> None:
331 doc = self._doc()
332 assert "error" in doc and "exit_code" in doc
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 140 days ago