test_engine.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. from __future__ import annotations
  2. import json
  3. import subprocess
  4. from pathlib import Path
  5. from drill.engine import RunResult, ScenarioConfig, VerifyConfig, snapshot_filesystem
  6. class TestVerifyConfig:
  7. def test_defaults(self):
  8. vc = VerifyConfig()
  9. assert vc.criteria == []
  10. assert vc.assertions == []
  11. assert vc.observe is False
  12. def test_from_dict(self):
  13. vc = VerifyConfig(
  14. criteria=["test criterion"],
  15. assertions=["tool-called Read"],
  16. observe=True,
  17. )
  18. assert len(vc.criteria) == 1
  19. assert len(vc.assertions) == 1
  20. assert vc.observe is True
  21. class TestScenarioConfig:
  22. def test_loads_from_yaml(self, tmp_path):
  23. scenario_file = tmp_path / "test.yaml"
  24. scenario_file.write_text("""
  25. scenario: test-scenario
  26. description: "A test"
  27. user_posture: naive
  28. setup:
  29. helpers:
  30. - create_base_repo
  31. assertions:
  32. - "git rev-parse --is-inside-work-tree"
  33. turns:
  34. - intent: "Do the thing"
  35. limits:
  36. max_turns: 10
  37. turn_timeout: 60
  38. verify:
  39. criteria:
  40. - "Thing was done"
  41. assertions:
  42. - "tool-called Bash"
  43. observe: true
  44. """)
  45. config = ScenarioConfig.from_yaml(scenario_file)
  46. assert config.scenario == "test-scenario"
  47. assert config.user_posture == "naive"
  48. assert config.limits["max_turns"] == 10
  49. assert len(config.turns) == 1
  50. assert len(config.verify.criteria) == 1
  51. assert len(config.verify.assertions) == 1
  52. assert config.verify.observe is True
  53. def test_loads_without_assertions(self, tmp_path):
  54. scenario_file = tmp_path / "test.yaml"
  55. scenario_file.write_text("""
  56. scenario: minimal
  57. verify:
  58. criteria:
  59. - "Something happened"
  60. """)
  61. config = ScenarioConfig.from_yaml(scenario_file)
  62. assert config.verify.assertions == []
  63. assert config.verify.observe is False
  64. def test_loads_without_verify(self, tmp_path):
  65. scenario_file = tmp_path / "test.yaml"
  66. scenario_file.write_text("""
  67. scenario: bare-minimum
  68. """)
  69. config = ScenarioConfig.from_yaml(scenario_file)
  70. assert config.verify.criteria == []
  71. assert config.verify.assertions == []
  72. class TestSnapshotFilesystem:
  73. def test_captures_git_state(self, tmp_path):
  74. subprocess.run(["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True)
  75. subprocess.run(
  76. ["git", "commit", "--allow-empty", "-m", "init"], cwd=tmp_path, capture_output=True
  77. )
  78. snapshot = snapshot_filesystem(tmp_path)
  79. data = json.loads(snapshot)
  80. assert "git_status" in data
  81. assert "branch" in data
  82. assert "worktree_list" in data
  83. assert "files" in data
  84. class TestRunResult:
  85. def test_serializes_to_dir(self, tmp_path):
  86. result = RunResult(
  87. scenario="test",
  88. backend="claude",
  89. timestamp="2026-04-07T14-30-00",
  90. session_log="session output here",
  91. filesystem_json='{"files": []}',
  92. tool_calls_jsonl='{"tool": "Bash"}\n',
  93. verdict_json='{"criteria": [], "observations": [], "summary": "ok"}',
  94. meta={"backend": "claude", "duration_seconds": 42, "actor_turns": 5},
  95. )
  96. result.save(tmp_path)
  97. assert (tmp_path / "session.log").read_text() == "session output here"
  98. assert (tmp_path / "filesystem.json").exists()
  99. assert (tmp_path / "tool_calls.jsonl").exists()
  100. assert (tmp_path / "verdict.json").exists()
  101. assert (tmp_path / "meta.json").exists()
  102. class TestEngineAssertionIntegration:
  103. def test_run_result_save_splits_artifacts_and_verdict(self, tmp_path):
  104. result = RunResult(
  105. scenario="test",
  106. backend="claude",
  107. timestamp="2026-04-20T10-00-00",
  108. session_log="log here",
  109. filesystem_json='{"files": []}',
  110. tool_calls_jsonl='{"tool": "Bash"}\n',
  111. verdict_json='{"criteria": [], "observations": [], "summary": "ok"}',
  112. meta={"backend": "claude"},
  113. )
  114. result.save_artifacts(tmp_path)
  115. assert (tmp_path / "session.log").exists()
  116. assert (tmp_path / "filesystem.json").exists()
  117. assert (tmp_path / "tool_calls.jsonl").exists()
  118. assert not (tmp_path / "verdict.json").exists()
  119. assert not (tmp_path / "meta.json").exists()
  120. result.save_verdict(tmp_path)
  121. assert (tmp_path / "verdict.json").exists()
  122. assert (tmp_path / "meta.json").exists()
  123. class TestEngineRunParams:
  124. def test_run_result_uses_custom_output_dir(self, tmp_path: Path) -> None:
  125. custom_dir = tmp_path / "custom" / "run-00"
  126. result = RunResult(
  127. scenario="test",
  128. backend="claude",
  129. timestamp="2026-04-20T10-00-00",
  130. session_log="log",
  131. filesystem_json='{"files": []}',
  132. tool_calls_jsonl='{"tool": "Bash"}\n',
  133. verdict_json='{"criteria": [], "observations": [], "summary": "ok"}',
  134. meta={"backend": "claude"},
  135. )
  136. result.save(custom_dir)
  137. assert (custom_dir / "session.log").read_text() == "log"
  138. assert (custom_dir / "verdict.json").exists()
  139. assert (custom_dir / "meta.json").exists()
  140. def test_run_result_nested_dir_created(self, tmp_path: Path) -> None:
  141. deep_dir = tmp_path / "a" / "b" / "c" / "run-05"
  142. result = RunResult(
  143. scenario="test",
  144. backend="claude",
  145. timestamp="2026-04-20T10-00-00",
  146. session_log="log",
  147. filesystem_json='{"files": []}',
  148. tool_calls_jsonl='{"tool": "Bash"}\n',
  149. verdict_json='{"criteria": [], "observations": [], "summary": "ok"}',
  150. meta={"backend": "claude"},
  151. )
  152. result.save(deep_dir)
  153. assert deep_dir.exists()
  154. assert (deep_dir / "session.log").exists()