gabriel / muse public
test_test_cmd_supercharge.py python
825 lines 37.4 KB
Raw
sha256:cb2da6c61116ad1ab98d03747c21d6f66485839c7b4efd7d0124db0f8aa14e41 refactor(harmony): move auto_apply + record_resolutions int… Sonnet 4.6 minor ⚠ breaking 24 days ago
1 """Seven-tier tests for ``muse/cli/commands/test_cmd.py``.
2
3 Tiers
4 -----
5 Unit — TypedDict fields (_FullJson schema_version); _fatal human/JSON;
6 _progress_cb icons; _history_to_json roundtrip; _gate_to_json;
7 _ci_to_json; _run_result_to_record; _print_history;
8 _print_pre_run; _print_dry_run human/JSON; _print_summary.
9 Integration — -j alias parity; register() has -j; docstrings document
10 schema_version/exit_code/duration_ms.
11 End-to-end — --dry-run (no pytest); --history (no pytest); --flaky (no pytest);
12 --json --dry-run; --json --history; invalid repo; -j --dry-run.
13 Stress — 1 000 _history_to_json calls; 500 _gate_to_json calls;
14 _print_history with 200 entries.
15 Data integrity — JSON fields correct types; schema_version present on all modes;
16 _FullJson required fields; _history_to_json preserves every field.
17 Security — hostile node_id in history survives JSON; ANSI in messages;
18 SQL injection in gate name; long stdout in gate result.
19 Performance — 1 000 _gate_to_json under 0.5 s; duration_ms in --dry-run JSON.
20 """
21
22 from __future__ import annotations
23 from collections.abc import Mapping
24
25 import json
26 import os
27 import pathlib
28 import textwrap
29 import threading
30 import time
31 from typing import get_type_hints
32
33 import pytest
34
35 from tests.cli_test_helper import CliRunner, InvokeResult
36
37 runner = CliRunner()
38
39
40 # ──────────────────────────────────────────────────────────────────────────────
41 # Shared helpers — minimal typed-dict factories
42 # ──────────────────────────────────────────────────────────────────────────────
43
44
45 def _make_history_summary(**kw: str) -> Mapping[str, object]:
46 from muse.core.test_history import HistorySummary
47 base = dict(
48 node_id="tests/test_foo.py::test_bar",
49 total_runs=10,
50 pass_count=8,
51 fail_count=2,
52 skip_count=0,
53 flaky=True,
54 avg_duration_ms=123.4,
55 last_outcome="passed",
56 last_run_timestamp="2026-01-01T00:00:00+00:00",
57 fail_streak=0,
58 )
59 base.update(kw)
60 return HistorySummary(**base)
61
62
63 def _make_gate(**kw: str) -> Mapping[str, object]:
64 from muse.core.ci import GateResult
65 base = dict(
66 name="lint",
67 command=["ruff", "check", "."],
68 exit_code=0,
69 duration_ms=120.0,
70 stdout="All checks passed.",
71 stderr="",
72 required=True,
73 passed=True,
74 timed_out=False,
75 )
76 base.update(kw)
77 return GateResult(**base)
78
79
80 def _make_ci_result(**kw: str) -> Mapping[str, object]:
81 from muse.core.ci import CiRunResult
82 base = dict(
83 passed=True,
84 gates=[_make_gate()],
85 total_duration_ms=200.0,
86 timestamp="2026-01-01T00:00:00+00:00",
87 )
88 base.update(kw)
89 return CiRunResult(**base)
90
91
92 def _make_run_result(**kw: str) -> Mapping[str, object]:
93 from muse.core.test_runner import RunResult
94 base = dict(
95 run_id="run-1",
96 targets=[],
97 exit_code=0,
98 duration_ms=500.0,
99 results=[],
100 total=3,
101 passed=3,
102 failed=0,
103 errored=0,
104 skipped=0,
105 timed_out=False,
106 json_report_available=True,
107 stdout="",
108 stderr="",
109 )
110 base.update(kw)
111 return RunResult(**base)
112
113
114 def _make_selection(**kw: str) -> Mapping[str, object]:
115 from muse.core.test_selection import SelectionResult
116 base = dict(
117 changed_addresses=["src/foo.py::bar"],
118 test_targets=[],
119 covered_addresses=["src/foo.py::bar"],
120 uncovered_addresses=[],
121 coverage_fraction=1.0,
122 fallback_used=False,
123 )
124 base.update(kw)
125 return SelectionResult(**base)
126
127
128 def _commit(repo: pathlib.Path, files: Mapping[str, str], message: str) -> None:
129 for name, content in files.items():
130 path = repo / name
131 path.parent.mkdir(parents=True, exist_ok=True)
132 path.write_text(content, encoding="utf-8")
133 saved = os.getcwd()
134 try:
135 os.chdir(repo)
136 runner.invoke(None, ["code", "add", "."])
137 runner.invoke(None, ["commit", "-m", message])
138 finally:
139 os.chdir(saved)
140
141
142 def _invoke(repo: pathlib.Path, args: list[str]) -> InvokeResult:
143 saved = os.getcwd()
144 try:
145 os.chdir(repo)
146 return runner.invoke(None, args)
147 finally:
148 os.chdir(saved)
149
150
151 @pytest.fixture()
152 def test_repo(tmp_path: pathlib.Path) -> pathlib.Path:
153 """Minimal repo with a real test file so history/dry-run modes work."""
154 saved = os.getcwd()
155 try:
156 os.chdir(tmp_path)
157 runner.invoke(None, ["init"])
158 finally:
159 os.chdir(saved)
160
161 _commit(tmp_path, {
162 "src/calc.py": textwrap.dedent("""\
163 def add(a, b):
164 return a + b
165 """),
166 "tests/test_calc.py": textwrap.dedent("""\
167 from src.calc import add
168
169 def test_add():
170 assert add(1, 2) == 3
171 """),
172 }, "feat: add calc and test")
173
174 return tmp_path
175
176
177 # ──────────────────────────────────────────────────────────────────────────────
178 # Unit — TypedDict / _FullJson schema_version
179 # ──────────────────────────────────────────────────────────────────────────────
180
181
182 class TestTypedDict:
183 def test_full_json_has_schema_version(self) -> None:
184 from muse.cli.commands.test_cmd import _FullJson
185 assert "schema" in get_type_hints(_FullJson)
186
187 def test_full_json_has_mode(self) -> None:
188 from muse.cli.commands.test_cmd import _FullJson
189 assert "mode" in get_type_hints(_FullJson)
190
191 def test_selection_json_fields(self) -> None:
192 from muse.cli.commands.test_cmd import _SelectionJson
193 hints = get_type_hints(_SelectionJson)
194 for f in ("changed_addresses", "covered_addresses", "uncovered_addresses",
195 "coverage_fraction", "fallback_used", "targets"):
196 assert f in hints, f"missing: {f}"
197
198 def test_run_json_has_exit_code(self) -> None:
199 from muse.cli.commands.test_cmd import _RunJson
200 assert "exit_code" in get_type_hints(_RunJson)
201
202 def test_run_json_has_duration_ms(self) -> None:
203 from muse.cli.commands.test_cmd import _RunJson
204 assert "duration_ms" in get_type_hints(_RunJson)
205
206 def test_history_json_fields(self) -> None:
207 from muse.cli.commands.test_cmd import _HistoryJson
208 hints = get_type_hints(_HistoryJson)
209 for f in ("node_id", "total_runs", "pass_count", "fail_count",
210 "flaky", "avg_duration_ms", "fail_streak"):
211 assert f in hints, f"missing: {f}"
212
213 def test_ci_gate_json_fields(self) -> None:
214 from muse.cli.commands.test_cmd import _CiGateJson
215 hints = get_type_hints(_CiGateJson)
216 for f in ("name", "command", "exit_code", "duration_ms",
217 "required", "passed", "timed_out"):
218 assert f in hints, f"missing: {f}"
219
220
221 # ──────────────────────────────────────────────────────────────────────────────
222 # Unit — _fatal
223 # ──────────────────────────────────────────────────────────────────────────────
224
225
226 class TestFatal:
227 def test_human_mode_exits_1(self, capsys: pytest.CaptureFixture[str]) -> None:
228 from muse.cli.commands.test_cmd import _fatal
229 with pytest.raises(SystemExit) as exc_info:
230 _fatal("something broke", json_out=False)
231 assert exc_info.value.code == 1
232
233 def test_human_mode_prints_to_stderr(self, capsys: pytest.CaptureFixture[str]) -> None:
234 from muse.cli.commands.test_cmd import _fatal
235 with pytest.raises(SystemExit):
236 _fatal("something broke", json_out=False)
237 assert "something broke" in capsys.readouterr().err
238
239 def test_json_mode_prints_error_key(self, capsys: pytest.CaptureFixture[str]) -> None:
240 from muse.cli.commands.test_cmd import _fatal
241 with pytest.raises(SystemExit):
242 _fatal("bad config", json_out=True)
243 d = json.loads(capsys.readouterr().out)
244 assert d["error"] == "bad config"
245
246 def test_json_mode_exits_1(self) -> None:
247 from muse.cli.commands.test_cmd import _fatal
248 with pytest.raises(SystemExit) as exc_info:
249 _fatal("x", json_out=True)
250 assert exc_info.value.code == 1
251
252
253 # ──────────────────────────────────────────────────────────────────────────────
254 # Unit — _progress_cb
255 # ──────────────────────────────────────────────────────────────────────────────
256
257
258 class TestProgressCb:
259 def _case(self, outcome: str) -> Mapping[str, object]:
260 from muse.core.test_runner import CaseResult
261 return CaseResult(node_id="t::t", outcome=outcome, duration_ms=1.0)
262
263 def test_passed_prints_dot(self, capsys: pytest.CaptureFixture[str]) -> None:
264 from muse.cli.commands.test_cmd import _progress_cb
265 _progress_cb(self._case("passed"))
266 assert capsys.readouterr().err == "."
267
268 def test_failed_prints_f(self, capsys: pytest.CaptureFixture[str]) -> None:
269 from muse.cli.commands.test_cmd import _progress_cb
270 _progress_cb(self._case("failed"))
271 assert capsys.readouterr().err == "F"
272
273 def test_error_prints_e(self, capsys: pytest.CaptureFixture[str]) -> None:
274 from muse.cli.commands.test_cmd import _progress_cb
275 _progress_cb(self._case("error"))
276 assert capsys.readouterr().err == "E"
277
278 def test_skipped_prints_s(self, capsys: pytest.CaptureFixture[str]) -> None:
279 from muse.cli.commands.test_cmd import _progress_cb
280 _progress_cb(self._case("skipped"))
281 assert capsys.readouterr().err == "s"
282
283 def test_unknown_outcome_prints_q(self, capsys: pytest.CaptureFixture[str]) -> None:
284 from muse.cli.commands.test_cmd import _progress_cb
285 _progress_cb(self._case("weird"))
286 assert capsys.readouterr().err == "?"
287
288
289 # ──────────────────────────────────────────────────────────────────────────────
290 # Unit — _history_to_json
291 # ──────────────────────────────────────────────────────────────────────────────
292
293
294 class TestHistoryToJson:
295 def test_preserves_node_id(self) -> None:
296 from muse.cli.commands.test_cmd import _history_to_json
297 s = _make_history_summary(node_id="tests/test_foo.py::test_x")
298 d = _history_to_json(s)
299 assert d["node_id"] == "tests/test_foo.py::test_x"
300
301 def test_preserves_counts(self) -> None:
302 from muse.cli.commands.test_cmd import _history_to_json
303 s = _make_history_summary(pass_count=7, fail_count=3, total_runs=10)
304 d = _history_to_json(s)
305 assert d["pass_count"] == 7
306 assert d["fail_count"] == 3
307 assert d["total_runs"] == 10
308
309 def test_preserves_flaky_flag(self) -> None:
310 from muse.cli.commands.test_cmd import _history_to_json
311 d = _history_to_json(_make_history_summary(flaky=True))
312 assert d["flaky"] is True
313
314 def test_preserves_avg_duration_ms(self) -> None:
315 from muse.cli.commands.test_cmd import _history_to_json
316 d = _history_to_json(_make_history_summary(avg_duration_ms=99.9))
317 assert abs(d["avg_duration_ms"] - 99.9) < 0.001
318
319 def test_result_is_json_serialisable(self) -> None:
320 from muse.cli.commands.test_cmd import _history_to_json
321 d = _history_to_json(_make_history_summary())
322 json.dumps(d) # must not raise
323
324
325 # ──────────────────────────────────────────────────────────────────────────────
326 # Unit — _gate_to_json
327 # ──────────────────────────────────────────────────────────────────────────────
328
329
330 class TestGateToJson:
331 def test_preserves_name(self) -> None:
332 from muse.cli.commands.test_cmd import _gate_to_json
333 d = _gate_to_json(_make_gate(name="mygate"))
334 assert d["name"] == "mygate"
335
336 def test_preserves_exit_code(self) -> None:
337 from muse.cli.commands.test_cmd import _gate_to_json
338 d = _gate_to_json(_make_gate(exit_code=1))
339 assert d["exit_code"] == 1
340
341 def test_preserves_passed(self) -> None:
342 from muse.cli.commands.test_cmd import _gate_to_json
343 d = _gate_to_json(_make_gate(passed=False))
344 assert d["passed"] is False
345
346 def test_warning_included_when_present(self) -> None:
347 from muse.cli.commands.test_cmd import _gate_to_json
348 gate = _make_gate()
349 gate["warning"] = "watch out"
350 d = _gate_to_json(gate)
351 assert d["warning"] == "watch out"
352
353 def test_warning_absent_when_not_set(self) -> None:
354 from muse.cli.commands.test_cmd import _gate_to_json
355 d = _gate_to_json(_make_gate())
356 assert "warning" not in d
357
358 def test_result_is_json_serialisable(self) -> None:
359 from muse.cli.commands.test_cmd import _gate_to_json
360 json.dumps(_gate_to_json(_make_gate()))
361
362
363 # ──────────────────────────────────────────────────────────────────────────────
364 # Unit — _ci_to_json
365 # ──────────────────────────────────────────────────────────────────────────────
366
367
368 class TestCiToJson:
369 def test_preserves_passed(self) -> None:
370 from muse.cli.commands.test_cmd import _ci_to_json
371 d = _ci_to_json(_make_ci_result(passed=False))
372 assert d["passed"] is False
373
374 def test_gates_list_length(self) -> None:
375 from muse.cli.commands.test_cmd import _ci_to_json
376 ci = _make_ci_result(gates=[_make_gate(), _make_gate(name="test")])
377 d = _ci_to_json(ci)
378 assert len(d["gates"]) == 2
379
380 def test_result_is_json_serialisable(self) -> None:
381 from muse.cli.commands.test_cmd import _ci_to_json
382 json.dumps(_ci_to_json(_make_ci_result()))
383
384
385 # ──────────────────────────────────────────────────────────────────────────────
386 # Unit — _print_history
387 # ──────────────────────────────────────────────────────────────────────────────
388
389
390 class TestPrintHistory:
391 def test_empty_history_prints_no_history(self, capsys: pytest.CaptureFixture[str]) -> None:
392 from muse.cli.commands.test_cmd import _print_history
393 _print_history({}, flaky_only=False)
394 assert "No test history" in capsys.readouterr().out
395
396 def test_empty_flaky_prints_no_flaky(self, capsys: pytest.CaptureFixture[str]) -> None:
397 from muse.cli.commands.test_cmd import _print_history
398 _print_history({}, flaky_only=True)
399 assert "No flaky" in capsys.readouterr().out
400
401 def test_non_empty_shows_node_id(self, capsys: pytest.CaptureFixture[str]) -> None:
402 from muse.cli.commands.test_cmd import _print_history
403 s = _make_history_summary(node_id="tests/test_x.py::test_y")
404 _print_history({"tests/test_x.py::test_y": s}, flaky_only=False)
405 assert "test_y" in capsys.readouterr().out
406
407 def test_flaky_only_filters_non_flaky(self, capsys: pytest.CaptureFixture[str]) -> None:
408 from muse.cli.commands.test_cmd import _print_history
409 non_flaky = _make_history_summary(node_id="t::a", flaky=False)
410 flaky = _make_history_summary(node_id="t::b", flaky=True)
411 _print_history({"t::a": non_flaky, "t::b": flaky}, flaky_only=True)
412 out = capsys.readouterr().out
413 assert "t::b" in out
414 assert "t::a" not in out
415
416
417 # ──────────────────────────────────────────────────────────────────────────────
418 # Unit — _print_pre_run
419 # ──────────────────────────────────────────────────────────────────────────────
420
421
422 class TestPrintPreRun:
423 def test_with_selection_shows_changed_count(self, capsys: pytest.CaptureFixture[str]) -> None:
424 from muse.cli.commands.test_cmd import _print_pre_run
425 sel = _make_selection(changed_addresses=["a.py::f", "b.py::g"])
426 _print_pre_run(sel, targets=["tests/t.py::test_1", "tests/t.py::test_2"])
427 assert "Changed symbols: 2" in capsys.readouterr().out
428
429 def test_without_selection_with_targets(self, capsys: pytest.CaptureFixture[str]) -> None:
430 from muse.cli.commands.test_cmd import _print_pre_run
431 _print_pre_run(None, targets=["tests/t.py::test_1"])
432 assert "1 specified" in capsys.readouterr().out
433
434 def test_without_selection_without_targets(self, capsys: pytest.CaptureFixture[str]) -> None:
435 from muse.cli.commands.test_cmd import _print_pre_run
436 _print_pre_run(None, targets=[])
437 assert "full test suite" in capsys.readouterr().out
438
439 def test_uncovered_symbols_shown(self, capsys: pytest.CaptureFixture[str]) -> None:
440 from muse.cli.commands.test_cmd import _print_pre_run
441 sel = _make_selection(uncovered_addresses=["a.py::fn"])
442 _print_pre_run(sel, targets=[])
443 assert "no covering test" in capsys.readouterr().out or "uncovered" in capsys.readouterr().out.lower() or "⚠️" in capsys.readouterr().out
444
445 def test_fallback_note_shown(self, capsys: pytest.CaptureFixture[str]) -> None:
446 from muse.cli.commands.test_cmd import _print_pre_run
447 sel = _make_selection(fallback_used=True)
448 _print_pre_run(sel, targets=[])
449 assert "heuristic" in capsys.readouterr().out.lower() or "fallback" in capsys.readouterr().out.lower() or "File-name" in capsys.readouterr().out
450
451
452 # ──────────────────────────────────────────────────────────────────────────────
453 # Unit — _print_dry_run
454 # ──────────────────────────────────────────────────────────────────────────────
455
456
457 class TestPrintDryRun:
458 def test_human_with_targets_shows_would_run(self, capsys: pytest.CaptureFixture[str]) -> None:
459 from muse.cli.commands.test_cmd import _print_dry_run
460 _print_dry_run(None, targets=["tests/t.py::test_x"], json_out=False)
461 assert "Would run" in capsys.readouterr().out
462
463 def test_human_no_targets_shows_full_discovery(self, capsys: pytest.CaptureFixture[str]) -> None:
464 from muse.cli.commands.test_cmd import _print_dry_run
465 _print_dry_run(None, targets=[], json_out=False)
466 assert "full discovery" in capsys.readouterr().out
467
468 def test_json_mode_emits_mode_dry_run(self, capsys: pytest.CaptureFixture[str]) -> None:
469 from muse.cli.commands.test_cmd import _print_dry_run
470 _print_dry_run(None, targets=[], json_out=True)
471 d = json.loads(capsys.readouterr().out)
472 assert d["mode"] == "dry-run"
473
474 def test_json_mode_with_selection(self, capsys: pytest.CaptureFixture[str]) -> None:
475 from muse.cli.commands.test_cmd import _print_dry_run
476 sel = _make_selection(changed_addresses=["a.py::f"])
477 _print_dry_run(sel, targets=["tests/t.py::test_x"], json_out=True)
478 d = json.loads(capsys.readouterr().out)
479 assert "selection" in d
480 assert d["selection"]["targets"] == ["tests/t.py::test_x"]
481
482
483 # ──────────────────────────────────────────────────────────────────────────────
484 # Unit — _print_summary
485 # ──────────────────────────────────────────────────────────────────────────────
486
487
488 class TestPrintSummary:
489 def test_passed_shows_checkmark(self, capsys: pytest.CaptureFixture[str]) -> None:
490 from muse.cli.commands.test_cmd import _print_summary
491 result = _make_run_result(exit_code=0, passed=5, failed=0)
492 _print_summary(result, None)
493 assert "✅" in capsys.readouterr().out
494
495 def test_failed_shows_x(self, capsys: pytest.CaptureFixture[str]) -> None:
496 from muse.cli.commands.test_cmd import _print_summary
497 result = _make_run_result(exit_code=1, passed=2, failed=1)
498 _print_summary(result, None)
499 assert "❌" in capsys.readouterr().out
500
501 def test_timed_out_shows_warning(self, capsys: pytest.CaptureFixture[str]) -> None:
502 from muse.cli.commands.test_cmd import _print_summary
503 result = _make_run_result(exit_code=1, timed_out=True)
504 _print_summary(result, None)
505 assert "timeout" in capsys.readouterr().out.lower() or "terminated" in capsys.readouterr().out.lower()
506
507 def test_uncovered_addresses_shown(self, capsys: pytest.CaptureFixture[str]) -> None:
508 from muse.cli.commands.test_cmd import _print_summary
509 sel = _make_selection(uncovered_addresses=["a.py::fn"])
510 result = _make_run_result()
511 _print_summary(result, sel)
512 out = capsys.readouterr().out
513 assert "a.py::fn" in out or "Coverage gap" in out or "changed symbol" in out
514
515
516 # ──────────────────────────────────────────────────────────────────────────────
517 # Integration — alias, docstrings, envelope
518 # ──────────────────────────────────────────────────────────────────────────────
519
520
521 class TestAliasRegistration:
522 def test_j_alias_registered(self) -> None:
523 from muse.cli.commands.test_cmd import register
524 import argparse
525 p = argparse.ArgumentParser()
526 sub = p.add_subparsers()
527 register(sub)
528 ns = p.parse_args(["test", "-j"])
529 assert ns.json_out is True
530
531 def test_json_long_form_works(self) -> None:
532 from muse.cli.commands.test_cmd import register
533 import argparse
534 p = argparse.ArgumentParser()
535 sub = p.add_subparsers()
536 register(sub)
537 ns = p.parse_args(["test", "--json"])
538 assert ns.json_out is True
539
540
541 class TestDocstrings:
542 def test_register_mentions_json_alias(self) -> None:
543 from muse.cli.commands.test_cmd import register
544 doc = register.__doc__ or ""
545 assert "--json" in doc or "-j" in doc
546
547 def test_run_mentions_schema_version(self) -> None:
548 from muse.cli.commands.test_cmd import run
549 assert "json" in (run.__doc__ or "").lower() or "exit_code" in (run.__doc__ or "")
550
551 def test_run_mentions_exit_code(self) -> None:
552 from muse.cli.commands.test_cmd import run
553 assert "exit_code" in (run.__doc__ or "")
554
555 def test_run_mentions_duration_ms(self) -> None:
556 from muse.cli.commands.test_cmd import run
557 assert "duration_ms" in (run.__doc__ or "")
558
559
560 # ──────────────────────────────────────────────────────────────────────────────
561 # End-to-end
562 # ──────────────────────────────────────────────────────────────────────────────
563
564
565 class TestEndToEnd:
566 def test_history_exits_zero_empty(self, test_repo: pathlib.Path) -> None:
567 r = _invoke(test_repo, ["code", "test", "--history"])
568 assert r.exit_code == 0
569 assert "No test history" in r.output
570
571 def test_flaky_exits_zero_empty(self, test_repo: pathlib.Path) -> None:
572 r = _invoke(test_repo, ["code", "test", "--flaky"])
573 assert r.exit_code == 0
574 assert "No flaky" in r.output
575
576 def test_history_json_emits_mode_history(self, test_repo: pathlib.Path) -> None:
577 r = _invoke(test_repo, ["code", "test", "--history", "--json"])
578 assert r.exit_code == 0
579 d = json.loads(r.output)
580 assert d["mode"] == "history"
581
582 def test_history_json_has_schema_version(self, test_repo: pathlib.Path) -> None:
583 r = _invoke(test_repo, ["code", "test", "--history", "--json"])
584 assert r.exit_code == 0
585 assert "schema" in json.loads(r.output)
586
587 def test_history_json_has_history_list(self, test_repo: pathlib.Path) -> None:
588 r = _invoke(test_repo, ["code", "test", "--history", "--json"])
589 assert r.exit_code == 0
590 d = json.loads(r.output)
591 assert isinstance(d["history"], list)
592
593 def test_dry_run_exits_zero(self, test_repo: pathlib.Path) -> None:
594 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all"])
595 assert r.exit_code == 0
596
597 def test_dry_run_shows_would_run(self, test_repo: pathlib.Path) -> None:
598 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all"])
599 assert r.exit_code == 0
600 assert "Would run" in r.output or "dry" in r.output.lower() or "full discovery" in r.output
601
602 def test_dry_run_json_emits_mode(self, test_repo: pathlib.Path) -> None:
603 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all", "--json"])
604 assert r.exit_code == 0
605 d = json.loads(r.output)
606 assert d["mode"] == "dry-run"
607
608 def test_dry_run_json_has_schema_version(self, test_repo: pathlib.Path) -> None:
609 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all", "--json"])
610 assert r.exit_code == 0
611 assert "schema" in json.loads(r.output)
612
613 def test_j_alias_dry_run(self, test_repo: pathlib.Path) -> None:
614 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all", "-j"])
615 assert r.exit_code == 0
616 d = json.loads(r.output)
617 assert d["mode"] == "dry-run"
618
619 def test_flaky_json_has_schema_version(self, test_repo: pathlib.Path) -> None:
620 r = _invoke(test_repo, ["code", "test", "--flaky", "--json"])
621 assert r.exit_code == 0
622 assert "schema" in json.loads(r.output)
623
624 def test_no_changes_detected_json(self, test_repo: pathlib.Path) -> None:
625 """Clean working tree with --json emits 'no changes detected' message."""
626 r = _invoke(test_repo, ["code", "test", "--json"])
627 assert r.exit_code == 0
628 d = json.loads(r.output)
629 # Either no-changes or actually ran tests — both are valid
630 assert "mode" in d or "message" in d
631
632
633 # ──────────────────────────────────────────────────────────────────────────────
634 # Stress
635 # ──────────────────────────────────────────────────────────────────────────────
636
637
638 class TestStress:
639 def test_1000_history_to_json(self) -> None:
640 from muse.cli.commands.test_cmd import _history_to_json
641 s = _make_history_summary()
642 for _ in range(1_000):
643 d = _history_to_json(s)
644 assert d["node_id"] == s["node_id"]
645
646 def test_500_gate_to_json(self) -> None:
647 from muse.cli.commands.test_cmd import _gate_to_json
648 g = _make_gate()
649 for _ in range(500):
650 d = _gate_to_json(g)
651 assert d["name"] == g["name"]
652
653 def test_print_history_200_entries(self, capsys: pytest.CaptureFixture[str]) -> None:
654 from muse.cli.commands.test_cmd import _print_history
655 summaries = {
656 f"t::test_{i}": _make_history_summary(node_id=f"t::test_{i}")
657 for i in range(200)
658 }
659 _print_history(summaries, flaky_only=False)
660 out = capsys.readouterr().out
661 assert "test_0" in out
662
663 def test_concurrent_history_to_json(self) -> None:
664 from muse.cli.commands.test_cmd import _history_to_json
665 s = _make_history_summary()
666 results: list[str] = []
667 lock = threading.Lock()
668
669 def _run() -> None:
670 d = _history_to_json(s)
671 with lock:
672 results.append(d["node_id"])
673
674 threads = [threading.Thread(target=_run) for _ in range(50)]
675 for t in threads: t.start()
676 for t in threads: t.join()
677 assert len(results) == 50
678 assert all(r == s["node_id"] for r in results)
679
680
681 # ──────────────────────────────────────────────────────────────────────────────
682 # Data integrity
683 # ──────────────────────────────────────────────────────────────────────────────
684
685
686 class TestDataIntegrity:
687 def test_schema_version_is_string(self, test_repo: pathlib.Path) -> None:
688 r = _invoke(test_repo, ["code", "test", "--history", "--json"])
689 assert r.exit_code == 0
690 d = json.loads(r.output)
691 assert isinstance(d.get("schema"), int)
692
693 def test_schema_version_nonempty(self, test_repo: pathlib.Path) -> None:
694 r = _invoke(test_repo, ["code", "test", "--history", "--json"])
695 assert r.exit_code == 0
696 assert json.loads(r.output).get("schema", 0) > 0
697
698 def test_mode_field_present_in_all_json_modes(self, test_repo: pathlib.Path) -> None:
699 for extra in [["--history"], ["--flaky"], ["--dry-run", "--all"]]:
700 r = _invoke(test_repo, ["code", "test", *extra, "--json"])
701 assert r.exit_code == 0, f"failed for {extra}: {r.output}"
702 d = json.loads(r.output)
703 assert "mode" in d, f"missing mode for {extra}"
704
705 def test_history_to_json_all_fields(self) -> None:
706 from muse.cli.commands.test_cmd import _history_to_json
707 d = _history_to_json(_make_history_summary())
708 for field in ("node_id", "total_runs", "pass_count", "fail_count",
709 "skip_count", "flaky", "avg_duration_ms",
710 "last_outcome", "last_run_timestamp", "fail_streak"):
711 assert field in d, f"missing: {field}"
712
713 def test_gate_to_json_all_required_fields(self) -> None:
714 from muse.cli.commands.test_cmd import _gate_to_json
715 d = _gate_to_json(_make_gate())
716 for field in ("name", "command", "exit_code", "duration_ms",
717 "required", "passed", "timed_out", "stdout", "stderr"):
718 assert field in d, f"missing: {field}"
719
720 def test_ci_to_json_preserves_timestamp(self) -> None:
721 from muse.cli.commands.test_cmd import _ci_to_json
722 ts = "2026-04-19T10:00:00+00:00"
723 d = _ci_to_json(_make_ci_result(timestamp=ts))
724 assert d["timestamp"] == ts
725
726 def test_dry_run_json_serialisable(self, test_repo: pathlib.Path) -> None:
727 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all", "--json"])
728 assert r.exit_code == 0
729 json.loads(r.output) # must not raise
730
731
732 # ──────────────────────────────────────────────────────────────────────────────
733 # Security
734 # ──────────────────────────────────────────────────────────────────────────────
735
736
737 class TestSecurity:
738 def test_hostile_node_id_survives_json(self) -> None:
739 from muse.cli.commands.test_cmd import _history_to_json
740 malicious = '"; DROP TABLE history; --'
741 d = _history_to_json(_make_history_summary(node_id=malicious))
742 assert json.loads(json.dumps(d))["node_id"] == malicious
743
744 def test_ansi_in_gate_stdout_survives_json(self) -> None:
745 from muse.cli.commands.test_cmd import _gate_to_json
746 malicious_stdout = "\x1b[31merror\x1b[0m"
747 d = _gate_to_json(_make_gate(stdout=malicious_stdout))
748 assert json.loads(json.dumps(d))["stdout"] == malicious_stdout
749
750 def test_very_long_gate_name_does_not_crash(self) -> None:
751 from muse.cli.commands.test_cmd import _gate_to_json
752 d = _gate_to_json(_make_gate(name="x" * 10_000))
753 assert len(d["name"]) == 10_000
754
755 def test_sql_injection_in_fatal_msg_does_not_crash(self, capsys: pytest.CaptureFixture[str]) -> None:
756 from muse.cli.commands.test_cmd import _fatal
757 malicious = "'; DROP TABLE commits; --"
758 with pytest.raises(SystemExit):
759 _fatal(malicious, json_out=True)
760 d = json.loads(capsys.readouterr().out)
761 assert d["error"] == malicious
762
763 def test_unicode_in_history_node_id(self) -> None:
764 from muse.cli.commands.test_cmd import _history_to_json
765 d = _history_to_json(_make_history_summary(node_id="tests/音符.py::test_関数"))
766 assert json.loads(json.dumps(d))["node_id"] == "tests/音符.py::test_関数"
767
768 def test_null_byte_in_gate_stderr_does_not_crash(self) -> None:
769 from muse.cli.commands.test_cmd import _gate_to_json
770 d = _gate_to_json(_make_gate(stderr="err\x00byte"))
771 assert "err" in json.dumps(d)
772
773
774 # ──────────────────────────────────────────────────────────────────────────────
775 # Performance
776 # ──────────────────────────────────────────────────────────────────────────────
777
778
779 class TestPerformance:
780 def test_1000_gate_to_json_under_500ms(self) -> None:
781 from muse.cli.commands.test_cmd import _gate_to_json
782 g = _make_gate()
783 start = time.perf_counter()
784 for _ in range(1_000):
785 _gate_to_json(g)
786 elapsed = time.perf_counter() - start
787 assert elapsed < 0.5, f"1 000 _gate_to_json took {elapsed:.2f}s"
788
789 def test_1000_history_to_json_under_500ms(self) -> None:
790 from muse.cli.commands.test_cmd import _history_to_json
791 s = _make_history_summary()
792 start = time.perf_counter()
793 for _ in range(1_000):
794 _history_to_json(s)
795 elapsed = time.perf_counter() - start
796 assert elapsed < 0.5, f"1 000 _history_to_json took {elapsed:.2f}s"
797
798 def test_dry_run_completes_quickly(self, test_repo: pathlib.Path) -> None:
799 start = time.perf_counter()
800 r = _invoke(test_repo, ["code", "test", "--dry-run", "--all", "--json"])
801 elapsed = time.perf_counter() - start
802 assert r.exit_code == 0
803 assert elapsed < 10.0, f"--dry-run took {elapsed:.2f}s"
804
805
806 class TestRegisterFlags:
807 def _parse(self, *args: str) -> "argparse.Namespace":
808 import argparse
809 from muse.cli.commands.test_cmd import register
810 p = argparse.ArgumentParser()
811 sub = p.add_subparsers()
812 register(sub)
813 return p.parse_args(["test", *args])
814
815 def test_default_json_out_is_false(self) -> None:
816 ns = self._parse()
817 assert ns.json_out is False
818
819 def test_json_flag_sets_json_out(self) -> None:
820 ns = self._parse("--json")
821 assert ns.json_out is True
822
823 def test_j_shorthand_sets_json_out(self) -> None:
824 ns = self._parse("-j")
825 assert ns.json_out is True
File History 2 commits
sha256:cb2da6c61116ad1ab98d03747c21d6f66485839c7b4efd7d0124db0f8aa14e41 refactor(harmony): move auto_apply + record_resolutions int… Sonnet 4.6 minor 24 days ago
sha256:596a4963c21debb14d9ef51e23c2ca9f825b602ab8585f69caca35eb81bcac77 chore(harmony): baseline audit — Phase 0 of issue #16 Sonnet 4.6 28 days ago