from arena.models import CodebaseArchive, ValidationCheck from arena.validator_plan import ValidatorContext, ValidatorPlan from arena.validator_graph import DeterministicValidatorExecutor, DeterministicRubricReviewer, RubricReview, ValidatorGraph def test_validator_passes_when_all_checks_succeed() -> None: graph = ValidatorGraph(executor=DeterministicValidatorExecutor()) report = graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=2, size_bytes=128), plan=ValidatorPlan.from_checks([ ValidationCheck(id="check-1", name="tests", command="uv run pytest", source="user"), ValidationCheck(id="check-2", name="syntax", command="python -m compileall ."), ]), ) assert report.run_id == "run-1" assert report.passed is True assert len(report.results) == 2 assert all(r.passed for r in report.results) assert "Validation checks passed: 2/2" in report.summary assert report.risks == [] def test_validator_fails_when_check_fails() -> None: graph = ValidatorGraph(executor=DeterministicValidatorExecutor()) report = graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=2, size_bytes=128), plan=ValidatorPlan.from_checks([ ValidationCheck(id="check-1", name="tests", command="uv run pytest", source="user"), ValidationCheck(id="check-2", name="bad", command="fail the build"), ]), ) assert report.passed is False assert [r.passed for r in report.results] == [True, False] assert "Validation checks passed: 1/2" in report.summary assert len(report.risks) >= 1 def test_validator_with_no_checks_passes() -> None: graph = ValidatorGraph(executor=DeterministicValidatorExecutor()) report = graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=ValidatorPlan(), ) assert report.passed is True assert "No validation checks configured" in report.summary def test_validator_includes_rubric_review_by_default() -> None: graph = ValidatorGraph(executor=DeterministicValidatorExecutor()) report = graph.validate( run_id="run-1", prompt="Build a CLI", criteria=[], codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=ValidatorPlan.from_checks([ ValidationCheck(id="check-1", name="tests", command="uv run pytest", source="user"), ]), ) assert report.rubric_score is not None assert 0.0 <= report.rubric_score <= 1.0 assert "rubric score" in report.summary.lower() def test_validator_with_custom_rubric_reviewer() -> None: class CustomReviewer: def review(self, *, prompt, criteria, check_results): return RubricReview(score=0.75, feedback="custom review", risks=["risk-1"]) graph = ValidatorGraph(executor=DeterministicValidatorExecutor(), rubric_reviewer=CustomReviewer()) report = graph.validate( run_id="run-1", prompt="Build a thing", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=ValidatorPlan.from_checks([ ValidationCheck(id="check-1", name="tests", command="uv run pytest", source="user"), ]), ) assert report.rubric_score == 0.75 assert report.rubric_feedback == "custom review" assert "risk-1" in report.risks def test_validator_uses_prompt_and_criteria_from_plan_context() -> None: from arena.models import Criteria captured: dict = {} class CaptureReviewer: def review(self, *, prompt, criteria, check_results): captured["prompt"] = prompt captured["criteria"] = list(criteria) return RubricReview(score=0.9, feedback="ok", risks=[]) context = ValidatorContext(prompt="Build a CLI", criteria=(Criteria(text="tests must pass"),)) graph = ValidatorGraph( executor=DeterministicValidatorExecutor(), rubric_reviewer=CaptureReviewer(), ) graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=ValidatorPlan.from_user_tests(["echo ok"], context=context), ) assert captured["prompt"] == "Build a CLI" assert captured["criteria"][0].text == "tests must pass" def test_validator_runs_generated_gradio_smoke_check_from_plan_context() -> None: from arena.models import Criteria commands: list[str] = [] class CaptureExecutor: def run(self, archive_pointer: str, command: str): commands.append(command) return True, "ok" context = ValidatorContext( prompt="Build a Gradio demo app", criteria=(Criteria(text="Works as a Gradio app"),), ) graph = ValidatorGraph(executor=CaptureExecutor(), rubric_enabled=False) report = graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=ValidatorPlan.from_user_tests(["python -m py_compile app.py"], context=context, rubric_enabled=False), ) assert report.passed is True assert len(commands) == 2 assert "127.0.0.1:{port}" in commands[1] def test_validator_skips_rubric_when_plan_has_no_rubric_step() -> None: calls: list[bool] = [] def counting_reviewer(*, prompt, criteria, check_results): calls.append(True) return RubricReview(score=1.0, feedback="ok", risks=[]) graph = ValidatorGraph( executor=DeterministicValidatorExecutor(), rubric_reviewer=counting_reviewer, rubric_enabled=True, ) plan = ValidatorPlan.from_user_tests(["echo ok"], rubric_enabled=False) report = graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=plan, ) assert calls == [] assert report.rubric_score is None assert "rubric score" not in report.summary.lower() def test_validator_closes_executor_after_validation() -> None: class CloseableExecutor: def __init__(self) -> None: self.closed = 0 def run(self, archive_pointer: str, command: str): return True, "ok" def close(self) -> None: self.closed += 1 executor = CloseableExecutor() graph = ValidatorGraph(executor=executor, rubric_enabled=False) report = graph.validate( run_id="run-1", codebase_archive=CodebaseArchive(pointer="local-snapshot://run-1", file_count=1, size_bytes=10), plan=ValidatorPlan.from_user_tests(["echo ok"]), ) assert report.passed is True assert executor.closed == 1