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_requires_every_dag_node(plan_dict: dict) -> None: dag = ProofPlanDAG.from_plan(KernelPlan.model_validate(plan_dict)) verdict = JudgeVerdict( verdict="accept", step_by_step_check="n1 is valid; n2 is valid", ) assert verdict.validate_coverage(dag) is verdict with pytest.raises(ValueError, match="does not cover DAG nodes.*n2"): JudgeVerdict( verdict="accept", step_by_step_check="n1 is valid", ).validate_coverage(dag) 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)