""" Unit tests for schema_refiner module (Phase 2 schema refinement). """ import pytest import json import copy from markitect.schema_refiner import ( SchemaRefiner, RefinementResult, RefinementAction ) from markitect.schema_analyzer import IssueType class TestSchemaRefiner: """Tests for SchemaRefiner class.""" def test_refine_exact_count_array(self): """Test refinement of exact array counts.""" schema = { "type": "object", "properties": { "items": { "type": "array", "minItems": 5, "maxItems": 5 } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success assert len(result.actions_taken) > 0 # Check that the array range was loosened refined_items = result.refined_schema["properties"]["items"] assert refined_items["minItems"] < 5 assert refined_items["maxItems"] > 5 def test_refine_const_value(self): """Test refinement of const constraints.""" schema = { "type": "object", "properties": { "level": { "type": "integer", "const": 1 } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success assert len(result.actions_taken) > 0 # const should be removed and replaced with a range refined_level = result.refined_schema["properties"]["level"] assert "const" not in refined_level assert "minimum" in refined_level assert "maximum" in refined_level def test_refine_overly_specific_number(self): """Test rounding of overly specific numbers.""" schema = { "type": "object", "properties": { "items": { "type": "array", "minItems": 73 } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, round_numbers=True) assert result.success # Should round to 70 if len(result.actions_taken) > 0: refined_items = result.refined_schema["properties"]["items"] assert refined_items["minItems"] == 70 def test_refine_narrow_range(self): """Test widening of narrow integer ranges.""" schema = { "type": "object", "properties": { "score": { "type": "integer", "minimum": 5, "maximum": 6 } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success # Range should be widened if len(result.actions_taken) > 0: refined_score = result.refined_schema["properties"]["score"] range_size = refined_score["maximum"] - refined_score["minimum"] assert range_size > 1 def test_refine_nested_properties(self): """Test refinement of nested property structures.""" schema = { "type": "object", "properties": { "outer": { "type": "object", "properties": { "inner": { "type": "array", "minItems": 3, "maxItems": 3 } } } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success assert len(result.actions_taken) > 0 # Check nested property was refined refined_inner = result.refined_schema["properties"]["outer"]["properties"]["inner"] assert refined_inner["minItems"] < 3 assert refined_inner["maxItems"] > 3 def test_refine_array_items_with_const(self): """Test refinement of array items with const properties.""" schema = { "type": "object", "properties": { "headings": { "type": "array", "items": { "type": "object", "properties": { "level": { "type": "integer", "const": 1 } } } } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success assert len(result.actions_taken) > 0 # const in items should be refined refined_level = result.refined_schema["properties"]["headings"]["items"]["properties"]["level"] assert "const" not in refined_level def test_refine_no_changes_needed(self): """Test refinement of already flexible schema.""" schema = { "type": "object", "x-markitect-sections": { "INTRO": {"classification": "required"} }, "x-markitect-content-control": { "intro": {"content_quality": {"min_words": 50}} }, "properties": { "items": { "type": "array", "minItems": 5, "maxItems": 50 # Good range } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success # May have some minor improvements but should be mostly unchanged assert len(result.actions_taken) < 3 def test_refine_with_disabled_options(self): """Test refinement with options disabled.""" schema = { "type": "object", "properties": { "items": { "type": "array", "minItems": 5, "maxItems": 5 }, "count": { "type": "integer", "const": 73 } } } refiner = SchemaRefiner() result = refiner.refine_schema( schema, loosen_counts=False, # Disabled round_numbers=False ) assert result.success # No changes should be made since options are disabled assert len(result.actions_taken) == 0 def test_refinement_action_details(self): """Test that refinement actions contain proper details.""" schema = { "type": "object", "properties": { "items": { "type": "array", "minItems": 5, "maxItems": 5 } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert len(result.actions_taken) > 0 action = result.actions_taken[0] assert isinstance(action, RefinementAction) assert action.issue_type == IssueType.EXACT_COUNT assert "properties.items" in action.path assert action.old_value is not None assert action.new_value is not None assert "loosened" in action.description.lower() or "converted" in action.description.lower() def test_original_schema_unchanged(self): """Test that original schema is not modified.""" schema = { "type": "object", "properties": { "items": { "type": "array", "minItems": 5, "maxItems": 5 } } } original_schema = copy.deepcopy(schema) refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) # Original should be unchanged assert schema == original_schema # But refined should be different assert result.refined_schema != original_schema def test_format_refinement_report(self): """Test refinement report formatting.""" schema = { "type": "object", "properties": { "items": { "type": "array", "minItems": 5, "maxItems": 5 } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) report = refiner.format_refinement_report(result) assert "Schema Refinement Report" in report assert "Actions Taken" in report or "No refinements needed" in report def test_refinement_with_multiple_issues(self): """Test refinement of schema with multiple issues.""" schema = { "type": "object", "properties": { "array1": { "type": "array", "minItems": 1, "maxItems": 1 }, "array2": { "type": "array", "minItems": 73 }, "level": { "type": "integer", "const": 2 } } } refiner = SchemaRefiner() result = refiner.refine_schema( schema, loosen_counts=True, round_numbers=True ) assert result.success assert len(result.actions_taken) >= 2 # Should fix multiple issues def test_navigation_to_deeply_nested_path(self): """Test path navigation for deeply nested schemas.""" schema = { "type": "object", "properties": { "level1": { "type": "object", "properties": { "level2": { "type": "object", "properties": { "level3": { "type": "array", "minItems": 1, "maxItems": 1 } } } } } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success # Should successfully navigate and refine deep path refined_level3 = result.refined_schema["properties"]["level1"]["properties"]["level2"]["properties"]["level3"] assert refined_level3["minItems"] < 1 or refined_level3["maxItems"] > 1 def test_deprecated_extension_detection(self): """Test detection (but not automatic migration) of deprecated extensions.""" schema = { "type": "object", "x-markitect-required-sections": ["INTRO"] } refiner = SchemaRefiner() result = refiner.refine_schema(schema, migrate_deprecated=True) assert result.success # Should document deprecated extension but not remove it automatically deprecated_actions = [a for a in result.actions_taken if a.issue_type == IssueType.DEPRECATED_EXTENSIONS] # Migration is detected but not fully automated (too risky) assert len(deprecated_actions) >= 0 def test_refine_empty_schema(self): """Test refinement of minimal schema.""" schema = { "type": "object" } refiner = SchemaRefiner() result = refiner.refine_schema(schema) assert result.success # Minimal schema shouldn't crash the refiner assert result.refined_schema is not None def test_refine_schema_with_string_const(self): """Test refinement of non-numeric const values.""" schema = { "type": "object", "properties": { "status": { "type": "string", "const": "active" } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success # String const should be removed (can't be converted to range) if len(result.actions_taken) > 0: refined_status = result.refined_schema["properties"]["status"] assert "const" not in refined_status def test_complex_manpage_schema(self): """Test refinement of a realistic manpage schema.""" schema = { "type": "object", "properties": { "headings": { "type": "object", "properties": { "level_1": { "type": "array", "minItems": 1, "maxItems": 1, "items": { "type": "object", "properties": { "level": { "type": "integer", "const": 1 } } } }, "level_2": { "type": "array", "minItems": 3, "maxItems": 30, "items": { "type": "object", "properties": { "level": { "type": "integer", "const": 2 } } } } } } } } refiner = SchemaRefiner() result = refiner.refine_schema(schema, loosen_counts=True) assert result.success assert len(result.actions_taken) >= 2 # Should fix at least the exact counts # level_1 should be loosened refined_level_1 = result.refined_schema["properties"]["headings"]["properties"]["level_1"] assert refined_level_1["minItems"] < 1 or refined_level_1["maxItems"] > 1 # const values in items should be loosened items_level_1 = refined_level_1["items"]["properties"]["level"] assert "const" not in items_level_1