"""Unit tests for Product.to_llm_context() — no database required.""" from uuid import uuid4 import pytest from innercontext.models import Product from innercontext.models.enums import ( DayTime, IngredientFunction, InteractionScope, ProductCategory, RoutineRole, ) from innercontext.models.product import ( ActiveIngredient, ProductContext, ProductEffectProfile, ProductInteraction, ) def _make(**kwargs): defaults = dict( id=uuid4(), name="Test", brand="B", category=ProductCategory.MOISTURIZER, routine_role=RoutineRole.SEAL, recommended_time=DayTime.BOTH, leave_on=True, ) defaults.update(kwargs) return Product(**defaults) # --------------------------------------------------------------------------- # Always-present keys # --------------------------------------------------------------------------- def test_always_present_keys(): p = _make() ctx = p.to_llm_context() for key in ("id", "name", "brand", "category", "routine_role", "recommended_time", "leave_on"): assert key in ctx, f"Expected '{key}' in to_llm_context() output" # --------------------------------------------------------------------------- # Optional string fields # --------------------------------------------------------------------------- def test_optional_string_fields_absent_when_none(): p = _make() ctx = p.to_llm_context() for key in ("line_name", "sku", "url", "barcode"): assert key not in ctx, f"'{key}' should not appear when None" def test_optional_string_fields_present_when_set(): p = _make(line_name="Hydrating", sku="CV-001", url="https://example.com", barcode="123456") ctx = p.to_llm_context() assert ctx["line_name"] == "Hydrating" assert ctx["sku"] == "CV-001" assert ctx["url"] == "https://example.com" assert ctx["barcode"] == "123456" # --------------------------------------------------------------------------- # pH handling # --------------------------------------------------------------------------- def test_ph_exact_collapses(): p = _make(ph_min=5.5, ph_max=5.5) ctx = p.to_llm_context() assert "ph" in ctx assert ctx["ph"] == 5.5 assert "ph_range" not in ctx assert "ph_min" not in ctx assert "ph_max" not in ctx def test_ph_range(): p = _make(ph_min=4.0, ph_max=6.0) ctx = p.to_llm_context() assert "ph_range" in ctx assert "4.0" in ctx["ph_range"] assert "6.0" in ctx["ph_range"] assert "ph" not in ctx def test_ph_only_min(): p = _make(ph_min=3.5, ph_max=None) ctx = p.to_llm_context() assert "ph_min" in ctx assert ctx["ph_min"] == 3.5 assert "ph_range" not in ctx assert "ph" not in ctx def test_ph_only_max(): p = _make(ph_min=None, ph_max=7.0) ctx = p.to_llm_context() assert "ph_max" in ctx assert ctx["ph_max"] == 7.0 assert "ph_range" not in ctx assert "ph" not in ctx # --------------------------------------------------------------------------- # Actives # --------------------------------------------------------------------------- def test_actives_pydantic_objects(): ai = ActiveIngredient( name="Niacinamide", percent=10.0, functions=[IngredientFunction.NIACINAMIDE], ) p = _make(actives=[ai]) ctx = p.to_llm_context() assert "actives" in ctx a = ctx["actives"][0] assert a["name"] == "Niacinamide" assert a["percent"] == 10.0 assert "niacinamide" in a["functions"] def test_actives_raw_dicts(): raw = {"name": "Retinol", "percent": 0.1, "functions": ["retinoid"]} p = _make(actives=[raw]) ctx = p.to_llm_context() assert "actives" in ctx assert ctx["actives"][0] == raw # --------------------------------------------------------------------------- # Effect profile # --------------------------------------------------------------------------- def test_effect_profile_all_zeros_omitted(): p = _make(product_effect_profile=ProductEffectProfile()) ctx = p.to_llm_context() assert "effect_profile" not in ctx def test_effect_profile_nonzero_included(): ep = ProductEffectProfile(hydration_immediate=4, barrier_repair_strength=3) p = _make(product_effect_profile=ep) ctx = p.to_llm_context() assert "effect_profile" in ctx assert ctx["effect_profile"]["hydration_immediate"] == 4 assert ctx["effect_profile"]["barrier_repair_strength"] == 3 # Zero fields should not be present assert "retinoid_strength" not in ctx["effect_profile"] # --------------------------------------------------------------------------- # Incompatible_with # --------------------------------------------------------------------------- def test_incompatible_with_pydantic_objects(): inc = ProductInteraction( target="AHA", scope=InteractionScope.SAME_DAY, reason="increases irritation" ) p = _make(incompatible_with=[inc]) ctx = p.to_llm_context() assert "incompatible_with" in ctx assert ctx["incompatible_with"][0] == "avoid AHA (same_day): increases irritation" def test_incompatible_with_raw_dicts(): raw = {"target": "Vitamin C", "scope": "same_step", "reason": None} p = _make(incompatible_with=[raw]) ctx = p.to_llm_context() assert "incompatible_with" in ctx assert ctx["incompatible_with"][0] == "avoid Vitamin C (same_step)" # --------------------------------------------------------------------------- # Context rules # --------------------------------------------------------------------------- def test_context_rules_all_none_omitted(): p = _make(context_rules=ProductContext()) ctx = p.to_llm_context() assert "context_rules" not in ctx def test_context_rules_with_value(): p = _make(context_rules=ProductContext(safe_after_shaving=True, low_uv_only=True)) ctx = p.to_llm_context() assert "context_rules" in ctx assert ctx["context_rules"]["safe_after_shaving"] is True assert ctx["context_rules"]["low_uv_only"] is True assert "safe_after_acids" not in ctx["context_rules"] # --------------------------------------------------------------------------- # Safety dict # --------------------------------------------------------------------------- def test_safety_dict_present_when_set(): p = _make(fragrance_free=True, alcohol_denat_free=True) ctx = p.to_llm_context() assert "safety" in ctx assert ctx["safety"]["fragrance_free"] is True assert ctx["safety"]["alcohol_denat_free"] is True # --------------------------------------------------------------------------- # Empty vs non-empty lists # --------------------------------------------------------------------------- def test_empty_lists_omitted(): p = _make(inci=[], targets=[]) ctx = p.to_llm_context() assert "inci" not in ctx assert "targets" not in ctx def test_nonempty_lists_included(): p = _make(inci=["Water", "Glycerin"], targets=["acne", "redness"]) ctx = p.to_llm_context() assert "inci" in ctx assert ctx["inci"] == ["Water", "Glycerin"] assert "targets" in ctx