innercontext/backend/tests/test_routines_helpers.py
Piotr Oleszczyk cfd2485b7e feat(api): add INCI tool-calling with normalized tool traces
Enable on-demand INCI retrieval in /routines/suggest through Gemini function calling so detailed ingredient data is fetched only when needed. Persist and normalize tool_trace data in AI logs to make function-call behavior directly inspectable via /ai-logs endpoints.
2026-03-04 11:35:19 +01:00

338 lines
9.6 KiB
Python

import uuid
from datetime import date, timedelta
from sqlmodel import Session
from innercontext.api.routines import (
_build_day_context,
_build_grooming_context,
_build_inci_tool_handler,
_build_objectives_context,
_build_products_context,
_build_recent_history,
_build_skin_context,
_contains_minoxidil_text,
_ev,
_get_available_products,
_is_minoxidil_product,
)
from innercontext.models import (
GroomingSchedule,
Product,
ProductInventory,
Routine,
RoutineStep,
SkinConditionSnapshot,
)
def test_contains_minoxidil_text():
assert _contains_minoxidil_text(None) is False
assert _contains_minoxidil_text("") is False
assert _contains_minoxidil_text("some random text") is False
assert _contains_minoxidil_text("contains MINOXIDIL here") is True
assert _contains_minoxidil_text("minoksydyl 5%") is True
def test_is_minoxidil_product():
# Setup product
p = Product(id=uuid.uuid4(), name="Test", brand="Brand", is_medication=True)
assert _is_minoxidil_product(p) is False
p.name = "Minoxidil 5%"
assert _is_minoxidil_product(p) is True
p.name = "Test"
p.brand = "Brand with minoksydyl"
assert _is_minoxidil_product(p) is True
p.brand = "Brand"
p.line_name = "Minoxidil Line"
assert _is_minoxidil_product(p) is True
p.line_name = None
p.usage_notes = "Use minoxidil daily"
assert _is_minoxidil_product(p) is True
p.usage_notes = None
p.inci = ["water", "minoxidil"]
assert _is_minoxidil_product(p) is True
p.inci = None
p.actives = [{"name": "minoxidil", "strength": "5%"}]
assert _is_minoxidil_product(p) is True
# As Pydantic model representation isn't exactly a dict in db sometimes, we just test dict
p.actives = [{"name": "Retinol", "strength": "1%"}]
assert _is_minoxidil_product(p) is False
def test_ev():
class DummyEnum:
value = "dummy"
assert _ev(None) == ""
assert _ev(DummyEnum()) == "dummy"
assert _ev("string") == "string"
def test_build_skin_context(session: Session):
# Empty
assert _build_skin_context(session) == "SKIN CONDITION: no data\n"
# With data
snap = SkinConditionSnapshot(
id=uuid.uuid4(),
snapshot_date=date.today(),
overall_state="good",
hydration_level=4,
barrier_state="intact",
active_concerns=["acne", "dryness"],
priorities=["hydration"],
notes="Feeling good",
)
session.add(snap)
session.commit()
ctx = _build_skin_context(session)
assert "SKIN CONDITION (snapshot from" in ctx
assert "Overall state: good" in ctx
assert "Hydration: 4/5" in ctx
assert "Barrier: intact" in ctx
assert "Active concerns: acne, dryness" in ctx
assert "Priorities: hydration" in ctx
assert "Notes: Feeling good" in ctx
def test_build_grooming_context(session: Session):
assert _build_grooming_context(session) == "GROOMING SCHEDULE: none\n"
sch = GroomingSchedule(
id=uuid.uuid4(), day_of_week=0, action="shaving_oneblade", notes="Morning"
)
session.add(sch)
session.commit()
ctx = _build_grooming_context(session)
assert "GROOMING SCHEDULE:" in ctx
assert "poniedziałek: shaving_oneblade (Morning)" in ctx
# Test weekdays filter
ctx2 = _build_grooming_context(session, weekdays=[1]) # not monday
assert "(no entries for specified days)" in ctx2
def test_build_recent_history(session: Session):
assert _build_recent_history(session) == "RECENT ROUTINES: none\n"
r = Routine(id=uuid.uuid4(), routine_date=date.today(), part_of_day="am")
session.add(r)
p = Product(
id=uuid.uuid4(),
name="Cleanser",
category="cleanser",
brand="Test",
recommended_time="both",
leave_on=False,
product_effect_profile={},
)
session.add(p)
session.commit()
s1 = RoutineStep(id=uuid.uuid4(), routine_id=r.id, order_index=1, product_id=p.id)
s2 = RoutineStep(
id=uuid.uuid4(), routine_id=r.id, order_index=2, action_type="shaving_razor"
)
# Step with non-existent product
s3 = RoutineStep(
id=uuid.uuid4(), routine_id=r.id, order_index=3, product_id=uuid.uuid4()
)
session.add_all([s1, s2, s3])
session.commit()
ctx = _build_recent_history(session)
assert "RECENT ROUTINES:" in ctx
assert "AM:" in ctx
assert "cleanser [" in ctx
assert "action: shaving_razor" in ctx
assert "unknown [" in ctx
def test_build_products_context(session: Session):
p1 = Product(
id=uuid.uuid4(),
name="Regaine",
category="serum",
is_medication=True,
brand="J&J",
recommended_time="both",
leave_on=True,
product_effect_profile={},
)
p2 = Product(
id=uuid.uuid4(),
name="Sunscreen",
category="spf",
brand="Test",
leave_on=True,
recommended_time="am",
pao_months=6,
product_effect_profile={"hydration_immediate": 2, "exfoliation_strength": 0},
incompatible_with=[{"target": "retinol", "scope": "same_routine"}],
context_rules={"safe_after_shaving": False},
min_interval_hours=12,
max_frequency_per_week=7,
)
session.add_all([p1, p2])
session.commit()
# Inventory
inv1 = ProductInventory(
id=uuid.uuid4(),
product_id=p2.id,
is_opened=True,
opened_at=date.today() - timedelta(days=10),
expiry_date=date.today() + timedelta(days=365),
)
inv2 = ProductInventory(id=uuid.uuid4(), product_id=p2.id, is_opened=False)
session.add_all([inv1, inv2])
session.commit()
# Usage
r = Routine(id=uuid.uuid4(), routine_date=date.today(), part_of_day="am")
session.add(r)
session.commit()
s = RoutineStep(id=uuid.uuid4(), routine_id=r.id, order_index=1, product_id=p2.id)
session.add(s)
session.commit()
ctx = _build_products_context(
session, time_filter="am", reference_date=date.today()
)
# p1 is medication but not minoxidil (wait, Regaine name doesn't contain minoxidil!) -> skipped
assert "Regaine" not in ctx
# Let's fix p1 to be minoxidil
p1.name = "Regaine Minoxidil"
session.add(p1)
session.commit()
ctx = _build_products_context(
session, time_filter="am", reference_date=date.today()
)
assert "Regaine Minoxidil" in ctx
assert "Sunscreen" in ctx
assert "inventory_status={active:2,opened:1,sealed:1}" in ctx
assert "nearest_open_expiry=" in ctx
assert "nearest_open_pao_deadline=" in ctx
assert "pao_months=6" in ctx
assert "effects={'hydration_immediate': 2}" in ctx
assert "incompatible_with=['avoid retinol (same_routine)']" in ctx
assert "context_rules={'safe_after_shaving': False}" in ctx
assert "min_interval_hours=12" in ctx
assert "max_frequency_per_week=7" in ctx
assert "used_in_last_7_days=1" in ctx
def test_build_objectives_context():
assert _build_objectives_context(False) == ""
assert "improve beard" in _build_objectives_context(True)
def test_build_day_context():
assert _build_day_context(None) == ""
assert "Leaving home: yes" in _build_day_context(True)
assert "Leaving home: no" in _build_day_context(False)
def test_get_available_products_respects_filters(session: Session):
regular_med = Product(
id=uuid.uuid4(),
name="Tretinoin",
category="serum",
is_medication=True,
brand="Test",
recommended_time="pm",
leave_on=True,
product_effect_profile={},
)
minoxidil_med = Product(
id=uuid.uuid4(),
name="Minoxidil 5%",
category="serum",
is_medication=True,
brand="Test",
recommended_time="both",
leave_on=True,
product_effect_profile={},
)
am_product = Product(
id=uuid.uuid4(),
name="AM SPF",
category="spf",
brand="Test",
recommended_time="am",
leave_on=True,
product_effect_profile={},
)
pm_product = Product(
id=uuid.uuid4(),
name="PM Cream",
category="moisturizer",
brand="Test",
recommended_time="pm",
leave_on=True,
product_effect_profile={},
)
session.add_all([regular_med, minoxidil_med, am_product, pm_product])
session.commit()
am_available = _get_available_products(session, time_filter="am")
am_names = {p.name for p in am_available}
assert "Tretinoin" not in am_names
assert "Minoxidil 5%" in am_names
assert "AM SPF" in am_names
assert "PM Cream" not in am_names
def test_build_inci_tool_handler_returns_only_available_ids(session: Session):
available = Product(
id=uuid.uuid4(),
name="Available",
category="serum",
brand="Test",
recommended_time="both",
leave_on=True,
inci=["Water", "Niacinamide"],
product_effect_profile={},
)
unavailable = Product(
id=uuid.uuid4(),
name="Unavailable",
category="serum",
brand="Test",
recommended_time="both",
leave_on=True,
inci=["Water", "Retinol"],
product_effect_profile={},
)
handler = _build_inci_tool_handler([available])
payload = handler(
{
"product_ids": [
str(available.id),
str(unavailable.id),
str(available.id),
123,
]
}
)
assert "products" in payload
products = payload["products"]
assert len(products) == 1
assert products[0]["id"] == str(available.id)
assert products[0]["name"] == "Available"
assert products[0]["inci"] == ["Water", "Niacinamide"]