"""Tests for EngineeringValidator and related classes.""" import pytest from pathlib import Path from optimization_engine.interview.engineering_validator import ( MaterialsDatabase, AntiPatternDetector, EngineeringValidator, ValidationResult, AntiPattern, Material, ) from optimization_engine.interview.interview_state import InterviewState class TestMaterial: """Tests for Material dataclass.""" def test_properties(self): """Test material property accessors.""" mat = Material( id="test", names=["test material"], category="test", properties={ "density_kg_m3": 2700, "yield_stress_mpa": 276, "ultimate_stress_mpa": 310, "elastic_modulus_gpa": 69, } ) assert mat.density == 2700 assert mat.yield_stress == 276 assert mat.ultimate_stress == 310 assert mat.elastic_modulus == 69 def test_get_safe_stress(self): """Test getting safe stress with safety factor.""" mat = Material( id="test", names=["test"], category="test", properties={"yield_stress_mpa": 300}, recommended_safety_factors={"static": 1.5, "fatigue": 3.0} ) safe = mat.get_safe_stress("static") assert safe == 200.0 # 300 / 1.5 safe_fatigue = mat.get_safe_stress("fatigue") assert safe_fatigue == 100.0 # 300 / 3.0 class TestMaterialsDatabase: """Tests for MaterialsDatabase.""" def test_load_materials(self): """Test that materials are loaded from JSON.""" db = MaterialsDatabase() assert len(db.materials) > 0 # Check for al_6061_t6 (the actual ID in the database) assert "al_6061_t6" in db.materials def test_get_material_exact(self): """Test exact material lookup.""" db = MaterialsDatabase() mat = db.get_material("al_6061_t6") assert mat is not None assert mat.id == "al_6061_t6" assert mat.yield_stress is not None def test_get_material_by_name(self): """Test material lookup by name.""" db = MaterialsDatabase() # Test lookup by one of the indexed names mat = db.get_material("aluminum 6061-t6") assert mat is not None assert "6061" in mat.id.lower() or "al" in mat.id.lower() def test_get_material_fuzzy(self): """Test fuzzy material matching.""" db = MaterialsDatabase() # Test various ways users might refer to aluminum mat = db.get_material("6061-t6") assert mat is not None def test_get_material_not_found(self): """Test material not found returns None.""" db = MaterialsDatabase() mat = db.get_material("unobtanium") assert mat is None def test_get_yield_stress(self): """Test getting yield stress for material.""" db = MaterialsDatabase() yield_stress = db.get_yield_stress("al_6061_t6") assert yield_stress is not None assert yield_stress > 200 # Al 6061-T6 is ~276 MPa def test_validate_stress_limit_valid(self): """Test stress validation - valid case.""" db = MaterialsDatabase() # Below yield - should pass result = db.validate_stress_limit("al_6061_t6", 200) assert result.valid def test_validate_stress_limit_over_yield(self): """Test stress validation - over yield.""" db = MaterialsDatabase() # Above yield - should have warning result = db.validate_stress_limit("al_6061_t6", 300) # It's valid=True but with warning severity assert result.severity in ["warning", "error"] def test_list_materials(self): """Test listing all materials.""" db = MaterialsDatabase() materials = db.list_materials() assert len(materials) >= 10 # We should have at least 10 materials # Returns Material objects, not strings assert all(isinstance(m, Material) for m in materials) assert any("aluminum" in m.id.lower() or "al" in m.id.lower() for m in materials) def test_list_materials_by_category(self): """Test filtering materials by category.""" db = MaterialsDatabase() steel_materials = db.list_materials(category="steel") assert len(steel_materials) > 0 assert all(m.category == "steel" for m in steel_materials) class TestAntiPatternDetector: """Tests for AntiPatternDetector.""" def test_load_patterns(self): """Test pattern loading from JSON.""" detector = AntiPatternDetector() assert len(detector.patterns) > 0 def test_check_all_mass_no_constraint(self): """Test detection of mass minimization without constraints.""" detector = AntiPatternDetector() state = InterviewState() # Set up mass minimization without constraints state.answers["objectives"] = [{"goal": "minimize_mass"}] state.answers["constraints"] = [] patterns = detector.check_all(state, {}) pattern_ids = [p.id for p in patterns] assert "mass_no_constraint" in pattern_ids def test_check_all_no_pattern_when_constraint_present(self): """Test no pattern when constraints are properly set.""" detector = AntiPatternDetector() state = InterviewState() # Set up mass minimization WITH constraints state.answers["objectives"] = [{"goal": "minimize_mass"}] state.answers["constraints"] = [{"type": "stress", "threshold": 200}] patterns = detector.check_all(state, {}) pattern_ids = [p.id for p in patterns] assert "mass_no_constraint" not in pattern_ids def test_check_all_bounds_too_wide(self): """Test detection of overly wide bounds.""" detector = AntiPatternDetector() state = InterviewState() # Set up design variables with very wide bounds state.answers["design_variables"] = [ {"name": "thickness", "min": 0.1, "max": 100} # 1000x range ] patterns = detector.check_all(state, {}) # Detector runs without error - pattern detection depends on implementation assert isinstance(patterns, list) def test_check_all_too_many_objectives(self): """Test detection of too many objectives.""" detector = AntiPatternDetector() state = InterviewState() # Set up 4 objectives (above recommended 3) state.answers["objectives"] = [ {"goal": "minimize_mass"}, {"goal": "minimize_stress"}, {"goal": "maximize_frequency"}, {"goal": "minimize_displacement"} ] patterns = detector.check_all(state, {}) pattern_ids = [p.id for p in patterns] assert "too_many_objectives" in pattern_ids def test_pattern_has_severity(self): """Test that patterns have correct severity.""" detector = AntiPatternDetector() state = InterviewState() state.answers["objectives"] = [{"goal": "minimize_mass"}] state.answers["constraints"] = [] patterns = detector.check_all(state, {}) mass_pattern = next((p for p in patterns if p.id == "mass_no_constraint"), None) assert mass_pattern is not None assert mass_pattern.severity in ["error", "warning"] def test_pattern_has_fix_suggestion(self): """Test that patterns have fix suggestions.""" detector = AntiPatternDetector() state = InterviewState() state.answers["objectives"] = [{"goal": "minimize_mass"}] state.answers["constraints"] = [] patterns = detector.check_all(state, {}) mass_pattern = next((p for p in patterns if p.id == "mass_no_constraint"), None) assert mass_pattern is not None assert mass_pattern.fix_suggestion is not None assert len(mass_pattern.fix_suggestion) > 0 class TestEngineeringValidator: """Tests for EngineeringValidator.""" def test_validate_constraint_stress(self): """Test stress constraint validation.""" validator = EngineeringValidator() # Valid stress constraint result = validator.validate_constraint( constraint_type="stress", value=200, material="al_6061_t6" ) assert result.valid def test_validate_constraint_displacement(self): """Test displacement constraint validation.""" validator = EngineeringValidator() # Reasonable displacement result = validator.validate_constraint( constraint_type="displacement", value=0.5 ) assert result.valid def test_validate_constraint_frequency(self): """Test frequency constraint validation.""" validator = EngineeringValidator() # Reasonable frequency result = validator.validate_constraint( constraint_type="frequency", value=50 ) assert result.valid def test_suggest_bounds(self): """Test bounds suggestion.""" validator = EngineeringValidator() param_name = "thickness" current_value = 5.0 suggestion = validator.suggest_bounds(param_name, current_value) # Returns tuple (min, max) or dict assert suggestion is not None if isinstance(suggestion, tuple): assert suggestion[0] < current_value assert suggestion[1] > current_value else: assert suggestion["min"] < current_value assert suggestion["max"] > current_value def test_detect_anti_patterns(self): """Test anti-pattern detection via validator.""" validator = EngineeringValidator() state = InterviewState() state.answers["objectives"] = [{"goal": "minimize_mass"}] state.answers["constraints"] = [] patterns = validator.detect_anti_patterns(state, {}) assert len(patterns) > 0 assert any(p.id == "mass_no_constraint" for p in patterns) def test_get_material(self): """Test getting material via validator's materials database.""" validator = EngineeringValidator() mat = validator.materials_db.get_material("al_6061_t6") assert mat is not None assert mat.yield_stress is not None class TestValidationResult: """Tests for ValidationResult dataclass.""" def test_valid_result(self): """Test creating valid result.""" result = ValidationResult(valid=True, message="OK") assert result.valid assert result.message == "OK" assert result.severity == "ok" def test_invalid_result(self): """Test creating invalid result.""" result = ValidationResult( valid=False, message="Stress too high", severity="error", suggestion="Lower the stress limit" ) assert not result.valid assert result.suggestion == "Lower the stress limit" def test_is_blocking(self): """Test is_blocking method.""" blocking = ValidationResult(valid=False, message="Error", severity="error") assert blocking.is_blocking() non_blocking = ValidationResult(valid=True, message="Warning", severity="warning") assert not non_blocking.is_blocking() class TestAntiPattern: """Tests for AntiPattern dataclass.""" def test_anti_pattern_creation(self): """Test creating AntiPattern.""" pattern = AntiPattern( id="test_pattern", name="Test Pattern", description="A test anti-pattern", severity="warning", fix_suggestion="Fix it" ) assert pattern.id == "test_pattern" assert pattern.severity == "warning" assert not pattern.acknowledged def test_acknowledge_pattern(self): """Test acknowledging pattern.""" pattern = AntiPattern( id="test", name="Test", description="Test", severity="error" ) assert not pattern.acknowledged pattern.acknowledged = True assert pattern.acknowledged def test_to_dict(self): """Test conversion to dict.""" pattern = AntiPattern( id="test", name="Test", description="Test desc", severity="warning", fix_suggestion="Do this" ) d = pattern.to_dict() assert d["id"] == "test" assert d["severity"] == "warning" assert d["fix_suggestion"] == "Do this"