Drop fields identified as redundant or low-value from the Product model, API schemas, frontend types, and forms. Raise effect_profile threshold in to_llm_context() from >0 to >=2 to suppress noise values. Remove sku/barcode from LLM context output (kept on model for catalog use). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
232 lines
6.8 KiB
Python
232 lines
6.8 KiB
Python
"""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,
|
|
)
|
|
from innercontext.models.product import (
|
|
ActiveIngredient,
|
|
ProductContext,
|
|
ProductEffectProfile,
|
|
ProductInteraction,
|
|
)
|
|
|
|
|
|
def _make(**kwargs):
|
|
defaults = dict(
|
|
id=uuid4(),
|
|
name="Test",
|
|
brand="B",
|
|
category=ProductCategory.MOISTURIZER,
|
|
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", "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", "url"):
|
|
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", url="https://example.com")
|
|
ctx = p.to_llm_context()
|
|
assert ctx["line_name"] == "Hydrating"
|
|
assert ctx["url"] == "https://example.com"
|
|
assert "sku" not in ctx
|
|
assert "barcode" not in ctx
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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
|