test_compare.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. """Tests for compare module."""
  2. from __future__ import annotations
  3. import json
  4. from pathlib import Path
  5. from drill.compare import BackendResult, format_compare_output, load_scenario_results
  6. def _write_verdict(path: Path, criteria: list[dict[str, str]]) -> None:
  7. verdict = {
  8. "criteria": criteria,
  9. "observations": ["test obs"],
  10. "summary": "ok",
  11. }
  12. path.parent.mkdir(parents=True, exist_ok=True)
  13. path.write_text(json.dumps(verdict))
  14. def _write_meta(path: Path, **kwargs: object) -> None:
  15. meta = {"scenario": "test", "backend": "claude", "actor_turns": 4, **kwargs}
  16. path.parent.mkdir(parents=True, exist_ok=True)
  17. path.write_text(json.dumps(meta))
  18. def _write_run_group(
  19. path: Path, n: int, runs: list[dict[str, object]], sweep_id: str = "abc12345"
  20. ) -> None:
  21. data = {
  22. "scenario": "test",
  23. "backend": "claude",
  24. "n": n,
  25. "timestamp": "2026-04-20T14-30-00",
  26. "sweep_id": sweep_id,
  27. "partial": False,
  28. "runs": runs,
  29. }
  30. path.parent.mkdir(parents=True, exist_ok=True)
  31. path.write_text(json.dumps(data))
  32. class TestLoadScenarioResults:
  33. def test_loads_new_format_single_run(self, tmp_path: Path) -> None:
  34. scenario_dir = tmp_path / "test-scenario" / "claude" / "2026-04-20T14-30-00-abc12345"
  35. run_dir = scenario_dir / "run-00"
  36. criteria = [{"criterion": "c1", "verdict": "pass", "evidence": "e", "rationale": "r"}]
  37. _write_verdict(run_dir / "verdict.json", criteria)
  38. _write_meta(run_dir / "meta.json")
  39. _write_run_group(
  40. scenario_dir / "run-group.json",
  41. n=1,
  42. runs=[{"index": 0, "status": "pass", "duration": 10.0}],
  43. )
  44. results = load_scenario_results(tmp_path / "test-scenario")
  45. assert "claude" in results
  46. assert results["claude"].total_runs == 1
  47. assert results["claude"].passed_runs == 1
  48. def test_loads_new_format_multi_run(self, tmp_path: Path) -> None:
  49. scenario_dir = tmp_path / "test-scenario" / "claude" / "2026-04-20T14-30-00-abc12345"
  50. for i in range(3):
  51. run_dir = scenario_dir / f"run-{i:02d}"
  52. verdict_val = "pass" if i < 2 else "fail"
  53. criteria = [
  54. {"criterion": "c1", "verdict": verdict_val, "evidence": "e", "rationale": "r"}
  55. ]
  56. _write_verdict(run_dir / "verdict.json", criteria)
  57. _write_meta(run_dir / "meta.json")
  58. _write_run_group(
  59. scenario_dir / "run-group.json",
  60. n=3,
  61. runs=[
  62. {"index": 0, "status": "pass", "duration": 10.0},
  63. {"index": 1, "status": "pass", "duration": 11.0},
  64. {"index": 2, "status": "fail", "duration": 12.0},
  65. ],
  66. )
  67. results = load_scenario_results(tmp_path / "test-scenario")
  68. assert results["claude"].total_runs == 3
  69. assert results["claude"].passed_runs == 2
  70. assert len(results["claude"].criterion_counts) == 1
  71. assert results["claude"].criterion_counts["c1"] == (2, 3)
  72. def test_loads_old_format_backwards_compat(self, tmp_path: Path) -> None:
  73. scenario_dir = tmp_path / "test-scenario" / "claude" / "2026-04-20T14-30-00"
  74. criteria = [{"criterion": "c1", "verdict": "pass", "evidence": "e", "rationale": "r"}]
  75. _write_verdict(scenario_dir / "verdict.json", criteria)
  76. _write_meta(scenario_dir / "meta.json")
  77. results = load_scenario_results(tmp_path / "test-scenario")
  78. assert "claude" in results
  79. assert results["claude"].total_runs == 1
  80. assert results["claude"].passed_runs == 1
  81. def test_sweep_filter(self, tmp_path: Path) -> None:
  82. base = tmp_path / "test-scenario" / "claude"
  83. # Sweep A
  84. dir_a = base / "2026-04-20T14-30-00-aaaa1111"
  85. _write_run_group(
  86. dir_a / "run-group.json",
  87. n=1,
  88. runs=[{"index": 0, "status": "pass", "duration": 10.0}],
  89. sweep_id="aaaa1111",
  90. )
  91. criteria = [{"criterion": "c1", "verdict": "pass", "evidence": "e", "rationale": "r"}]
  92. _write_verdict(dir_a / "run-00" / "verdict.json", criteria)
  93. _write_meta(dir_a / "run-00" / "meta.json")
  94. # Sweep B
  95. dir_b = base / "2026-04-20T15-00-00-bbbb2222"
  96. _write_run_group(
  97. dir_b / "run-group.json",
  98. n=1,
  99. runs=[{"index": 0, "status": "fail", "duration": 10.0}],
  100. sweep_id="bbbb2222",
  101. )
  102. criteria_b = [{"criterion": "c1", "verdict": "fail", "evidence": "e", "rationale": "r"}]
  103. _write_verdict(dir_b / "run-00" / "verdict.json", criteria_b)
  104. _write_meta(dir_b / "run-00" / "meta.json")
  105. results_a = load_scenario_results(tmp_path / "test-scenario", sweep_id="aaaa1111")
  106. assert results_a["claude"].passed_runs == 1
  107. results_b = load_scenario_results(tmp_path / "test-scenario", sweep_id="bbbb2222")
  108. assert results_b["claude"].passed_runs == 0
  109. class TestBackendResult:
  110. def test_pass_rate(self) -> None:
  111. br = BackendResult(
  112. backend="claude",
  113. total_runs=10,
  114. passed_runs=8,
  115. errored_runs=0,
  116. avg_turns=4.2,
  117. criterion_counts={"c1": (10, 10), "c2": (8, 10)},
  118. sweep_id="abc12345",
  119. timestamp="2026-04-20T14-30-00",
  120. partial=False,
  121. )
  122. assert br.pass_rate == 0.8
  123. def test_pass_rate_zero_runs(self) -> None:
  124. br = BackendResult(
  125. backend="claude",
  126. total_runs=0,
  127. passed_runs=0,
  128. errored_runs=0,
  129. avg_turns=0.0,
  130. criterion_counts={},
  131. sweep_id=None,
  132. timestamp=None,
  133. partial=False,
  134. )
  135. assert br.pass_rate == 0.0
  136. def _make_backend_result(
  137. backend: str = "claude",
  138. total_runs: int = 10,
  139. passed_runs: int = 8,
  140. errored_runs: int = 0,
  141. avg_turns: float = 4.2,
  142. criterion_counts: dict[str, tuple[int, int]] | None = None,
  143. sweep_id: str | None = "abc12345",
  144. timestamp: str | None = "2026-04-20T14-30-00",
  145. partial: bool = False,
  146. ) -> BackendResult:
  147. return BackendResult(
  148. backend=backend,
  149. total_runs=total_runs,
  150. passed_runs=passed_runs,
  151. errored_runs=errored_runs,
  152. avg_turns=avg_turns,
  153. criterion_counts=criterion_counts or {"c1": (passed_runs, total_runs)},
  154. sweep_id=sweep_id,
  155. timestamp=timestamp,
  156. partial=partial,
  157. )
  158. class TestFormatCompareOutput:
  159. def test_no_results(self) -> None:
  160. output = format_compare_output("test", {})
  161. assert "No results found" in output
  162. def test_multi_run_includes_pass_rate_and_ci(self) -> None:
  163. results = {"claude": _make_backend_result(total_runs=10, passed_runs=8)}
  164. output = format_compare_output("test", results)
  165. assert "Overall pass rate" in output
  166. assert "95% CI" in output
  167. assert "80.0%" in output
  168. def test_multi_run_sweep_header_includes_date(self) -> None:
  169. results = {"claude": _make_backend_result()}
  170. output = format_compare_output("test", results)
  171. assert "Sweep: abc12345 | 2026-04-20" in output
  172. def test_single_run_simple_table(self) -> None:
  173. results = {
  174. "claude": _make_backend_result(
  175. total_runs=1,
  176. passed_runs=1,
  177. criterion_counts={"c1": (1, 1)},
  178. )
  179. }
  180. output = format_compare_output("test", results)
  181. assert "PASS" in output
  182. assert "Overall pass rate" not in output
  183. def test_partial_warning(self) -> None:
  184. results = {"claude": _make_backend_result(partial=True)}
  185. output = format_compare_output("test", results)
  186. assert "incomplete" in output.lower() or "interrupted" in output.lower()
  187. def test_small_n_note(self) -> None:
  188. results = {"claude": _make_backend_result(total_runs=5, passed_runs=3)}
  189. output = format_compare_output("test", results)
  190. assert "--n 10+" in output