from __future__ import annotations import pytest from pydantic import ValidationError from gap_pipeline.models import ( JudgeVerdict, KernelCandidate, KernelPlan, ProofPlanDAG, ) def test_valid_plan_and_candidate(plan_dict: dict, candidate_dict: dict) -> None: plan = KernelPlan.model_validate(plan_dict) candidate = KernelCandidate.model_validate(candidate_dict) assert len(plan.core_steps) == 2 assert candidate.question.startswith("Let x") def test_plan_requires_slots(plan_dict: dict) -> None: plan_dict["mutable_slots"] = {} with pytest.raises(ValidationError, match="mutable slot"): KernelPlan.model_validate(plan_dict) def test_plan_rejects_more_than_five_core_steps(plan_dict: dict) -> None: plan_dict["core_steps"] = [f"step {index}" for index in range(6)] with pytest.raises(ValidationError): KernelPlan.model_validate(plan_dict) def test_candidate_requires_question_and_solution(candidate_dict: dict) -> None: candidate_dict["solution"] = "" with pytest.raises(ValidationError, match="non-empty"): KernelCandidate.model_validate(candidate_dict) def test_accept_verdict_cannot_report_blocking_issue() -> None: with pytest.raises(ValidationError, match="cannot contain"): JudgeVerdict( verdict="accept", step_by_step_check="checked", blocking_issues="an error", ) def test_path_dag_is_derived_from_ordered_plan(plan_dict: dict) -> None: dag = ProofPlanDAG.from_plan(KernelPlan.model_validate(plan_dict)) assert [node.node_id for node in dag.nodes] == ["n1", "n2"] assert dag.nodes[0].dependencies == [] assert dag.nodes[1].dependencies == ["n1"] assert dag.terminal_node_id == "n2" assert dag.method_labels_payload()[0].startswith("n1: ") def test_dag_rejects_cycle() -> None: with pytest.raises(ValidationError, match="cycle"): ProofPlanDAG.model_validate( { "nodes": [ { "node_id": "n1", "method_label": "first", "dependencies": ["n2"], }, { "node_id": "n2", "method_label": "second", "dependencies": ["n1"], }, ], "terminal_node_id": "n2", } ) def test_path_dag_rejects_skipped_dependency() -> None: with pytest.raises(ValidationError, match="lead to the terminal"): ProofPlanDAG.model_validate( { "nodes": [ { "node_id": "n1", "method_label": "first", "dependencies": [], }, { "node_id": "n2", "method_label": "second", "dependencies": [], }, ], "terminal_node_id": "n2", } ) def test_judge_verdict_does_not_require_keyword_coverage() -> None: verdict = JudgeVerdict( verdict="reject", step_by_step_check=( "The early derivation is correct, but the announced replacement " "never appears in the candidate problem." ), blocking_issues="the candidate is unchanged from the original", patch_suggestion="apply the declared replacement", ) assert verdict.verdict == "reject" def test_judge_verdict_normalizes_structured_text_fields() -> None: verdict = JudgeVerdict.model_validate( { "verdict": "accept", "step_by_step_check": { "n1": "correctly instantiated", "n2": "correctly instantiated", }, "blocking_issues": None, "patch_suggestion": None, } ) assert '"n1"' in verdict.step_by_step_check assert verdict.blocking_issues == "" assert verdict.patch_suggestion == "" def test_dag_labels_must_match_prompt_a_plan(plan_dict: dict) -> None: plan = KernelPlan.model_validate(plan_dict) dag = ProofPlanDAG.from_plan(plan) changed_plan = plan.model_copy( update={"core_steps": ["a different method", plan.core_steps[1]]} ) with pytest.raises(ValueError, match="must equal Prompt-A core_steps"): dag.validate_plan(changed_plan)