177 lines
5.4 KiB
Python
177 lines
5.4 KiB
Python
import uuid
|
|
from datetime import date
|
|
from unittest.mock import patch
|
|
|
|
from sqlmodel import Session
|
|
|
|
from innercontext.api.products import (
|
|
_build_shopping_context,
|
|
_extract_requested_product_ids,
|
|
build_product_details_tool_handler,
|
|
)
|
|
from innercontext.models import (
|
|
Product,
|
|
ProductInventory,
|
|
SexAtBirth,
|
|
SkinConditionSnapshot,
|
|
)
|
|
from innercontext.models.profile import UserProfile
|
|
|
|
|
|
def test_build_shopping_context(session: Session):
|
|
# Empty context
|
|
ctx = _build_shopping_context(session, reference_date=date.today())
|
|
assert "USER PROFILE: no data" in ctx
|
|
assert "(brak danych)" in ctx
|
|
assert "POSIADANE PRODUKTY" in ctx
|
|
|
|
profile = UserProfile(birth_date=date(1990, 1, 10), sex_at_birth=SexAtBirth.MALE)
|
|
session.add(profile)
|
|
session.commit()
|
|
|
|
# Add snapshot
|
|
snap = SkinConditionSnapshot(
|
|
id=uuid.uuid4(),
|
|
snapshot_date=date.today(),
|
|
overall_state="fair",
|
|
skin_type="combination",
|
|
hydration_level=3,
|
|
sensitivity_level=4,
|
|
barrier_state="mildly_compromised",
|
|
active_concerns=["redness"],
|
|
priorities=["soothing"],
|
|
)
|
|
session.add(snap)
|
|
|
|
# Add product
|
|
p = Product(
|
|
id=uuid.uuid4(),
|
|
name="Soothing Serum",
|
|
brand="BrandX",
|
|
category="serum",
|
|
recommended_time="both",
|
|
leave_on=True,
|
|
targets=["redness"],
|
|
product_effect_profile={"soothing_strength": 4, "hydration_immediate": 1},
|
|
actives=[{"name": "Centella"}],
|
|
)
|
|
session.add(p)
|
|
session.commit()
|
|
|
|
# Add inventory
|
|
inv = ProductInventory(id=uuid.uuid4(), product_id=p.id, is_opened=True)
|
|
session.add(inv)
|
|
session.commit()
|
|
|
|
ctx = _build_shopping_context(session, reference_date=date(2026, 3, 5))
|
|
assert "USER PROFILE:" in ctx
|
|
assert "Age: 36" in ctx
|
|
assert "Sex at birth: male" in ctx
|
|
assert "Typ skóry: combination" in ctx
|
|
assert "Nawilżenie: 3/5" in ctx
|
|
assert "Wrażliwość: 4/5" in ctx
|
|
assert "Aktywne problemy: redness" in ctx
|
|
assert "Priorytety: soothing" in ctx
|
|
|
|
# Check product
|
|
assert "[✓] id=" in ctx
|
|
assert "Soothing Serum" in ctx
|
|
assert f"id={p.id}" in ctx
|
|
assert "BrandX" in ctx
|
|
assert "targets: ['redness']" in ctx
|
|
assert "actives: ['Centella']" in ctx
|
|
assert "effects: {'soothing': 4}" in ctx
|
|
|
|
|
|
def test_suggest_shopping(client, session):
|
|
with patch(
|
|
"innercontext.api.products.call_gemini_with_function_tools"
|
|
) as mock_gemini:
|
|
mock_response = type(
|
|
"Response",
|
|
(),
|
|
{
|
|
"text": '{"suggestions": [{"category": "cleanser", "product_type": "cleanser", "priority": "high", "key_ingredients": [], "target_concerns": [], "why_needed": "reason", "recommended_time": "am", "frequency": "daily"}], "reasoning": "Test shopping"}'
|
|
},
|
|
)
|
|
mock_gemini.return_value = mock_response
|
|
|
|
r = client.post("/products/suggest")
|
|
assert r.status_code == 200
|
|
data = r.json()
|
|
assert len(data["suggestions"]) == 1
|
|
assert data["suggestions"][0]["product_type"] == "cleanser"
|
|
assert data["reasoning"] == "Test shopping"
|
|
kwargs = mock_gemini.call_args.kwargs
|
|
assert "USER PROFILE:" in kwargs["contents"]
|
|
assert "function_handlers" in kwargs
|
|
assert "get_product_details" in kwargs["function_handlers"]
|
|
|
|
|
|
def test_shopping_context_medication_skip(session: Session):
|
|
p = Product(
|
|
id=uuid.uuid4(),
|
|
name="Epiduo",
|
|
brand="Galderma",
|
|
category="serum",
|
|
recommended_time="pm",
|
|
leave_on=True,
|
|
is_medication=True,
|
|
product_effect_profile={},
|
|
)
|
|
session.add(p)
|
|
session.commit()
|
|
|
|
ctx = _build_shopping_context(session, reference_date=date.today())
|
|
assert "Epiduo" not in ctx
|
|
|
|
|
|
def test_extract_requested_product_ids_dedupes_and_limits():
|
|
ids = _extract_requested_product_ids(
|
|
{"product_ids": ["a", "b", "a", 1, "c", "d"]},
|
|
max_ids=3,
|
|
)
|
|
assert ids == ["a", "b", "c"]
|
|
|
|
|
|
def test_shopping_tool_handlers_return_payloads(session: Session):
|
|
product = Product(
|
|
id=uuid.uuid4(),
|
|
name="Test Product",
|
|
brand="Brand",
|
|
category="serum",
|
|
recommended_time="both",
|
|
leave_on=True,
|
|
inci=["Water", "Niacinamide"],
|
|
actives=[{"name": "Niacinamide", "percent": 5, "functions": ["niacinamide"]}],
|
|
context_rules={"safe_after_shaving": True},
|
|
product_effect_profile={},
|
|
)
|
|
|
|
payload = {"product_ids": [str(product.id)]}
|
|
|
|
details = build_product_details_tool_handler([product])(payload)
|
|
assert details["products"][0]["inci"] == ["Water", "Niacinamide"]
|
|
assert details["products"][0]["actives"][0]["name"] == "Niacinamide"
|
|
assert "context_rules" in details["products"][0]
|
|
assert details["products"][0]["last_used_on"] is None
|
|
|
|
|
|
def test_shopping_tool_handler_includes_last_used_on_from_mapping(session: Session):
|
|
product = Product(
|
|
id=uuid.uuid4(),
|
|
name="Mapped Product",
|
|
brand="Brand",
|
|
category="serum",
|
|
recommended_time="both",
|
|
leave_on=True,
|
|
product_effect_profile={},
|
|
)
|
|
|
|
payload = {"product_ids": [str(product.id)]}
|
|
details = build_product_details_tool_handler(
|
|
[product],
|
|
last_used_on_by_product={str(product.id): date(2026, 3, 1)},
|
|
)(payload)
|
|
|
|
assert details["products"][0]["last_used_on"] == "2026-03-01"
|