kiwi/tests/services/recipe/test_style_adapter.py
pyr0ball 9371df1c95 feat: recipe engine Phase 3 — StyleAdapter, LLM levels 3-4, user settings
Task 13: StyleAdapter with 5 cuisine templates (Italian, Latin, East Asian,
Eastern European, Mediterranean). Each template includes weighted method_bias
(sums to 1.0), element-filtered aromatics/depth/structure helpers, and
seasoning/finishing-fat vectors. StyleTemplate is a fully immutable frozen
dataclass with tuple fields.

Task 14: LLMRecipeGenerator for Levels 3 and 4. Level 3 builds a structured
element-scaffold prompt; Level 4 generates a minimal wildcard prompt (<1500
chars). Allergy hard-exclusion wired through RecipeRequest.allergies into
both prompt builders and the generate() call path. Parsed LLM response
(title, ingredients, directions, notes) fully propagated to RecipeSuggestion.

Task 15: User settings key-value store. Migration 012 adds user_settings
table. Store.get_setting / set_setting with upsert. GET/PUT /settings/{key}
endpoints with Pydantic SettingBody, key allowlist, get_session dependency.
RecipeEngine reads cooking_equipment from settings when hard_day_mode=True.

55 tests passing.
2026-03-31 14:15:18 -07:00

75 lines
2.4 KiB
Python

from app.services.recipe.style_adapter import StyleAdapter
# --- Spec-required tests ---
def test_italian_style_biases_aromatics():
"""Garlic and onion appear when they're in both pantry and italian aromatics."""
adapter = StyleAdapter()
italian = adapter.get("italian")
pantry = ["garlic", "onion", "ginger"]
result = italian.bias_aroma_selection(pantry)
assert "garlic" in result
assert "onion" in result
def test_east_asian_method_weights_sum_to_one():
"""East Asian method_bias weights sum to ~1.0."""
adapter = StyleAdapter()
east_asian = adapter.get("east_asian")
weights = east_asian.method_weights()
assert abs(sum(weights.values()) - 1.0) < 1e-6
def test_style_adapter_loads_all_five_styles():
"""Adapter discovers all 5 cuisine YAML files."""
adapter = StyleAdapter()
assert len(adapter.styles) == 5
# --- Additional coverage ---
def test_load_italian_style():
adapter = StyleAdapter()
italian = adapter.get("italian")
assert italian is not None
assert "basil" in italian.aromatics or "oregano" in italian.aromatics
def test_bias_aroma_selection_excludes_non_style_items():
"""bias_aroma_selection does not include items not in the style's aromatics."""
adapter = StyleAdapter()
italian = adapter.get("italian")
pantry = ["butter", "parmesan", "basil", "cumin", "soy sauce"]
result = italian.bias_aroma_selection(pantry)
assert "basil" in result
assert "soy sauce" not in result
assert "cumin" not in result
def test_preferred_depth_sources():
"""preferred_depth_sources returns only depth sources present in pantry."""
adapter = StyleAdapter()
italian = adapter.get("italian")
pantry = ["parmesan", "olive oil", "pasta"]
result = italian.preferred_depth_sources(pantry)
assert "parmesan" in result
assert "olive oil" not in result
def test_bias_aroma_selection_adapter_method():
"""StyleAdapter.bias_aroma_selection returns italian-biased items."""
adapter = StyleAdapter()
pantry = ["butter", "parmesan", "basil", "cumin", "soy sauce"]
biased = adapter.bias_aroma_selection("italian", pantry)
assert "basil" in biased
assert "soy sauce" not in biased or "basil" in biased
def test_list_all_styles():
adapter = StyleAdapter()
styles = adapter.list_all()
style_ids = [s.style_id for s in styles]
assert "italian" in style_ids
assert "latin" in style_ids
assert "east_asian" in style_ids