Both functions are DB-free public API additions to assembly_recipes.py. get_role_candidates() scores pantry candidates against a wizard step using element-profile overlap with prior picks; build_from_selection() builds a RecipeSuggestion from explicit role overrides with required-role validation.
216 lines
6.8 KiB
Python
216 lines
6.8 KiB
Python
"""Tests for Build Your Own recipe assembly schemas."""
|
|
import pytest
|
|
from app.models.schemas.recipe import (
|
|
AssemblyRoleOut,
|
|
AssemblyTemplateOut,
|
|
RoleCandidateItem,
|
|
RoleCandidatesResponse,
|
|
BuildRequest,
|
|
)
|
|
|
|
|
|
def test_assembly_role_out_schema():
|
|
"""Test AssemblyRoleOut schema creation and field access."""
|
|
role = AssemblyRoleOut(
|
|
display="protein",
|
|
required=True,
|
|
keywords=["chicken"],
|
|
hint="Main ingredient"
|
|
)
|
|
assert role.display == "protein"
|
|
assert role.required is True
|
|
assert role.keywords == ["chicken"]
|
|
assert role.hint == "Main ingredient"
|
|
|
|
|
|
def test_assembly_template_out_schema():
|
|
"""Test AssemblyTemplateOut schema with nested roles."""
|
|
tmpl = AssemblyTemplateOut(
|
|
id="burrito_taco",
|
|
title="Burrito / Taco",
|
|
icon="🌯",
|
|
descriptor="Protein, veg, and sauce in a tortilla or over rice",
|
|
role_sequence=[
|
|
AssemblyRoleOut(
|
|
display="base",
|
|
required=True,
|
|
keywords=["tortilla"],
|
|
hint="The wrap"
|
|
),
|
|
],
|
|
)
|
|
assert tmpl.id == "burrito_taco"
|
|
assert tmpl.title == "Burrito / Taco"
|
|
assert tmpl.icon == "🌯"
|
|
assert len(tmpl.role_sequence) == 1
|
|
assert tmpl.role_sequence[0].display == "base"
|
|
|
|
|
|
def test_role_candidate_item_schema():
|
|
"""Test RoleCandidateItem schema with tags."""
|
|
item = RoleCandidateItem(
|
|
name="bell pepper",
|
|
in_pantry=True,
|
|
tags=["sweet", "vegetable"]
|
|
)
|
|
assert item.name == "bell pepper"
|
|
assert item.in_pantry is True
|
|
assert "sweet" in item.tags
|
|
|
|
|
|
def test_role_candidates_response_schema():
|
|
"""Test RoleCandidatesResponse with compatible and other candidates."""
|
|
resp = RoleCandidatesResponse(
|
|
compatible=[
|
|
RoleCandidateItem(name="bell pepper", in_pantry=True, tags=["sweet"])
|
|
],
|
|
other=[
|
|
RoleCandidateItem(
|
|
name="corn",
|
|
in_pantry=False,
|
|
tags=["sweet", "starchy"]
|
|
)
|
|
],
|
|
available_tags=["sweet", "starchy"],
|
|
)
|
|
assert len(resp.compatible) == 1
|
|
assert resp.compatible[0].name == "bell pepper"
|
|
assert len(resp.other) == 1
|
|
assert "sweet" in resp.available_tags
|
|
assert "starchy" in resp.available_tags
|
|
|
|
|
|
def test_build_request_schema():
|
|
"""Test BuildRequest schema with template and role overrides."""
|
|
req = BuildRequest(
|
|
template_id="burrito_taco",
|
|
role_overrides={"protein": "chicken", "sauce": "verde"}
|
|
)
|
|
assert req.template_id == "burrito_taco"
|
|
assert req.role_overrides["protein"] == "chicken"
|
|
assert req.role_overrides["sauce"] == "verde"
|
|
|
|
|
|
def test_role_candidates_response_defaults():
|
|
"""Test RoleCandidatesResponse with default factory fields."""
|
|
resp = RoleCandidatesResponse()
|
|
assert resp.compatible == []
|
|
assert resp.other == []
|
|
assert resp.available_tags == []
|
|
|
|
|
|
def test_build_request_defaults():
|
|
"""Test BuildRequest with default role_overrides."""
|
|
req = BuildRequest(template_id="test_template")
|
|
assert req.template_id == "test_template"
|
|
assert req.role_overrides == {}
|
|
|
|
|
|
def test_get_templates_for_api_returns_13():
|
|
from app.services.recipe.assembly_recipes import get_templates_for_api
|
|
templates = get_templates_for_api()
|
|
assert len(templates) == 13
|
|
|
|
|
|
def test_get_templates_for_api_shape():
|
|
from app.services.recipe.assembly_recipes import get_templates_for_api
|
|
templates = get_templates_for_api()
|
|
t = next(t for t in templates if t["id"] == "burrito_taco")
|
|
assert t["title"] == "Burrito / Taco"
|
|
assert t["icon"] == "🌯"
|
|
assert isinstance(t["role_sequence"], list)
|
|
assert len(t["role_sequence"]) >= 1
|
|
role = t["role_sequence"][0]
|
|
assert "display" in role
|
|
assert "required" in role
|
|
assert "keywords" in role
|
|
assert "hint" in role
|
|
|
|
|
|
def test_get_templates_for_api_all_have_slugs():
|
|
from app.services.recipe.assembly_recipes import get_templates_for_api
|
|
templates = get_templates_for_api()
|
|
slugs = {t["id"] for t in templates}
|
|
assert len(slugs) == 13
|
|
assert all(isinstance(s, str) and len(s) > 3 for s in slugs)
|
|
|
|
|
|
def test_get_role_candidates_splits_compatible_other():
|
|
from app.services.recipe.assembly_recipes import get_role_candidates
|
|
profile_index = {
|
|
"rice": ["Starch", "Structure"],
|
|
"chicken": ["Protein"],
|
|
"broccoli": ["Vegetable"],
|
|
}
|
|
result = get_role_candidates(
|
|
template_slug="stir_fry",
|
|
role_display="protein",
|
|
pantry_set={"rice", "chicken", "broccoli"},
|
|
prior_picks=["rice"],
|
|
profile_index=profile_index,
|
|
)
|
|
assert isinstance(result["compatible"], list)
|
|
assert isinstance(result["other"], list)
|
|
assert isinstance(result["available_tags"], list)
|
|
all_names = [c["name"] for c in result["compatible"] + result["other"]]
|
|
assert "chicken" in all_names
|
|
|
|
|
|
def test_get_role_candidates_available_tags():
|
|
from app.services.recipe.assembly_recipes import get_role_candidates
|
|
profile_index = {
|
|
"chicken": ["Protein", "Umami"],
|
|
"tofu": ["Protein"],
|
|
}
|
|
result = get_role_candidates(
|
|
template_slug="stir_fry",
|
|
role_display="protein",
|
|
pantry_set={"chicken", "tofu"},
|
|
prior_picks=[],
|
|
profile_index=profile_index,
|
|
)
|
|
assert "Protein" in result["available_tags"]
|
|
|
|
|
|
def test_get_role_candidates_unknown_template_returns_empty():
|
|
from app.services.recipe.assembly_recipes import get_role_candidates
|
|
result = get_role_candidates(
|
|
template_slug="nonexistent_template",
|
|
role_display="protein",
|
|
pantry_set={"chicken"},
|
|
prior_picks=[],
|
|
profile_index={},
|
|
)
|
|
assert result == {"compatible": [], "other": [], "available_tags": []}
|
|
|
|
|
|
def test_build_from_selection_returns_recipe():
|
|
from app.services.recipe.assembly_recipes import build_from_selection
|
|
result = build_from_selection(
|
|
template_slug="burrito_taco",
|
|
role_overrides={"tortilla or wrap": "flour tortilla", "protein": "chicken"},
|
|
pantry_set={"flour tortilla", "chicken", "salsa"},
|
|
)
|
|
assert result is not None
|
|
assert len(result.directions) > 0
|
|
assert result.id == -1
|
|
|
|
|
|
def test_build_from_selection_missing_required_role_returns_none():
|
|
from app.services.recipe.assembly_recipes import build_from_selection
|
|
result = build_from_selection(
|
|
template_slug="burrito_taco",
|
|
role_overrides={"protein": "chicken"},
|
|
pantry_set={"chicken"},
|
|
)
|
|
assert result is None
|
|
|
|
|
|
def test_build_from_selection_unknown_template_returns_none():
|
|
from app.services.recipe.assembly_recipes import build_from_selection
|
|
result = build_from_selection(
|
|
template_slug="does_not_exist",
|
|
role_overrides={},
|
|
pantry_set={"chicken"},
|
|
)
|
|
assert result is None
|