test_sweep.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. """Tests for Sweep orchestrator."""
  2. from __future__ import annotations
  3. import json
  4. from dataclasses import asdict
  5. from pathlib import Path
  6. from unittest.mock import patch
  7. from drill.engine import Engine, RunResult
  8. from drill.sweep import RunGroup, RunStatus, Sweep, write_run_group
  9. class TestRunStatus:
  10. def test_pass_status(self) -> None:
  11. rs = RunStatus(index=0, status="pass", duration=10.5)
  12. assert rs.error is None
  13. assert rs.status == "pass"
  14. def test_error_status(self) -> None:
  15. rs = RunStatus(index=2, status="error", duration=1.2, error="tmux crashed")
  16. assert rs.error == "tmux crashed"
  17. def test_serializes_to_dict(self) -> None:
  18. rs = RunStatus(index=0, status="pass", duration=10.5)
  19. d = asdict(rs)
  20. assert d["index"] == 0
  21. assert d["status"] == "pass"
  22. assert d["duration"] == 10.5
  23. assert d["error"] is None
  24. class TestRunGroup:
  25. def test_creates_with_defaults(self) -> None:
  26. rg = RunGroup(
  27. scenario="test",
  28. backend="claude",
  29. n=3,
  30. timestamp="2026-04-20T14-30-00",
  31. sweep_id="abc12345",
  32. runs=[],
  33. )
  34. assert rg.partial is False
  35. def test_partial_flag(self) -> None:
  36. rg = RunGroup(
  37. scenario="test",
  38. backend="claude",
  39. n=3,
  40. timestamp="2026-04-20T14-30-00",
  41. sweep_id="abc12345",
  42. runs=[RunStatus(index=0, status="pass", duration=10.0)],
  43. partial=True,
  44. )
  45. assert rg.partial is True
  46. assert len(rg.runs) == 1
  47. class TestWriteRunGroup:
  48. def test_writes_json(self, tmp_path: Path) -> None:
  49. rg = RunGroup(
  50. scenario="test-scenario",
  51. backend="claude",
  52. n=2,
  53. timestamp="2026-04-20T14-30-00",
  54. sweep_id="abc12345",
  55. runs=[
  56. RunStatus(index=0, status="pass", duration=100.0),
  57. RunStatus(index=1, status="fail", duration=95.0),
  58. ],
  59. )
  60. write_run_group(rg, tmp_path)
  61. path = tmp_path / "run-group.json"
  62. assert path.exists()
  63. data = json.loads(path.read_text())
  64. assert data["scenario"] == "test-scenario"
  65. assert data["sweep_id"] == "abc12345"
  66. assert data["partial"] is False
  67. assert len(data["runs"]) == 2
  68. assert data["runs"][0]["status"] == "pass"
  69. assert data["runs"][1]["status"] == "fail"
  70. def test_writes_partial(self, tmp_path: Path) -> None:
  71. rg = RunGroup(
  72. scenario="test",
  73. backend="claude",
  74. n=5,
  75. timestamp="2026-04-20T14-30-00",
  76. sweep_id="abc12345",
  77. runs=[RunStatus(index=0, status="pass", duration=100.0)],
  78. partial=True,
  79. )
  80. write_run_group(rg, tmp_path)
  81. data = json.loads((tmp_path / "run-group.json").read_text())
  82. assert data["partial"] is True
  83. assert len(data["runs"]) == 1
  84. def test_omits_null_errors(self, tmp_path: Path) -> None:
  85. rg = RunGroup(
  86. scenario="test",
  87. backend="claude",
  88. n=1,
  89. timestamp="2026-04-20T14-30-00",
  90. sweep_id="abc12345",
  91. runs=[RunStatus(index=0, status="pass", duration=50.0)],
  92. )
  93. write_run_group(rg, tmp_path)
  94. data = json.loads((tmp_path / "run-group.json").read_text())
  95. run_data = data["runs"][0]
  96. assert "error" not in run_data
  97. class TestSweepIntegration:
  98. def test_full_sweep_writes_run_group(self, tmp_path: Path) -> None:
  99. """Test that Sweep creates run dirs and writes run-group.json."""
  100. scenario_file = tmp_path / "scenarios" / "test.yaml"
  101. scenario_file.parent.mkdir(parents=True)
  102. scenario_file.write_text(
  103. "scenario: test-scenario\n"
  104. "description: test\n"
  105. "user_posture: naive\n"
  106. "setup: {}\n"
  107. "turns:\n - intent: do the thing\n"
  108. "limits:\n max_turns: 5\n"
  109. "verify:\n criteria:\n - thing was done\n"
  110. )
  111. backends_dir = tmp_path / "backends"
  112. backends_dir.mkdir()
  113. (backends_dir / "mock-backend.yaml").write_text(
  114. "name: mock-backend\n"
  115. "cli: echo\n"
  116. "args: []\n"
  117. "required_env: []\n"
  118. "hooks:\n pre_run: []\n post_run: []\n"
  119. "shutdown: /exit\n"
  120. "idle:\n quiescence_seconds: 1\n ready_pattern: '.'\n"
  121. "startup_timeout: 5\n"
  122. "terminal:\n cols: 80\n rows: 24\n"
  123. "session_logs: {}\n"
  124. )
  125. results_dir = tmp_path / "results"
  126. fixtures_dir = tmp_path / "fixtures"
  127. fixtures_dir.mkdir()
  128. fake_verdict = json.dumps(
  129. {
  130. "criteria": [
  131. {
  132. "criterion": "thing was done",
  133. "verdict": "pass",
  134. "evidence": "yes",
  135. "rationale": "it was done",
  136. }
  137. ],
  138. "observations": [],
  139. "summary": "ok",
  140. }
  141. )
  142. fake_result = RunResult(
  143. scenario="test-scenario",
  144. backend="mock-backend",
  145. timestamp="2026-04-20T14-30-00",
  146. session_log="log",
  147. filesystem_json='{"files": []}',
  148. tool_calls_jsonl='{"tool": "Bash"}',
  149. verdict_json=fake_verdict,
  150. meta={"actor_turns": 3},
  151. )
  152. sweep = Sweep(
  153. scenario_path=scenario_file,
  154. backend_names=["mock-backend"],
  155. backends_dir=backends_dir,
  156. fixtures_dir=fixtures_dir,
  157. results_dir=results_dir,
  158. n=3,
  159. sweep_id="test1234",
  160. )
  161. with patch.object(Engine, "run", return_value=fake_result):
  162. groups = sweep.run_all()
  163. assert len(groups) == 1
  164. group = groups[0]
  165. assert group.scenario == "test-scenario"
  166. assert len(group.runs) == 3
  167. assert all(r.status == "pass" for r in group.runs)
  168. assert group.partial is False
  169. # Verify run-group.json was written
  170. scenario_results = results_dir / "test-scenario" / "mock-backend"
  171. assert scenario_results.exists()
  172. group_dirs = list(scenario_results.iterdir())
  173. assert len(group_dirs) == 1
  174. rg_path = group_dirs[0] / "run-group.json"
  175. assert rg_path.exists()
  176. rg_data = json.loads(rg_path.read_text())
  177. assert rg_data["sweep_id"] == "test1234"
  178. assert len(rg_data["runs"]) == 3