gabriel / muse public
test_maintenance_supercharge.py python
365 lines 14.7 KB
Raw
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor ⚠ breaking 121 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 argparse
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, blob_id
26 from muse.core.paths import muse_dir
27 from tests.cli_test_helper import CliRunner, InvokeResult
28
29 runner = CliRunner()
30 _REPO_ID = "maintenance-sg"
31
32
33 # ---------------------------------------------------------------------------
34 # Helpers
35 # ---------------------------------------------------------------------------
36
37
38
39 def _init_repo(path: pathlib.Path) -> pathlib.Path:
40 dot_muse = muse_dir(path)
41 for d in ("commits", "snapshots", "objects", "refs/heads", "code"):
42 (dot_muse / d).mkdir(parents=True, exist_ok=True)
43 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
44 (dot_muse / "repo.json").write_text(
45 json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8"
46 )
47 return path
48
49
50 def _write_obj(repo: pathlib.Path, content: bytes) -> str:
51 oid = blob_id(content)
52 write_object(repo, oid, content)
53 return oid
54
55
56 def _invoke(repo: pathlib.Path, *args: str) -> InvokeResult:
57 from muse.cli.app import main as cli
58 return runner.invoke(cli, list(args), env={"MUSE_REPO_ROOT": str(repo)})
59
60
61 # ---------------------------------------------------------------------------
62 # run --json envelope
63 # ---------------------------------------------------------------------------
64
65 class TestRunJsonEnvelope:
66 """``maintenance run --json`` envelope has all required fields."""
67
68 _REQUIRED = {"status", "error", "tasks_run", "results", "dry_run", "duration_ms", "exit_code"}
69
70 def test_all_required_keys_present(self, tmp_path: pathlib.Path) -> None:
71 repo = _init_repo(tmp_path)
72 r = _invoke(repo, "maintenance", "run", "--json")
73 assert r.exit_code == 0
74 d = json.loads(r.output)
75 missing = self._REQUIRED - d.keys()
76 assert not missing, f"Missing keys: {missing}"
77
78 def test_status_ok_on_success(self, tmp_path: pathlib.Path) -> None:
79 repo = _init_repo(tmp_path)
80 r = _invoke(repo, "maintenance", "run", "--json")
81 assert json.loads(r.output)["status"] == "ok"
82
83 def test_error_empty_on_success(self, tmp_path: pathlib.Path) -> None:
84 repo = _init_repo(tmp_path)
85 r = _invoke(repo, "maintenance", "run", "--json")
86 assert json.loads(r.output)["error"] == ""
87
88 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
89 repo = _init_repo(tmp_path)
90 r = _invoke(repo, "maintenance", "run", "--json")
91 assert json.loads(r.output)["exit_code"] == 0
92
93 def test_duration_ms_is_nonneg_float(self, tmp_path: pathlib.Path) -> None:
94 repo = _init_repo(tmp_path)
95 r = _invoke(repo, "maintenance", "run", "--json")
96 d = json.loads(r.output)
97 assert isinstance(d["duration_ms"], float)
98 assert d["duration_ms"] >= 0.0
99
100 def test_no_elapsed_ms_key(self, tmp_path: pathlib.Path) -> None:
101 """Deprecated elapsed_ms replaced by duration_ms for consistency."""
102 repo = _init_repo(tmp_path)
103 r = _invoke(repo, "maintenance", "run", "--json")
104 d = json.loads(r.output)
105 assert "elapsed_ms" not in d
106
107 def test_dry_run_flag_reflected(self, tmp_path: pathlib.Path) -> None:
108 repo = _init_repo(tmp_path)
109 r = _invoke(repo, "maintenance", "run", "--dry-run", "--json")
110 d = json.loads(r.output)
111 assert d["dry_run"] is True
112
113 def test_tasks_run_is_list(self, tmp_path: pathlib.Path) -> None:
114 repo = _init_repo(tmp_path)
115 r = _invoke(repo, "maintenance", "run", "--task", "gc", "--json")
116 d = json.loads(r.output)
117 assert isinstance(d["tasks_run"], list)
118 assert "gc" in d["tasks_run"]
119
120
121 # ---------------------------------------------------------------------------
122 # status --json envelope
123 # ---------------------------------------------------------------------------
124
125 class TestStatusJsonEnvelope:
126 """``maintenance status --json`` envelope has all required fields."""
127
128 _REQUIRED = {"status", "error", "enabled", "period_hours", "tasks", "last_run", "exit_code"}
129
130 def test_all_required_keys_present(self, tmp_path: pathlib.Path) -> None:
131 repo = _init_repo(tmp_path)
132 r = _invoke(repo, "maintenance", "status", "--json")
133 assert r.exit_code == 0
134 d = json.loads(r.output)
135 missing = self._REQUIRED - d.keys()
136 assert not missing, f"Missing keys: {missing}"
137
138 def test_status_ok_on_success(self, tmp_path: pathlib.Path) -> None:
139 repo = _init_repo(tmp_path)
140 r = _invoke(repo, "maintenance", "status", "--json")
141 assert json.loads(r.output)["status"] == "ok"
142
143 def test_error_empty_on_success(self, tmp_path: pathlib.Path) -> None:
144 repo = _init_repo(tmp_path)
145 r = _invoke(repo, "maintenance", "status", "--json")
146 assert json.loads(r.output)["error"] == ""
147
148 def test_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
149 repo = _init_repo(tmp_path)
150 r = _invoke(repo, "maintenance", "status", "--json")
151 assert json.loads(r.output)["exit_code"] == 0
152
153
154 # ---------------------------------------------------------------------------
155 # schedule --json
156 # ---------------------------------------------------------------------------
157
158 class TestScheduleJson:
159 """``maintenance schedule --json`` emits a structured result."""
160
161 _REQUIRED = {"status", "error", "enabled", "period_hours", "exit_code"}
162
163 def test_schedule_json_flag_accepted(self, tmp_path: pathlib.Path) -> None:
164 repo = _init_repo(tmp_path)
165 r = _invoke(repo, "maintenance", "schedule", "--period-hours", "48", "--json")
166 assert r.exit_code == 0
167
168 def test_schedule_json_all_required_keys(self, tmp_path: pathlib.Path) -> None:
169 repo = _init_repo(tmp_path)
170 r = _invoke(repo, "maintenance", "schedule", "--json")
171 assert r.exit_code == 0
172 d = json.loads(r.output)
173 missing = self._REQUIRED - d.keys()
174 assert not missing, f"Missing keys: {missing}"
175
176 def test_schedule_json_status_ok(self, tmp_path: pathlib.Path) -> None:
177 repo = _init_repo(tmp_path)
178 r = _invoke(repo, "maintenance", "schedule", "--json")
179 assert json.loads(r.output)["status"] == "ok"
180
181 def test_schedule_json_reflects_period_hours(self, tmp_path: pathlib.Path) -> None:
182 repo = _init_repo(tmp_path)
183 r = _invoke(repo, "maintenance", "schedule", "--period-hours", "72", "--json")
184 d = json.loads(r.output)
185 assert d["period_hours"] == 72
186
187 def test_schedule_json_reflects_enabled_true(self, tmp_path: pathlib.Path) -> None:
188 repo = _init_repo(tmp_path)
189 r = _invoke(repo, "maintenance", "schedule", "--enable", "--json")
190 d = json.loads(r.output)
191 assert d["enabled"] is True
192
193 def test_schedule_json_reflects_enabled_false(self, tmp_path: pathlib.Path) -> None:
194 repo = _init_repo(tmp_path)
195 r = _invoke(repo, "maintenance", "schedule", "--disable", "--json")
196 d = json.loads(r.output)
197 assert d["enabled"] is False
198
199 def test_schedule_json_exit_code_zero(self, tmp_path: pathlib.Path) -> None:
200 repo = _init_repo(tmp_path)
201 r = _invoke(repo, "maintenance", "schedule", "--json")
202 assert json.loads(r.output)["exit_code"] == 0
203
204
205 # ---------------------------------------------------------------------------
206 # OID integrity — verify-objects failures list
207 # ---------------------------------------------------------------------------
208
209 class TestVerifyObjectsOidIntegrity:
210 """Failure OIDs in verify-objects results carry the sha256: prefix."""
211
212 def test_failures_list_uses_sha256_prefix(self, tmp_path: pathlib.Path) -> None:
213 """When an object is corrupt, its ID in the failures list is sha256:-prefixed."""
214 repo = _init_repo(tmp_path)
215 oid = _write_obj(repo, b"original content")
216 obj_path = object_path(repo, oid)
217 obj_path.chmod(0o644)
218 obj_path.write_bytes(b"corrupted data")
219
220 r = _invoke(repo, "maintenance", "run", "--task", "verify-objects", "--json")
221 d = json.loads(r.output)
222 failures = d["results"]["verify-objects"]["failures"]
223 assert failures, "Expected at least one failure entry"
224 for fid in failures:
225 assert fid.startswith("sha256:"), f"Failure OID not prefixed: {fid!r}"
226
227 def test_clean_store_failures_list_empty(self, tmp_path: pathlib.Path) -> None:
228 repo = _init_repo(tmp_path)
229 for i in range(3):
230 _write_obj(repo, f"clean-{i}".encode())
231 r = _invoke(repo, "maintenance", "run", "--task", "verify-objects", "--json")
232 d = json.loads(r.output)
233 assert d["results"]["verify-objects"]["failures"] == []
234
235
236 # ---------------------------------------------------------------------------
237 # No-prose pollution
238 # ---------------------------------------------------------------------------
239
240 class TestNoProsePollution:
241 def test_run_json_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
242 repo = _init_repo(tmp_path)
243 r = _invoke(repo, "maintenance", "run", "--json")
244 json.loads(r.output) # must not raise
245
246 def test_status_json_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
247 repo = _init_repo(tmp_path)
248 r = _invoke(repo, "maintenance", "status", "--json")
249 json.loads(r.output) # must not raise
250
251 def test_schedule_json_stdout_is_valid_json(self, tmp_path: pathlib.Path) -> None:
252 repo = _init_repo(tmp_path)
253 r = _invoke(repo, "maintenance", "schedule", "--json")
254 json.loads(r.output) # must not raise
255
256 def test_no_emoji_in_run_json_stdout(self, tmp_path: pathlib.Path) -> None:
257 repo = _init_repo(tmp_path)
258 r = _invoke(repo, "maintenance", "run", "--json")
259 assert "❌" not in r.output
260 assert "✅" not in r.output
261
262 def test_no_traceback_in_run_json(self, tmp_path: pathlib.Path) -> None:
263 repo = _init_repo(tmp_path)
264 r = _invoke(repo, "maintenance", "run", "--json")
265 assert "Traceback" not in r.output
266
267
268 # ---------------------------------------------------------------------------
269 # TypedDicts
270 # ---------------------------------------------------------------------------
271
272 class TestTypedDicts:
273 def test_maintenance_run_json_exists(self) -> None:
274 from muse.cli.commands.maintenance import _MaintenanceRunJson
275 assert _MaintenanceRunJson is not None
276
277 def test_maintenance_status_json_exists(self) -> None:
278 from muse.cli.commands.maintenance import _MaintenanceStatusJson
279 assert _MaintenanceStatusJson is not None
280
281 def test_maintenance_schedule_json_exists(self) -> None:
282 from muse.cli.commands.maintenance import _MaintenanceScheduleJson
283 assert _MaintenanceScheduleJson is not None
284
285 def test_maintenance_error_json_exists(self) -> None:
286 from muse.cli.commands.maintenance import _MaintenanceErrorJson
287 assert _MaintenanceErrorJson is not None
288
289 def test_run_json_has_required_annotations(self) -> None:
290 from muse.cli.commands.maintenance import _MaintenanceRunJson
291 hints = get_type_hints(_MaintenanceRunJson)
292 for field in ("status", "error", "duration_ms", "exit_code"):
293 assert field in hints, f"Missing annotation: {field!r}"
294
295 def test_status_json_has_required_annotations(self) -> None:
296 from muse.cli.commands.maintenance import _MaintenanceStatusJson
297 hints = get_type_hints(_MaintenanceStatusJson)
298 for field in ("status", "error", "exit_code"):
299 assert field in hints, f"Missing annotation: {field!r}"
300
301 def test_schedule_json_has_required_annotations(self) -> None:
302 from muse.cli.commands.maintenance import _MaintenanceScheduleJson
303 hints = get_type_hints(_MaintenanceScheduleJson)
304 for field in ("status", "error", "enabled", "period_hours", "exit_code"):
305 assert field in hints, f"Missing annotation: {field!r}"
306
307
308 # ---------------------------------------------------------------------------
309 # Docstring coverage
310 # ---------------------------------------------------------------------------
311
312 class TestDocstring:
313 def _doc(self) -> str:
314 import muse.cli.commands.maintenance as mod
315 return mod.__doc__ or ""
316
317 def test_docstring_documents_status(self) -> None:
318 assert "status" in self._doc()
319
320 def test_docstring_documents_error(self) -> None:
321 assert "error" in self._doc()
322
323 def test_docstring_documents_exit_code(self) -> None:
324 assert "exit_code" in self._doc()
325
326 def test_docstring_documents_duration_ms(self) -> None:
327 assert "duration_ms" in self._doc()
328
329 def test_docstring_documents_error_schema(self) -> None:
330 doc = self._doc()
331 assert "error" in doc and "exit_code" in doc
332
333
334 # ---------------------------------------------------------------------------
335 # TestRegisterFlags — argparse-level verification
336 # ---------------------------------------------------------------------------
337
338
339 class TestRegisterFlags:
340 """Verify that register() wires --json / -j correctly."""
341
342 def _make_parser(self) -> "argparse.ArgumentParser":
343 import argparse
344 from muse.cli.commands.maintenance import register
345 ap = argparse.ArgumentParser()
346 subs = ap.add_subparsers()
347 register(subs)
348 return ap
349
350 def test_json_flag_long(self) -> None:
351 ns = self._make_parser().parse_args(["maintenance", "run", "--json"])
352 assert ns.json_out is True
353
354 def test_j_alias(self) -> None:
355 ns = self._make_parser().parse_args(["maintenance", "run", "-j"])
356 assert ns.json_out is True
357
358 def test_default_is_text(self) -> None:
359 ns = self._make_parser().parse_args(["maintenance", "run"])
360 assert ns.json_out is False
361
362 def test_dest_is_json_out(self) -> None:
363 ns = self._make_parser().parse_args(["maintenance", "run", "-j"])
364 assert hasattr(ns, "json_out")
365 assert not hasattr(ns, "fmt")
File History 1 commit
sha256:51ce277f663e01a43eaffbe77509b1de7ac2d4251b55d23306304bcdeb92c90d feat(pack): delta-encode snapshots in MPackBundle wire format Sonnet 4.6 minor 121 days ago