"""Tests for RoutineSuggestionValidator.""" from datetime import date, timedelta from uuid import uuid4 from innercontext.validators.routine_validator import ( RoutineSuggestionValidator, RoutineValidationContext, ) # Helper to create mock product class MockProduct: def __init__( self, product_id, name, actives=None, effect_profile=None, context_rules=None, min_interval_hours=None, category="serum", ): self.id = product_id self.name = name self.actives = actives or [] self.effect_profile = effect_profile self.context_rules = context_rules self.min_interval_hours = min_interval_hours self.category = category # Helper to create mock active ingredient class MockActive: def __init__(self, functions): self.functions = functions # Helper to create mock effect profile class MockEffectProfile: def __init__( self, retinoid_strength=0, exfoliation_strength=0, barrier_disruption_risk=0, irritation_risk=0, ): self.retinoid_strength = retinoid_strength self.exfoliation_strength = exfoliation_strength self.barrier_disruption_risk = barrier_disruption_risk self.irritation_risk = irritation_risk # Helper to create mock context rules class MockContextRules: def __init__(self, safe_after_shaving=True, safe_with_compromised_barrier=True): self.safe_after_shaving = safe_after_shaving self.safe_with_compromised_barrier = safe_with_compromised_barrier # Helper to create mock routine step class MockStep: def __init__(self, product_id=None, action_type=None, **kwargs): self.product_id = product_id self.action_type = action_type for key, value in kwargs.items(): setattr(self, key, value) # Helper to create mock routine response class MockRoutine: def __init__(self, steps): self.steps = steps def test_detects_retinoid_acid_conflict(): """Validator catches retinoid + AHA/BHA in same routine.""" # Setup retinoid_id = uuid4() acid_id = uuid4() retinoid = MockProduct( retinoid_id, "Retinoid Serum", actives=[MockActive(functions=["retinoid"])], effect_profile=MockEffectProfile(retinoid_strength=3), ) acid = MockProduct( acid_id, "AHA Toner", actives=[MockActive(functions=["exfoliant_aha"])], effect_profile=MockEffectProfile(exfoliation_strength=4), ) context = RoutineValidationContext( valid_product_ids={retinoid_id, acid_id}, routine_date=date.today(), part_of_day="pm", leaving_home=None, barrier_state="intact", products_by_id={retinoid_id: retinoid, acid_id: acid}, last_used_dates={}, ) routine = MockRoutine( steps=[ MockStep(product_id=retinoid_id), MockStep(product_id=acid_id), ] ) # Execute validator = RoutineSuggestionValidator() result = validator.validate(routine, context) # Assert assert not result.is_valid assert any( "retinoid" in err.lower() and "acid" in err.lower() for err in result.errors ) def test_rejects_unknown_product_ids(): """Validator catches UUIDs not in database.""" known_id = uuid4() unknown_id = uuid4() product = MockProduct(known_id, "Known Product") context = RoutineValidationContext( valid_product_ids={known_id}, # Only known_id is valid routine_date=date.today(), part_of_day="am", leaving_home=None, barrier_state="intact", products_by_id={known_id: product}, last_used_dates={}, ) routine = MockRoutine( steps=[ MockStep(product_id=unknown_id), # This ID doesn't exist ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert not result.is_valid assert any("unknown" in err.lower() for err in result.errors) def test_enforces_min_interval_hours(): """Validator catches product used within min_interval.""" product_id = uuid4() product = MockProduct( product_id, "High Frequency Product", min_interval_hours=48, # Must wait 48 hours ) today = date.today() yesterday = today - timedelta(days=1) # Only 24 hours ago context = RoutineValidationContext( valid_product_ids={product_id}, routine_date=today, part_of_day="am", leaving_home=None, barrier_state="intact", products_by_id={product_id: product}, last_used_dates={product_id: yesterday}, # Used yesterday ) routine = MockRoutine( steps=[ MockStep(product_id=product_id), ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert not result.is_valid assert any( "interval" in err.lower() or "recently" in err.lower() for err in result.errors ) def test_blocks_dose_field(): """Validator rejects responses with prohibited 'dose' field.""" product_id = uuid4() product = MockProduct(product_id, "Product") context = RoutineValidationContext( valid_product_ids={product_id}, routine_date=date.today(), part_of_day="am", leaving_home=None, barrier_state="intact", products_by_id={product_id: product}, last_used_dates={}, ) # Step with prohibited 'dose' field step_with_dose = MockStep(product_id=product_id, dose="2 drops") routine = MockRoutine(steps=[step_with_dose]) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert not result.is_valid assert any( "dose" in err.lower() and "prohibited" in err.lower() for err in result.errors ) def test_missing_spf_in_am_leaving_home(): """Validator warns when no SPF despite leaving home.""" product_id = uuid4() product = MockProduct(product_id, "Moisturizer", category="moisturizer") context = RoutineValidationContext( valid_product_ids={product_id}, routine_date=date.today(), part_of_day="am", leaving_home=True, # User is leaving home barrier_state="intact", products_by_id={product_id: product}, last_used_dates={}, ) routine = MockRoutine( steps=[ MockStep(product_id=product_id), # No SPF product ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) # Should pass validation but have warnings assert result.is_valid assert len(result.warnings) > 0 assert any("spf" in warn.lower() for warn in result.warnings) def test_compromised_barrier_restrictions(): """Validator blocks high-risk actives with compromised barrier.""" product_id = uuid4() harsh_product = MockProduct( product_id, "Harsh Acid", effect_profile=MockEffectProfile( barrier_disruption_risk=5, # Very high risk irritation_risk=4, ), context_rules=MockContextRules(safe_with_compromised_barrier=False), ) context = RoutineValidationContext( valid_product_ids={product_id}, routine_date=date.today(), part_of_day="pm", leaving_home=None, barrier_state="compromised", # Barrier is compromised products_by_id={product_id: harsh_product}, last_used_dates={}, ) routine = MockRoutine( steps=[ MockStep(product_id=product_id), ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert not result.is_valid assert any( "barrier" in err.lower() and "safety" in err.lower() for err in result.errors ) def test_step_must_have_product_or_action(): """Validator rejects steps with neither product_id nor action_type.""" context = RoutineValidationContext( valid_product_ids=set(), routine_date=date.today(), part_of_day="am", leaving_home=None, barrier_state="intact", products_by_id={}, last_used_dates={}, ) # Empty step (neither product nor action) routine = MockRoutine( steps=[ MockStep(product_id=None, action_type=None), ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert not result.is_valid assert any("product_id" in err and "action_type" in err for err in result.errors) def test_step_cannot_have_both_product_and_action(): """Validator rejects steps with both product_id and action_type.""" product_id = uuid4() product = MockProduct(product_id, "Product") context = RoutineValidationContext( valid_product_ids={product_id}, routine_date=date.today(), part_of_day="am", leaving_home=None, barrier_state="intact", products_by_id={product_id: product}, last_used_dates={}, ) # Step with both product_id AND action_type (invalid) routine = MockRoutine( steps=[ MockStep(product_id=product_id, action_type="shaving"), ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert not result.is_valid assert any("cannot have both" in err.lower() for err in result.errors) def test_accepts_valid_routine(): """Validator accepts a properly formed safe routine.""" cleanser_id = uuid4() moisturizer_id = uuid4() spf_id = uuid4() cleanser = MockProduct(cleanser_id, "Cleanser", category="cleanser") moisturizer = MockProduct(moisturizer_id, "Moisturizer", category="moisturizer") spf = MockProduct(spf_id, "SPF", category="spf") context = RoutineValidationContext( valid_product_ids={cleanser_id, moisturizer_id, spf_id}, routine_date=date.today(), part_of_day="am", leaving_home=True, barrier_state="intact", products_by_id={ cleanser_id: cleanser, moisturizer_id: moisturizer, spf_id: spf, }, last_used_dates={}, ) routine = MockRoutine( steps=[ MockStep(product_id=cleanser_id), MockStep(product_id=moisturizer_id), MockStep(product_id=spf_id), ] ) validator = RoutineSuggestionValidator() result = validator.validate(routine, context) assert result.is_valid assert len(result.errors) == 0