markitect-main/tests/unit/prompts/test_halting_policy.py

222 lines
7.6 KiB
Python
Raw Normal View History

"""
Unit tests for HaltingPolicyEngine.
Tests halting decisions based on quality results, iteration limits,
marginal improvement, and resource budgets.
"""
import pytest
from markitect.prompts.quality.models import (
GateType,
HaltDecision,
QualityPolicy,
ValidationResult,
ValidationStatus,
)
from markitect.prompts.quality.policy import HaltingPolicyEngine
def _make_result(status=ValidationStatus.PASS, score=1.0, gate_id="gate-1"):
"""Helper to create a ValidationResult."""
return ValidationResult.create(
gate_id=gate_id,
gate_type=GateType.PATTERN,
artifact_id="art-1",
status=status,
score=score,
)
class TestQualityMetDecision:
"""Tests for quality met halting."""
def test_all_pass_halts_quality_met(self):
"""Test all gates passing triggers quality met halt."""
policy = QualityPolicy(max_iterations=5)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.PASS, 1.0)]
record = engine.evaluate(results, iteration=1)
assert record.decision == HaltDecision.HALT_QUALITY_MET
assert "quality gates passed" in record.reason.lower()
def test_required_gates_all_pass(self):
"""Test required gates all passing triggers quality met."""
policy = QualityPolicy(
max_iterations=5,
required_gate_ids=["required-gate"],
)
engine = HaltingPolicyEngine(policy)
results = [
_make_result(ValidationStatus.PASS, 1.0, gate_id="required-gate"),
_make_result(ValidationStatus.FAIL, 0.5, gate_id="optional-gate"),
]
record = engine.evaluate(results, iteration=1)
assert record.decision == HaltDecision.HALT_QUALITY_MET
def test_required_gate_fails_continues(self):
"""Test required gate failing allows continuation."""
policy = QualityPolicy(
max_iterations=5,
required_gate_ids=["required-gate"],
)
engine = HaltingPolicyEngine(policy)
results = [
_make_result(ValidationStatus.FAIL, 0.5, gate_id="required-gate"),
]
record = engine.evaluate(results, iteration=1)
assert record.decision == HaltDecision.CONTINUE
class TestIterationLimitDecision:
"""Tests for iteration limit halting."""
def test_at_iteration_limit(self):
"""Test halting at iteration limit."""
policy = QualityPolicy(max_iterations=3)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.5)]
record = engine.evaluate(results, iteration=3)
assert record.decision == HaltDecision.HALT_ITERATION_LIMIT
assert record.iteration == 3
def test_before_iteration_limit(self):
"""Test not halting before iteration limit."""
policy = QualityPolicy(max_iterations=5)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.5)]
record = engine.evaluate(results, iteration=2)
assert record.decision == HaltDecision.CONTINUE
class TestBudgetExhaustedDecision:
"""Tests for resource budget exhaustion."""
def test_budget_exhausted(self):
"""Test halting when budget is exhausted."""
policy = QualityPolicy(max_iterations=10, resource_budget=5)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.5)]
record = engine.evaluate(results, iteration=1, total_runs=5)
assert record.decision == HaltDecision.HALT_BUDGET_EXHAUSTED
def test_budget_not_exhausted(self):
"""Test not halting when budget remains."""
policy = QualityPolicy(max_iterations=10, resource_budget=10)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.5)]
record = engine.evaluate(results, iteration=1, total_runs=3)
assert record.decision == HaltDecision.CONTINUE
class TestMarginalImprovementDecision:
"""Tests for marginal improvement halting."""
def test_no_improvement_halts(self):
"""Test halting when improvement is below threshold."""
policy = QualityPolicy(max_iterations=10, min_improvement=0.05)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.52)]
record = engine.evaluate(
results,
iteration=2,
score_history=[0.50], # improvement: 0.02 < 0.05
)
assert record.decision == HaltDecision.HALT_NO_IMPROVEMENT
def test_sufficient_improvement_continues(self):
"""Test continuing when improvement meets threshold."""
policy = QualityPolicy(max_iterations=10, min_improvement=0.05)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.60)]
record = engine.evaluate(
results,
iteration=2,
score_history=[0.50], # improvement: 0.10 >= 0.05
)
assert record.decision == HaltDecision.CONTINUE
def test_first_iteration_no_history(self):
"""Test first iteration with no history continues."""
policy = QualityPolicy(max_iterations=10, min_improvement=0.05)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.50)]
record = engine.evaluate(results, iteration=1)
assert record.decision == HaltDecision.CONTINUE
class TestPriorityOrder:
"""Tests for the priority order of halting checks."""
def test_budget_checked_before_iteration(self):
"""Test budget exhaustion takes priority over iteration limit."""
policy = QualityPolicy(max_iterations=3, resource_budget=2)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.FAIL, 0.5)]
record = engine.evaluate(results, iteration=3, total_runs=2)
assert record.decision == HaltDecision.HALT_BUDGET_EXHAUSTED
def test_iteration_checked_before_quality(self):
"""Test iteration limit checked before quality met."""
policy = QualityPolicy(max_iterations=2)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.PASS, 1.0)]
# At iteration limit AND quality met — iteration limit wins
# Actually quality is checked after iteration, so quality would be checked
# But iteration 2 >= max 2 triggers first
record = engine.evaluate(results, iteration=2)
assert record.decision == HaltDecision.HALT_ITERATION_LIMIT
class TestHaltingRecord:
"""Tests for HaltingRecord attributes."""
def test_record_has_scores(self):
"""Test halting record includes score history."""
policy = QualityPolicy(max_iterations=5)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.PASS, 0.9)]
record = engine.evaluate(
results, iteration=2, score_history=[0.5]
)
assert record.scores == [0.5, 0.9]
def test_record_to_dict(self):
"""Test halting record serialization."""
policy = QualityPolicy(max_iterations=3)
engine = HaltingPolicyEngine(policy)
results = [_make_result(ValidationStatus.PASS, 1.0)]
record = engine.evaluate(results, iteration=1)
d = record.to_dict()
assert d["decision"] == "halted_quality_met"
assert d["iteration"] == 1
assert d["max_iterations"] == 3