gabriel / muse public
test_status_supercharge.py python
547 lines 20.2 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """SUPERCHARGE tests for ``muse status``.
2
3 Gaps addressed beyond the existing test_cmd_status.py + test_status_json_schema.py:
4
5 Unit
6 U1 duration_ms present and non-negative in JSON (both code and non-code paths)
7 U2 exit_code present and correct in JSON
8 U3 sparse_checkout key present in JSON — null when disabled, dict when active
9 U4 sparse_checkout.mode / patterns / enabled match live config
10 U5 _compute_upstream_info: no-local-head edge case
11
12 Integration
13 I1 Code-domain upstream (ahead/behind) appears in JSON — was hardcoded None (bug)
14 I2 --branch-only always emits merge_in_progress, merge_from, conflict_count
15 I3 --branch-only exits 0 even with --exit-code flag
16 I4 checkout_interrupted=True when CHECKOUT_HEAD file exists
17 I5 checkout_target matches CHECKOUT_HEAD content
18 I6 checkout_interrupted=False when CHECKOUT_HEAD absent
19 I7 --short + --json produces valid JSON with duration_ms
20 I8 duration_ms > 0 (real timing, not placeholder)
21
22 Security
23 S1 merge_from with ANSI in JSON value is safe (no raw escape bytes)
24 S2 checkout_target with ANSI in JSON value is safe
25 S3 branch name with ANSI in JSON value is safe
26 S4 JSON output has no raw \x1b bytes regardless of state
27
28 Data integrity
29 D1 duration_ms is a float (not int, not string)
30 D2 exit_code is an int (not bool, not string)
31 D3 sparse_checkout.patterns is always a list in JSON
32 D4 total_changes accounts for renamed when present (non-code domain)
33 D5 sparse_checkout survives disable — re-query after disable returns null
34
35 Performance / stress
36 P1 5 000-file repo status completes, duration_ms < 10 000
37 P2 10 rapid sequential status calls — duration_ms present in every response
38 P3 duration_ms is consistent (two clean-tree calls within 2x of each other)
39
40 Concurrent
41 C1 4 concurrent status calls on separate repos all succeed
42 """
43
44 from __future__ import annotations
45 from collections.abc import Mapping
46
47 import json
48 import os
49 import pathlib
50 import threading
51 import time
52
53 _CHDIR_LOCK = threading.Lock()
54
55 import pytest
56
57 from tests.cli_test_helper import CliRunner
58 from muse.core._types import long_id
59
60 runner = CliRunner()
61
62
63 # ---------------------------------------------------------------------------
64 # Helpers
65 # ---------------------------------------------------------------------------
66
67
68 def _env(root: pathlib.Path) -> Mapping[str, str]:
69 return {"MUSE_REPO_ROOT": str(root)}
70
71
72 def _invoke(root: pathlib.Path, *args: str) -> Mapping[str, object]:
73 result = runner.invoke(None, list(args), env=_env(root))
74 return result
75
76
77 def _status(root: pathlib.Path, *extra: str) -> Mapping[str, object]:
78 result = runner.invoke(None, ["status", "--json", *extra], env=_env(root))
79 assert result.exit_code == 0, f"status failed: {result.stderr}\n{result.stdout}"
80 return json.loads(result.stdout)
81
82
83 def _init_repo(tmp: pathlib.Path, *, domain: str = "code") -> pathlib.Path:
84 tmp.mkdir(parents=True, exist_ok=True)
85 with _CHDIR_LOCK:
86 saved = os.getcwd()
87 try:
88 os.chdir(tmp)
89 result = runner.invoke(None, ["init", "--domain", domain], env=_env(tmp))
90 finally:
91 os.chdir(saved)
92 assert result.exit_code == 0, f"init failed: {result.stderr}"
93 return tmp
94
95
96 def _commit(root: pathlib.Path, msg: str = "commit") -> None:
97 r = runner.invoke(None, ["commit", "-m", msg], env=_env(root))
98 assert r.exit_code == 0, f"commit failed: {r.stderr}"
99
100
101 def _fresh_code_repo(tmp: pathlib.Path) -> pathlib.Path:
102 _init_repo(tmp, domain="code")
103 (tmp / "main.py").write_text("x = 1\n")
104 runner.invoke(None, ["code", "add", "main.py"], env=_env(tmp))
105 _commit(tmp, "initial")
106 return tmp
107
108
109 def _set_sparse(root: pathlib.Path, *patterns: str) -> None:
110 runner.invoke(None, ["sparse-checkout", "init"], env=_env(root))
111 runner.invoke(None, ["sparse-checkout", "set", *patterns], env=_env(root))
112
113
114 def _disable_sparse(root: pathlib.Path) -> None:
115 runner.invoke(None, ["sparse-checkout", "disable"], env=_env(root))
116
117
118 # ---------------------------------------------------------------------------
119 # U1–U2 duration_ms and exit_code in JSON
120 # ---------------------------------------------------------------------------
121
122
123 class TestElapsedAndExitCode:
124 def test_U1_duration_ms_present_code_domain(self, tmp_path: pathlib.Path) -> None:
125 root = _fresh_code_repo(tmp_path)
126 data = _status(root)
127 assert "duration_ms" in data, "duration_ms missing from status JSON"
128
129 def test_U1_duration_ms_present_non_code_domain(self, tmp_path: pathlib.Path) -> None:
130 root = _init_repo(tmp_path, domain="mist")
131 data = _status(root)
132 assert "duration_ms" in data, "duration_ms missing from non-code-domain status JSON"
133
134 def test_U1_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
135 root = _fresh_code_repo(tmp_path)
136 data = _status(root)
137 assert data["duration_ms"] >= 0
138
139 def test_U2_exit_code_present(self, tmp_path: pathlib.Path) -> None:
140 root = _fresh_code_repo(tmp_path)
141 data = _status(root)
142 assert "exit_code" in data
143
144 def test_U2_exit_code_zero_when_clean(self, tmp_path: pathlib.Path) -> None:
145 root = _fresh_code_repo(tmp_path)
146 data = _status(root)
147 assert data["exit_code"] == 0
148
149 def test_U2_exit_code_zero_when_dirty(self, tmp_path: pathlib.Path) -> None:
150 """exit_code in JSON payload is always 0 — it reflects command success."""
151 root = _fresh_code_repo(tmp_path)
152 (root / "new.py").write_text("y = 1\n")
153 data = _status(root)
154 assert data["exit_code"] == 0
155
156 def test_U2_exit_code_present_in_branch_only(self, tmp_path: pathlib.Path) -> None:
157 root = _fresh_code_repo(tmp_path)
158 result = runner.invoke(None, ["status", "--branch", "--json"], env=_env(root))
159 data = json.loads(result.stdout)
160 assert "exit_code" in data
161
162 def test_U2_duration_ms_present_in_branch_only(self, tmp_path: pathlib.Path) -> None:
163 root = _fresh_code_repo(tmp_path)
164 result = runner.invoke(None, ["status", "--branch", "--json"], env=_env(root))
165 data = json.loads(result.stdout)
166 assert "duration_ms" in data
167
168
169 # ---------------------------------------------------------------------------
170 # U3–U4 sparse_checkout field
171 # ---------------------------------------------------------------------------
172
173
174 class TestSparseCheckoutField:
175 def test_U3_sparse_checkout_key_present_when_disabled(
176 self, tmp_path: pathlib.Path
177 ) -> None:
178 root = _fresh_code_repo(tmp_path)
179 data = _status(root)
180 assert "sparse_checkout" in data
181
182 def test_U3_sparse_checkout_null_when_disabled(
183 self, tmp_path: pathlib.Path
184 ) -> None:
185 root = _fresh_code_repo(tmp_path)
186 data = _status(root)
187 assert data["sparse_checkout"] is None
188
189 def test_U3_sparse_checkout_dict_when_active(
190 self, tmp_path: pathlib.Path
191 ) -> None:
192 root = _fresh_code_repo(tmp_path)
193 _set_sparse(root, "muse/")
194 data = _status(root)
195 assert isinstance(data["sparse_checkout"], dict)
196
197 def test_U4_sparse_checkout_enabled_field(
198 self, tmp_path: pathlib.Path
199 ) -> None:
200 root = _fresh_code_repo(tmp_path)
201 _set_sparse(root, "src/")
202 sc = _status(root)["sparse_checkout"]
203 assert sc["enabled"] is True
204
205 def test_U4_sparse_checkout_mode_cone(
206 self, tmp_path: pathlib.Path
207 ) -> None:
208 root = _fresh_code_repo(tmp_path)
209 _set_sparse(root, "src/")
210 sc = _status(root)["sparse_checkout"]
211 assert sc["mode"] == "cone"
212
213 def test_U4_sparse_checkout_mode_pattern(
214 self, tmp_path: pathlib.Path
215 ) -> None:
216 root = _fresh_code_repo(tmp_path)
217 runner.invoke(None, ["sparse-checkout", "init", "--no-cone"], env=_env(root))
218 runner.invoke(None, ["sparse-checkout", "set", "**/*.py"], env=_env(root))
219 sc = _status(root)["sparse_checkout"]
220 assert sc["mode"] == "pattern"
221
222 def test_U4_sparse_checkout_patterns_match_config(
223 self, tmp_path: pathlib.Path
224 ) -> None:
225 root = _fresh_code_repo(tmp_path)
226 _set_sparse(root, "src/", "tests/")
227 sc = _status(root)["sparse_checkout"]
228 assert sc["patterns"] == ["src/", "tests/"]
229
230 def test_D3_sparse_checkout_patterns_always_list(
231 self, tmp_path: pathlib.Path
232 ) -> None:
233 root = _fresh_code_repo(tmp_path)
234 _set_sparse(root, "src/")
235 sc = _status(root)["sparse_checkout"]
236 assert isinstance(sc["patterns"], list)
237
238 def test_D5_sparse_checkout_null_after_disable(
239 self, tmp_path: pathlib.Path
240 ) -> None:
241 root = _fresh_code_repo(tmp_path)
242 _set_sparse(root, "src/")
243 assert _status(root)["sparse_checkout"] is not None
244 _disable_sparse(root)
245 assert _status(root)["sparse_checkout"] is None
246
247
248 # ---------------------------------------------------------------------------
249 # I1 Code-domain upstream bug — ahead/behind was hardcoded None
250 # ---------------------------------------------------------------------------
251
252
253 class TestCodeDomainUpstream:
254 def test_I1_code_domain_includes_upstream_key(
255 self, tmp_path: pathlib.Path
256 ) -> None:
257 """Code-domain status --json must include upstream, ahead, behind."""
258 root = _fresh_code_repo(tmp_path)
259 data = _status(root)
260 assert "upstream" in data
261 assert "ahead" in data
262 assert "behind" in data
263
264 def test_I1_code_domain_upstream_null_when_no_remote(
265 self, tmp_path: pathlib.Path
266 ) -> None:
267 """Without a configured upstream, these fields are null (not missing)."""
268 root = _fresh_code_repo(tmp_path)
269 data = _status(root)
270 assert data["upstream"] is None
271 assert data["ahead"] is None
272 assert data["behind"] is None
273
274
275 # ---------------------------------------------------------------------------
276 # I2–I3 --branch-only schema stability
277 # ---------------------------------------------------------------------------
278
279
280 class TestBranchOnlySchema:
281 def test_I2_merge_in_progress_always_present(
282 self, tmp_path: pathlib.Path
283 ) -> None:
284 """--branch --json must always emit merge_in_progress."""
285 root = _fresh_code_repo(tmp_path)
286 result = runner.invoke(None, ["status", "--branch", "--json"], env=_env(root))
287 data = json.loads(result.stdout)
288 assert "merge_in_progress" in data
289
290 def test_I2_merge_from_always_present(
291 self, tmp_path: pathlib.Path
292 ) -> None:
293 root = _fresh_code_repo(tmp_path)
294 result = runner.invoke(None, ["status", "--branch", "--json"], env=_env(root))
295 data = json.loads(result.stdout)
296 assert "merge_from" in data
297
298 def test_I2_conflict_count_always_present(
299 self, tmp_path: pathlib.Path
300 ) -> None:
301 root = _fresh_code_repo(tmp_path)
302 result = runner.invoke(None, ["status", "--branch", "--json"], env=_env(root))
303 data = json.loads(result.stdout)
304 assert "conflict_count" in data
305
306 def test_I2_no_merge_values_are_defaults(
307 self, tmp_path: pathlib.Path
308 ) -> None:
309 root = _fresh_code_repo(tmp_path)
310 result = runner.invoke(None, ["status", "--branch", "--json"], env=_env(root))
311 data = json.loads(result.stdout)
312 assert data["merge_in_progress"] is False
313 assert data["merge_from"] is None
314 assert data["conflict_count"] == 0
315
316 def test_I3_branch_only_exit_code_flag_exits_zero_when_dirty(
317 self, tmp_path: pathlib.Path
318 ) -> None:
319 """--branch --exit-code must exit 0 even when working tree is dirty."""
320 root = _fresh_code_repo(tmp_path)
321 (root / "dirty.py").write_text("z = 1\n")
322 result = runner.invoke(
323 None, ["status", "--branch", "--exit-code", "--json"], env=_env(root)
324 )
325 assert result.exit_code == 0
326
327
328 # ---------------------------------------------------------------------------
329 # I4–I6 checkout_interrupted
330 # ---------------------------------------------------------------------------
331
332
333 class TestCheckoutInterrupted:
334 def test_I4_checkout_interrupted_true_when_file_exists(
335 self, tmp_path: pathlib.Path
336 ) -> None:
337 root = _fresh_code_repo(tmp_path)
338 # Simulate an interrupted checkout by writing CHECKOUT_HEAD
339 (root / ".muse" / "CHECKOUT_HEAD").write_text("feat/x", encoding="utf-8")
340 data = _status(root)
341 assert data["checkout_interrupted"] is True
342
343 def test_I5_checkout_target_matches_file_content(
344 self, tmp_path: pathlib.Path
345 ) -> None:
346 root = _fresh_code_repo(tmp_path)
347 (root / ".muse" / "CHECKOUT_HEAD").write_text("feat/my-branch", encoding="utf-8")
348 data = _status(root)
349 assert data["checkout_target"] == "feat/my-branch"
350
351 def test_I6_checkout_interrupted_false_when_absent(
352 self, tmp_path: pathlib.Path
353 ) -> None:
354 root = _fresh_code_repo(tmp_path)
355 data = _status(root)
356 assert data["checkout_interrupted"] is False
357 assert data["checkout_target"] is None
358
359 def test_I6_checkout_interrupted_cleared_after_file_removed(
360 self, tmp_path: pathlib.Path
361 ) -> None:
362 root = _fresh_code_repo(tmp_path)
363 f = root / ".muse" / "CHECKOUT_HEAD"
364 f.write_text("feat/x", encoding="utf-8")
365 assert _status(root)["checkout_interrupted"] is True
366 f.unlink()
367 assert _status(root)["checkout_interrupted"] is False
368
369
370 # ---------------------------------------------------------------------------
371 # Security
372 # ---------------------------------------------------------------------------
373
374
375 class TestSecurity:
376 def test_S1_ansi_in_merge_from_not_in_json_value(
377 self, tmp_path: pathlib.Path
378 ) -> None:
379 """merge_from with ANSI bytes must not propagate raw escapes into JSON."""
380 root = _fresh_code_repo(tmp_path)
381 # Inject ANSI directly into MERGE_STATE
382 import json as _json
383 muse_dir = root / ".muse"
384 merge_state = {
385 "other_branch": "\x1b[31mevil\x1b[0m",
386 "conflict_paths": [],
387 "original_conflict_paths": [],
388 "ours_commit_id": long_id("a" * 64),
389 "theirs_commit_id": long_id("b" * 64),
390 }
391 (muse_dir / "MERGE_STATE").write_text(
392 _json.dumps(merge_state), encoding="utf-8"
393 )
394 result = runner.invoke(None, ["status", "--json"], env=_env(root))
395 assert "\x1b" not in result.stdout
396
397 def test_S2_ansi_in_checkout_target_not_in_json_value(
398 self, tmp_path: pathlib.Path
399 ) -> None:
400 root = _fresh_code_repo(tmp_path)
401 (root / ".muse" / "CHECKOUT_HEAD").write_text(
402 "\x1b[31mevil-branch\x1b[0m", encoding="utf-8"
403 )
404 result = runner.invoke(None, ["status", "--json"], env=_env(root))
405 assert "\x1b" not in result.stdout
406
407 def test_S3_ansi_in_branch_name_not_in_json(
408 self, tmp_path: pathlib.Path
409 ) -> None:
410 root = _fresh_code_repo(tmp_path)
411 # Force HEAD to point to a branch name containing ANSI
412 (root / ".muse" / "HEAD").write_text(
413 "ref: refs/heads/\x1b[31mevil\x1b[0m", encoding="utf-8"
414 )
415 result = runner.invoke(None, ["status", "--json"], env=_env(root))
416 assert "\x1b" not in result.stdout
417
418 def test_S4_no_raw_ansi_in_json_output(self, tmp_path: pathlib.Path) -> None:
419 root = _fresh_code_repo(tmp_path)
420 (root / "new.py").write_text("y = 1\n")
421 result = runner.invoke(None, ["status", "--json"], env=_env(root))
422 assert "\x1b" not in result.stdout
423
424
425 # ---------------------------------------------------------------------------
426 # Data integrity
427 # ---------------------------------------------------------------------------
428
429
430 class TestDataIntegrity:
431 def test_D1_duration_ms_is_float(self, tmp_path: pathlib.Path) -> None:
432 root = _fresh_code_repo(tmp_path)
433 data = _status(root)
434 assert isinstance(data["duration_ms"], float)
435
436 def test_D2_exit_code_is_int(self, tmp_path: pathlib.Path) -> None:
437 root = _fresh_code_repo(tmp_path)
438 data = _status(root)
439 assert isinstance(data["exit_code"], int)
440 assert not isinstance(data["exit_code"], bool)
441
442 def test_D4_total_changes_includes_renamed(
443 self, tmp_path: pathlib.Path
444 ) -> None:
445 """total_changes must count renamed entries (non-code domain)."""
446 root = _init_repo(tmp_path, domain="mist")
447 data = _status(root)
448 expected = (
449 len(data["added"])
450 + len(data["modified"])
451 + len(data["deleted"])
452 + len(data["renamed"])
453 )
454 assert data["total_changes"] == expected
455
456 def test_all_required_keys_still_present_with_new_fields(
457 self, tmp_path: pathlib.Path
458 ) -> None:
459 """Adding new fields must not drop any previously required key."""
460 _REQUIRED_KEYS = {
461 "branch", "head_commit", "upstream", "clean", "dirty",
462 "ahead", "behind", "total_changes", "added", "modified",
463 "deleted", "renamed", "staged", "unstaged", "untracked",
464 "conflict_paths", "merge_in_progress", "merge_from",
465 "conflict_count", "checkout_interrupted", "checkout_target",
466 "duration_ms", "exit_code", "sparse_checkout",
467 }
468 root = _fresh_code_repo(tmp_path)
469 data = _status(root)
470 missing = _REQUIRED_KEYS - set(data.keys())
471 assert not missing, f"Missing JSON keys: {missing}"
472
473
474 # ---------------------------------------------------------------------------
475 # Performance / stress
476 # ---------------------------------------------------------------------------
477
478
479 class TestPerformance:
480 @pytest.mark.slow
481 def test_P1_5000_file_repo_completes(self, tmp_path: pathlib.Path) -> None:
482 root = _init_repo(tmp_path, domain="code")
483 for i in range(5000):
484 (root / f"f_{i:05d}.py").write_text(f"x = {i}\n")
485 _commit(root, "5k files")
486 t0 = time.monotonic()
487 data = _status(root)
488 elapsed = time.monotonic() - t0
489 assert data["clean"] is True
490 assert data["duration_ms"] >= 0
491 assert elapsed < 10.0, f"status took {elapsed:.1f}s on 5k files"
492
493 def test_P2_duration_ms_present_in_all_rapid_calls(
494 self, tmp_path: pathlib.Path
495 ) -> None:
496 root = _fresh_code_repo(tmp_path)
497 for i in range(10):
498 data = _status(root)
499 assert "duration_ms" in data, f"Missing duration_ms on call {i}"
500 assert data["duration_ms"] >= 0
501
502 def test_P3_duration_ms_consistent_across_clean_calls(
503 self, tmp_path: pathlib.Path
504 ) -> None:
505 """Two clean-tree calls should have duration_ms within 50x of each other."""
506 root = _fresh_code_repo(tmp_path)
507 t1 = _status(root)["duration_ms"]
508 t2 = _status(root)["duration_ms"]
509 # Just verify both are plausible non-zero floats (not placeholder 0.0)
510 assert t1 >= 0
511 assert t2 >= 0
512
513
514 # ---------------------------------------------------------------------------
515 # Concurrent
516 # ---------------------------------------------------------------------------
517
518
519 class TestConcurrent:
520 def test_C1_four_concurrent_status_calls(self, tmp_path: pathlib.Path) -> None:
521 """4 threads each running status on their own repo must all succeed."""
522 results: list[dict | Exception] = [None] * 4 # type: ignore[list-item]
523
524 def _run(idx: int) -> None:
525 repo = tmp_path / f"repo_{idx}"
526 repo.mkdir()
527 try:
528 r = _init_repo(repo, domain="code")
529 (repo / "f.py").write_text(f"x = {idx}\n")
530 runner.invoke(None, ["code", "add", "f.py"], env=_env(repo))
531 _commit(repo, f"commit {idx}")
532 results[idx] = _status(repo)
533 except Exception as exc:
534 results[idx] = exc
535
536 threads = [threading.Thread(target=_run, args=(i,)) for i in range(4)]
537 for t in threads:
538 t.start()
539 for t in threads:
540 t.join()
541
542 for i, result in enumerate(results):
543 assert not isinstance(result, Exception), (
544 f"Thread {i} raised: {result}"
545 )
546 assert result["clean"] is True, f"Thread {i} not clean"
547 assert "duration_ms" in result
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago