innercontext/backend/tests/test_product_model.py
Piotr Oleszczyk 8f7d893a63 Initial commit: backend API, data models, and test suite
FastAPI backend for personal health and skincare data with MCP export.
Includes SQLModel models for products, inventory, medications, lab results,
routines, and skin condition snapshots. Pytest suite with 111 tests running
on SQLite in-memory (no PostgreSQL required).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 15:10:24 +01:00

234 lines
7 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,
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