""" Unit tests for quality gate models and individual gate implementations. Tests QualityGate models, SchemaValidationGate, and PatternValidationGate. """ import json import pytest from markitect.prompts.quality.models import ( GateType, ValidationStatus, ValidationDiagnostic, ValidationResult, QualityPolicy, HaltDecision, HaltingRecord, ) from markitect.prompts.quality.gates.schema_gate import SchemaValidationGate from markitect.prompts.quality.gates.pattern_gate import PatternValidationGate class TestValidationDiagnostic: """Tests for ValidationDiagnostic.""" def test_create_diagnostic(self): """Test creating a diagnostic.""" diag = ValidationDiagnostic( code="TEST_ERROR", message="Something went wrong", severity="error", ) assert diag.code == "TEST_ERROR" assert diag.severity == "error" def test_to_dict(self): """Test diagnostic serialization.""" diag = ValidationDiagnostic(code="E1", message="msg", severity="warning") d = diag.to_dict() assert d["code"] == "E1" assert d["severity"] == "warning" def test_from_dict(self): """Test diagnostic deserialization.""" data = {"code": "E1", "message": "msg", "severity": "info"} diag = ValidationDiagnostic.from_dict(data) assert diag.code == "E1" assert diag.severity == "info" class TestValidationResult: """Tests for ValidationResult.""" def test_create_result(self): """Test creating a validation result.""" result = ValidationResult.create( gate_id="gate-1", gate_type=GateType.SCHEMA, artifact_id="art-1", status=ValidationStatus.PASS, score=1.0, ) assert result.gate_id == "gate-1" assert result.status == ValidationStatus.PASS assert result.score == 1.0 def test_result_unique_ids(self): """Test that each result gets a unique ID.""" r1 = ValidationResult.create( gate_id="g", gate_type=GateType.PATTERN, artifact_id="a", status=ValidationStatus.PASS, ) r2 = ValidationResult.create( gate_id="g", gate_type=GateType.PATTERN, artifact_id="a", status=ValidationStatus.PASS, ) assert r1.id != r2.id def test_to_dict_from_dict(self): """Test round-trip serialization.""" result = ValidationResult.create( gate_id="g1", gate_type=GateType.SCHEMA, artifact_id="a1", status=ValidationStatus.FAIL, score=0.5, diagnostics=[ ValidationDiagnostic(code="E1", message="err", severity="error"), ], ) d = result.to_dict() restored = ValidationResult.from_dict(d) assert restored.id == result.id assert restored.status == ValidationStatus.FAIL assert restored.score == 0.5 assert len(restored.diagnostics) == 1 class TestQualityPolicy: """Tests for QualityPolicy.""" def test_default_policy(self): """Test default policy values.""" policy = QualityPolicy() assert policy.max_iterations == 3 assert policy.min_improvement == 0.05 assert policy.fail_on_gate_failure is True assert policy.resource_budget == 10 def test_to_dict_from_dict(self): """Test round-trip serialization.""" policy = QualityPolicy( max_iterations=5, min_improvement=0.1, required_gate_ids=["g1", "g2"], ) d = policy.to_dict() restored = QualityPolicy.from_dict(d) assert restored.max_iterations == 5 assert restored.min_improvement == 0.1 assert restored.required_gate_ids == ["g1", "g2"] class TestSchemaValidationGate: """Tests for SchemaValidationGate.""" def test_valid_json_passes(self): """Test valid JSON against schema passes.""" schema = { "type": "object", "required": ["name", "version"], "properties": { "name": {"type": "string"}, "version": {"type": "integer"}, }, } gate = SchemaValidationGate(schema=schema, name="test-schema") content = json.dumps({"name": "test", "version": 1}) result = gate.validate(content, "art-1") assert result.status == ValidationStatus.PASS assert result.score == 1.0 assert len(result.diagnostics) == 0 def test_missing_required_field_fails(self): """Test missing required field fails validation.""" schema = { "type": "object", "required": ["name", "version"], "properties": { "name": {"type": "string"}, "version": {"type": "integer"}, }, } gate = SchemaValidationGate(schema=schema) content = json.dumps({"name": "test"}) result = gate.validate(content, "art-1") assert result.status == ValidationStatus.FAIL assert any(d.code == "SCHEMA_VIOLATION" for d in result.diagnostics) def test_wrong_type_fails(self): """Test wrong type fails validation.""" schema = { "type": "object", "properties": { "count": {"type": "integer"}, }, } gate = SchemaValidationGate(schema=schema) content = json.dumps({"count": "not-a-number"}) result = gate.validate(content, "art-1") assert result.status == ValidationStatus.FAIL def test_invalid_json_fails(self): """Test invalid JSON content fails.""" schema = {"type": "object"} gate = SchemaValidationGate(schema=schema) result = gate.validate("not json {{{", "art-1") assert result.status == ValidationStatus.FAIL assert result.score == 0.0 assert any(d.code == "INVALID_JSON" for d in result.diagnostics) def test_gate_has_correct_type(self): """Test gate type is SCHEMA.""" gate = SchemaValidationGate(schema={"type": "object"}) assert gate.gate_type == GateType.SCHEMA def test_empty_schema_passes_any_object(self): """Test empty schema passes any valid JSON.""" gate = SchemaValidationGate(schema={}) result = gate.validate(json.dumps({"any": "thing"}), "art-1") assert result.status == ValidationStatus.PASS def test_score_reflects_error_count(self): """Test that score decreases with more errors.""" schema = { "type": "object", "required": ["a", "b", "c"], "properties": { "a": {"type": "string"}, "b": {"type": "string"}, "c": {"type": "string"}, }, } gate = SchemaValidationGate(schema=schema) # Missing all 3 required fields result = gate.validate(json.dumps({}), "art-1") assert result.status == ValidationStatus.FAIL assert result.score < 1.0 class TestPatternValidationGate: """Tests for PatternValidationGate.""" def test_required_pattern_present(self): """Test content with required patterns passes.""" gate = PatternValidationGate( required_patterns=[r"## Endpoints", r"### Authentication"], ) content = "# API\n## Endpoints\n### Authentication\nDetails here." result = gate.validate(content, "art-1") assert result.status == ValidationStatus.PASS assert result.score == 1.0 def test_required_pattern_missing(self): """Test missing required pattern fails.""" gate = PatternValidationGate( required_patterns=[r"## Endpoints", r"### Authentication"], ) content = "# API\n## Endpoints\nNo auth section." result = gate.validate(content, "art-1") assert result.status == ValidationStatus.FAIL assert any(d.code == "MISSING_PATTERN" for d in result.diagnostics) def test_forbidden_pattern_absent(self): """Test content without forbidden patterns passes.""" gate = PatternValidationGate( forbidden_patterns=[r"TODO", r"FIXME"], ) content = "This is a clean document." result = gate.validate(content, "art-1") assert result.status == ValidationStatus.PASS def test_forbidden_pattern_present(self): """Test content with forbidden pattern fails.""" gate = PatternValidationGate( forbidden_patterns=[r"TODO", r"FIXME"], ) content = "This needs work. TODO: fix this." result = gate.validate(content, "art-1") assert result.status == ValidationStatus.FAIL assert any(d.code == "FORBIDDEN_PATTERN" for d in result.diagnostics) def test_combined_required_and_forbidden(self): """Test both required and forbidden patterns together.""" gate = PatternValidationGate( required_patterns=[r"## Summary"], forbidden_patterns=[r"FIXME"], ) # Has required, no forbidden result1 = gate.validate("## Summary\nAll good.", "art-1") assert result1.status == ValidationStatus.PASS # Has required AND forbidden result2 = gate.validate("## Summary\nFIXME: broken.", "art-1") assert result2.status == ValidationStatus.FAIL def test_no_patterns_passes(self): """Test gate with no patterns always passes.""" gate = PatternValidationGate() result = gate.validate("anything", "art-1") assert result.status == ValidationStatus.PASS def test_gate_has_correct_type(self): """Test gate type is PATTERN.""" gate = PatternValidationGate() assert gate.gate_type == GateType.PATTERN def test_score_proportional_to_failures(self): """Test score is proportional to number of checks passed.""" gate = PatternValidationGate( required_patterns=[r"A", r"B", r"C", r"D"], ) # Only A is present → 3 out of 4 fail result = gate.validate("A", "art-1") assert result.status == ValidationStatus.FAIL assert 0.0 < result.score < 1.0 def test_regex_pattern_matching(self): """Test regex patterns work correctly.""" gate = PatternValidationGate( required_patterns=[r"\d{3}-\d{4}"], ) result = gate.validate("Call 555-1234 for info", "art-1") assert result.status == ValidationStatus.PASS