Files
Vorrania/backend/tests/test_rezept_abgleich.py
ScarriffleandClaude Opus 5 fe6327c913 Rezept-Zuordnung: ein Eigenschaftswort frisst keinen Artikelnamen mehr
Gefunden bei der Probe, ob ein Embedding-Modell die Zuordnung besser
hinbekommt: Die Sperrliste der Eigenschaftswoerter lief als eine einzige
Liste mit Praefix-Vergleich. "Frischkaese" fing mit "frisch" an, galt
damit als blosse Eigenschaft und fiel aus dem Vergleich - die Zutat
"Kaese" fand den Frischkaese deshalb NIE. Ebenso "Biomilch" (bio),
"Rohrzucker" (roh), "Kleingebaeck" (klein), "Ganzkornbrot" (ganz),
"Grobgriess" (grob).

Unsichtbar, weil kein falscher Treffer entstand, sondern ein richtiger
ausblieb - die Zutat stand einfach als "nicht zugeordnet" da.

Jetzt zwei Toepfe statt einem. Stammformen wie "getrock" oder "vegan"
vergleichen weiter ueber den Wortanfang, weil Deutsch beugt und diese
Stuecke keine Lebensmittelnamen anfangen. Kurze Alltagswoerter wie
"frisch", "bio", "roh", "klein" zaehlen nur noch als Eigenschaft, wenn
hoechstens eine Beugungsendung dranhaengt: "frische" ja, "frischkase"
nein.

Gegenprobe im Test: "frische Hefe" findet die "Frischen Kraeuter"
weiterhin nicht - die Sperre ist praeziser geworden, nicht schwaecher.

7 neue Tests, Suite bei 494.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-02 14:01:56 +02:00

484 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Rezeptzutaten gegen den Bestand: Kann ich das kochen?
Ohne Mealie – die Zutaten stehen hier so im Test, wie Mealie sie liefern würde.
"""
import pytest
from app.models import (
BaseUnit,
Category,
CategoryMeasure,
CategoryTracking,
Group,
Measure,
Product,
)
from app.services.rezepte import faktor_fuer_portionen, kochbar, pruefe_zutaten
from app.services.stock import check_in
def _zutat(name, menge=None, einheit=None, text=None):
"""Eine Zutat im Format von Mealie."""
return {
"quantity": menge,
"unit": {"name": einheit, "abbreviation": einheit} if einheit else None,
"food": {"name": name},
"display": text or f"{menge or ''} {einheit or ''} {name}".strip(),
}
def _kategorie(db, name):
cat = Category(name=name, tracking=CategoryTracking.food.value)
db.add(cat)
db.commit()
db.refresh(cat)
return cat
def _artikel(db, name, cat, base=BaseUnit.gram, package_size=1000, bestand=0.0):
product = Product(name=name, base_unit=base, package_size=package_size,
category_id=cat.id, source="manual")
db.add(product)
db.commit()
db.refresh(product)
if bestand:
check_in(db, product, bestand, base.value, None, None, None)
db.commit()
return product
# --------------------------------------------------------------------------
# Portionen
# --------------------------------------------------------------------------
def test_portionen_skalieren_die_mengen(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=5000)
faktor, hinweis = faktor_fuer_portionen({"recipeServings": 4}, 6)
assert faktor == 1.5
assert hinweis == ""
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 200, "g")], faktor=faktor)
assert zutat.menge == 300
assert zutat.benoetigt_base == 300
def test_ohne_portionsangabe_wird_nicht_skaliert(db):
faktor, hinweis = faktor_fuer_portionen({"recipeServings": 0}, 6)
assert faktor == 1.0
assert "keine Portionszahl" in hinweis
def test_ohne_wunsch_bleibt_es_wie_im_rezept(db):
assert faktor_fuer_portionen({"recipeServings": 4}, None) == (1.0, "")
# --------------------------------------------------------------------------
# Reicht der Bestand?
# --------------------------------------------------------------------------
def test_genug_vorhanden(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 200, "g")])
assert zutat.status == "vorhanden"
assert zutat.fehlt_base == 0
assert kochbar([zutat]) is True
def test_zu_wenig_meldet_die_fehlmenge(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=150)
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 500, "g")])
assert zutat.status == "zu_wenig"
assert zutat.fehlt_base == 350
assert kochbar([zutat]) is False
def test_gar_nichts_da(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=0)
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 500, "g")])
assert zutat.status == "fehlt"
assert zutat.fehlt_base == 500
def test_unbekannte_zutat_wird_nicht_still_als_vorhanden_gewertet(db):
_kategorie(db, "Trockenwaren")
[zutat] = pruefe_zutaten(db, [_zutat("Safranfäden", 1, "g")])
assert zutat.status == "kein_treffer"
assert kochbar([zutat]) is False
def test_zutat_ohne_menge_ist_unklar(db):
cat = _kategorie(db, "Gewürze")
_artikel(db, "Salz", cat, bestand=500)
[zutat] = pruefe_zutaten(db, [_zutat("Salz", None, None, text="Salz nach Geschmack")])
assert zutat.status == "menge_unbekannt"
assert kochbar([zutat]) is False
def test_ohne_menge_und_ohne_bestand_fehlt_es(db):
cat = _kategorie(db, "Gewürze")
_artikel(db, "Salz", cat, bestand=0)
[zutat] = pruefe_zutaten(db, [_zutat("Salz", None, None)])
assert zutat.status == "fehlt"
# --------------------------------------------------------------------------
# Zuordnung
# --------------------------------------------------------------------------
def test_aehnlicher_name_wird_gefunden(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl Typ 550", cat, bestand=2000)
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 200, "g")])
assert zutat.art == "product"
assert zutat.ziel_name == "Weissmehl Typ 550"
def test_gruppe_zaehlt_marken_zusammen(db):
"""„Milch" ist markenuebergreifend gemeint – dafuer gibt es Gruppen."""
gruppe = Group(name="Milch")
db.add(gruppe)
db.commit()
cat = _kategorie(db, "Molkerei")
a = _artikel(db, "Valflora Vollmilch", cat, base=BaseUnit.milliliter, bestand=1000)
b = _artikel(db, "Emmi Vollmilch", cat, base=BaseUnit.milliliter, bestand=500)
a.group_id = gruppe.id
b.group_id = gruppe.id
db.commit()
[zutat] = pruefe_zutaten(db, [_zutat("Milch", 1200, "ml")])
assert zutat.art == "group"
assert zutat.bestand_base == 1500 # beide Marken zusammen
assert zutat.status == "vorhanden"
def test_gemerkte_zuordnung_schlaegt_die_suche(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=1000)
richtig = _artikel(db, "Dinkelmehl", cat, bestand=2000)
[zutat] = pruefe_zutaten(
db, [_zutat("Mehl", 100, "g")],
zuordnung={"mehl": ("product", richtig.id)},
)
assert zutat.ziel_id == richtig.id
assert zutat.ziel_name == "Dinkelmehl"
def test_voellig_fremde_zutat_liefert_gar_keinen_vorschlag(db):
"""Lieber nichts als Unsinn.
Fruehere Fassung schlug hier „Weissmehl" vor, weil sich Buchstaben
ueberlappen. Ein Vorschlag, den niemand nachvollziehen kann, ist schlimmer
als keiner – er verleitet zum Durchklicken.
"""
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("Kurkuma", 1, "g")])
assert zutat.status == "kein_treffer"
assert zutat.ziel_id is None
assert zutat.kandidaten == []
# --------------------------------------------------------------------------
# Was NICHT passen darf – die Faelle aus dem echten Katalog
# --------------------------------------------------------------------------
def test_gegenstaende_kommen_nicht_in_frage(db):
"""Ein Joystick ist kein Zucker und eine Waffelplatte kein Apfel."""
essen = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", essen, bestand=1000)
kram = Category(name="Elektronik", tracking=CategoryTracking.object.value)
db.add(kram)
db.commit()
db.refresh(kram)
for name in ("Joystick für WII", "Snack Collection Platten 4 (Waffeln)",
"Shirt Casual Fit"):
db.add(Product(name=name, base_unit=BaseUnit.piece, category_id=kram.id,
source="manual"))
db.commit()
for zutat_name in ("Zucker", "Apfel", "Salz"):
[zutat] = pruefe_zutaten(db, [_zutat(zutat_name, 1, "g")])
assert zutat.status == "kein_treffer", zutat_name
assert all("Joystick" not in k.name for k in zutat.kandidaten)
def test_mehl_trifft_nicht_das_geschnetzelte(db):
"""Buchstaben-Aehnlichkeit allein reicht nicht – es braucht ein ganzes Wort."""
fleisch = _kategorie(db, "Fleisch & Fisch")
_artikel(db, "Mühlen Geschnetzeltes", fleisch, bestand=500)
[zutat] = pruefe_zutaten(db, [_zutat("Mehl Typ 405", 125, "g")])
assert zutat.status == "kein_treffer"
def test_eier_treffen_nicht_den_reis(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Reis", cat, bestand=2000)
[zutat] = pruefe_zutaten(db, [_zutat("Eier", 3, None)])
assert zutat.status == "kein_treffer"
def test_kategorie_wird_zuerst_durchsucht(db):
"""Steht die Zutat in einer passenden Kategorie, gilt nur die."""
fleisch = _kategorie(db, "Fleisch & Fisch")
_artikel(db, "Mühlen Geschnetzeltes", fleisch, bestand=500)
backen = _kategorie(db, "Mehl & Zucker")
richtig = _artikel(db, "Weissmehl Typ 405", backen, bestand=2000)
[zutat] = pruefe_zutaten(db, [_zutat("Mehl Typ 405", 125, "g")])
assert zutat.ziel_id == richtig.id
assert zutat.status == "vorhanden"
def test_vegan_allein_begruendet_keinen_treffer(db):
"""„vegan" ist eine Eigenschaft, kein Lebensmittel.
Sonst passt jedes vegane Produkt auf jede vegane Zutat: „vegane Butter"
landete auf „veganes Mühlen Knusper Bleu", „veganer Lime Curd" auf
„Vegane Mühlen-Schnitzel".
"""
fleisch = _kategorie(db, "Fleischersatz")
_artikel(db, "veganes Mühlen Knusper Bleu", fleisch, bestand=400)
_artikel(db, "Vegane Mühlen Nuggets Klassisch", fleisch, bestand=400)
_artikel(db, "Vegane Mühlen-Schnitzel auf Basis von Soja", fleisch, bestand=400)
zutaten = [
_zutat("vegane Butter, Block, ungesalzen", 45, "g"),
_zutat("veganer griechischer Joghurt oder Skyr", 80, "g"),
_zutat("veganer Lime Curd", 120, "g"),
]
for zutat in pruefe_zutaten(db, zutaten):
assert zutat.status == "kein_treffer", zutat.text
assert zutat.ziel_name == ""
def test_vegane_zutat_findet_das_passende_vegane_produkt(db):
"""Die Eigenschaft darf nicht sperren, nur nicht allein tragen."""
molk = _kategorie(db, "Molkereiprodukte")
butter = _artikel(db, "Vegane Butter", molk, bestand=500)
_artikel(db, "Vegane Mühlen-Schnitzel", molk, bestand=400)
[zutat] = pruefe_zutaten(db, [_zutat("vegane Butter, Block, ungesalzen", 45, "g")])
assert zutat.ziel_id == butter.id
assert zutat.status == "vorhanden"
def test_zutat_aus_lauter_eigenschaften_trifft_nichts(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("frisch gemahlen, optional", 1, "g")])
assert zutat.status == "kein_treffer"
def test_beiwoerter_stoeren_den_echten_treffer_nicht(db):
cat = _kategorie(db, "Obst & Gemüse")
aepfel = _artikel(db, "Äpfel", cat, bestand=3000)
[zutat] = pruefe_zutaten(
db, [_zutat("Apfel geschält und in 3 mm dünne Scheiben geschnitten", 900, "g")]
)
assert zutat.ziel_id == aepfel.id
def test_falsche_einheit_spricht_gegen_den_artikel(db):
"""„200 ml Schlagsahne" ist kein „Kräuter Sahne Hähnchen"."""
fleisch = _kategorie(db, "Fleisch & Fisch")
_artikel(db, "Kräuter Sahne Hähnchen", fleisch, base=BaseUnit.gram, bestand=800)
[zutat] = pruefe_zutaten(db, [_zutat("Schlagsahne", 200, "ml")])
assert zutat.status == "kein_treffer"
assert "nicht in 'ml' geführt" in zutat.hinweis
def test_passende_einheit_gewinnt_gegen_hoehere_punktzahl(db):
molk = _kategorie(db, "Molkereiprodukte")
fleisch = _kategorie(db, "Fleisch & Fisch")
_artikel(db, "Sahne Hähnchen", fleisch, base=BaseUnit.gram, bestand=800)
richtig = _artikel(db, "Schlagsahne Rahm", molk, base=BaseUnit.milliliter, bestand=500)
[zutat] = pruefe_zutaten(db, [_zutat("Schlagsahne", 200, "ml")])
assert zutat.ziel_id == richtig.id
def test_fehlendes_hausmass_spricht_nicht_gegen_den_artikel(db):
"""Nur eine BEKANNTE Einheit falscher Art ist ein Hinweis.
„1 Tasse Linsen" heisst nicht, dass die Linsen falsch sind – es fehlt bloss
die Maßangabe, und die laesst sich nachtragen.
"""
cat = _kategorie(db, "Hülsenfrüchte")
linsen = _artikel(db, "Berglinsen", cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("Berglinsen", 1, "Tasse")])
assert zutat.ziel_id == linsen.id
assert zutat.status == "menge_unbekannt"
def test_zweiteinheit_laesst_die_fremde_einheit_zu(db):
cat = _kategorie(db, "Backwaren")
broetchen = _artikel(db, "Brötchen", cat, base=BaseUnit.piece, package_size=6,
bestand=12)
broetchen.secondary_base = BaseUnit.gram.value
broetchen.secondary_count = 1
broetchen.secondary_amount = 50
db.commit()
[zutat] = pruefe_zutaten(db, [_zutat("Brötchen", 100, "g")])
assert zutat.ziel_id == broetchen.id
def test_umlaut_und_mehrzahl_finden_sich(db):
"""Rezept schreibt „Apfel", im Katalog stehen „Äpfel"."""
cat = _kategorie(db, "Obst & Gemüse")
aepfel = _artikel(db, "Äpfel", cat, base=BaseUnit.gram, bestand=2000)
[zutat] = pruefe_zutaten(db, [_zutat("Apfel", 900, "g")])
assert zutat.ziel_id == aepfel.id
assert zutat.status == "vorhanden"
def test_eigenschaftswort_verschluckt_den_artikelnamen_nicht(db):
'''Der Fund aus der Modellprobe: 'Frischkaese' fing mit 'frisch' an.
Damit galt der ganze Name als blosse Eigenschaft, fiel aus dem Vergleich -
und die Zutat 'Kaese' fand den Frischkaese NIE. Unsichtbar, weil kein
falscher Treffer entstand, sondern ein richtiger ausblieb.
'''
cat = _kategorie(db, "Molkerei")
kaese = _artikel(db, "Frischkäse", cat, bestand=400)
[zutat] = pruefe_zutaten(db, [_zutat("Käse", 100, "g")])
assert zutat.ziel_id == kaese.id
assert zutat.status == "vorhanden"
@pytest.mark.parametrize("artikel,zutat_name", [
("Biomilch", "Milch"),
("Rohrzucker", "Zucker"),
("Kleingebäck", "Gebäck"),
("Ganzkornbrot", "Brot"),
("Grobgrieß", "Grieß"),
])
def test_komposita_mit_eigenschaft_am_anfang_bleiben_auffindbar(db, artikel, zutat_name):
cat = _kategorie(db, "Vorrat")
ziel = _artikel(db, artikel, cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat(zutat_name, 100, "g")])
assert zutat.ziel_id == ziel.id
def test_das_eigenschaftswort_selbst_begruendet_weiter_keinen_treffer(db):
"""Die Gegenprobe – sonst waere die Sperre bloss ausgehebelt."""
cat = _kategorie(db, "Kuehlregal")
_artikel(db, "Frische Kräuter", cat, bestand=100)
[zutat] = pruefe_zutaten(db, [_zutat("frische Hefe", 20, "g")])
assert zutat.status == "kein_treffer"
def test_vollmilch_findet_die_h_vollmilch(db):
cat = _kategorie(db, "Molkerei")
milch = _artikel(db, "H Vollmilch", cat, base=BaseUnit.milliliter, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("Vollmilch Zimmertemperatur", 120, "ml")])
assert zutat.ziel_id == milch.id
assert zutat.status == "vorhanden"
# --------------------------------------------------------------------------
# Hausmaße
# --------------------------------------------------------------------------
def test_essloeffel_wird_umgerechnet(db):
cat = _kategorie(db, "Mehl")
mehl = _artikel(db, "Weissmehl", cat, bestand=1000)
el = db.query(Measure).filter(Measure.name == "Esslöffel").one()
db.add(CategoryMeasure(measure_id=el.id, category_id=cat.id, amount=10,
base=BaseUnit.gram.value))
db.commit()
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 2, "EL")])
assert zutat.benoetigt_base == 20
assert zutat.status == "vorhanden"
def test_fehlendes_mass_wird_zum_anlegen_gemeldet(db):
"""Daraus baut die Oberflaeche „Maß jetzt anlegen"."""
cat = _kategorie(db, "Hülsenfrüchte")
_artikel(db, "Berglinsen", cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("Berglinsen", 1, "Tasse")])
assert zutat.status == "menge_unbekannt"
assert zutat.fehlendes_mass == "Tasse"
assert zutat.kategorie_id == cat.id
assert "kein Maß" in zutat.hinweis
def test_unbekannte_einheit_wird_benannt(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=1000)
[zutat] = pruefe_zutaten(db, [_zutat("Weissmehl", 1, "furlong")])
assert zutat.status == "menge_unbekannt"
assert "furlong" in zutat.hinweis
# --------------------------------------------------------------------------
# Ganzes Rezept
# --------------------------------------------------------------------------
def test_ein_rezept_mit_allem(db):
cat = _kategorie(db, "Trockenwaren")
_artikel(db, "Weissmehl", cat, bestand=1000)
_artikel(db, "Zucker", cat, bestand=50)
zutaten = [
_zutat("Weissmehl", 200, "g"), # reicht
_zutat("Zucker", 100, "g"), # zu wenig
_zutat("Safran", 1, "g"), # kennt niemand
]
ergebnis = pruefe_zutaten(db, zutaten)
assert [z.status for z in ergebnis] == ["vorhanden", "zu_wenig", "kein_treffer"]
assert kochbar(ergebnis) is False