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>
234 lines
7 KiB
Python
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
|