innercontext/backend/tests/test_product_model.py
Piotr Oleszczyk 6333c6678a refactor: remove personal_rating, DRY get_or_404, fix ty errors
- Drop Product.personal_rating from model, API schemas, and all frontend
  views (list table, detail view, quick-edit form, new-product form)
- Extract get_or_404 into backend/innercontext/api/utils.py; remove five
  duplicate copies from individual API modules
- Fix all ty type errors: generic get_or_404 with TypeVar, cast() in
  coerce_effect_profile validator, col() for ilike on SQLModel column,
  dict[str, Any] annotation in test helper, ty: ignore for CORSMiddleware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 11:20:13 +01:00

234 lines
6.9 KiB
Python

"""Unit tests for Product.to_llm_context() — no database required."""
from uuid import uuid4
from typing import Any
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: Any) -> Product:
defaults: dict[str, Any] = 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