text_to_solver_program/tests/test_review.py
dschlueter e80a104cf9 feat: Prüf-Prompt prüft Parametereinheit gegen ausdrückliche Textdefinition
Neuer Schritt 1b im Pruef-Prompt: jedes Parameter-Feld unit wird gegen
eine ausdrückliche Definition im Aufgabentext abgeglichen (Betrag→€,
Menge→Stück, Zeit→h). Widerspricht die unit der Definition (z. B.
unit 'Stück' bei einem als 'Betrag' definierten Parameter l_i/u_i),
wird ein missing_assumption-Eintrag gefordert – löst eine
Korrekturrunde aus, die die Parameter-Einheit anpasst.

Anlass: Portfoliomanagement §3 definiert l_i/u_i als Schranken für den
investierten Betrag; das Modell trug unit='Stück' ein, von keiner Prüfung
gefangen (Befund 1 aus STAND.md). Die vorherige Regel (Commit c99e6f6)
zielte auf Annahmen, nicht auf das Parameter-Feld unit.

Ausschlussliste angepasst: 'Einheit je Parameter' nur noch ausgeschlossen,
wenn der Text NICHT ausdrücklich definiert. 436 Tests grün.
2026-09-18 21:27:55 +02:00

109 lines
5.1 KiB
Python
Raw 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.

from core.model_schema import AssumptionReview, CheckIssue, CheckSeverity
from checks.review import select_best
def make_review(matches=True, verdict="justified", missing=(), units_consistent=True):
return AssumptionReview.model_validate({
"objective_check": {
"task_evidence": "Bestimme die Aufteilung mit der geringsten Volatilität",
"asked_quantity": "Standardabweichung der Portfoliorendite",
"modeled_quantity": "Standardabweichung" if matches else "Varianz",
"matches": matches,
"units_consistent": units_consistent,
"unit_explanation": "Rendite €, Risiko €, Kosten €" if units_consistent else "Rendite €, Risiko dimensionslos, Kosten €",
"explanation": "-",
},
"checks": [{"unclear_point": "-", "verdict": verdict, "task_evidence": "-", "explanation": "-"}],
"missing_assumptions": list(missing),
})
def test_clean_review_needs_no_revision():
assert not make_review().needs_revision
def test_objective_mismatch_triggers_revision():
assert make_review(matches=False).needs_revision
def test_faulty_assumption_triggers_revision():
assert make_review(verdict="stricter_than_task").needs_revision
def test_missing_assumption_triggers_revision():
missing = [{"unclear_point": "-", "assumption": "-", "alternative": "-", "impact": "-"}]
assert make_review(missing=missing).needs_revision
# --- Auswahl der besten Korrekturrunde (checks.review.select_best) ---
ERROR = CheckIssue(severity=CheckSeverity.ERROR, code="CPSAT_WITH_CONTINUOUS_VARS", message="-")
WARNING = CheckIssue(severity=CheckSeverity.WARNING, code="UNDECLARED_SYMBOL", message="-")
def test_later_hallucinated_round_is_discarded():
# beobachtet in portfolio_qp: Runde 1 sauber, Runde 2 beschreibt ein Fremdproblem mit Check-Error
candidates = [
(None, [], make_review(matches=False)),
(None, [WARNING], make_review()),
(None, [ERROR, WARNING], make_review(matches=False)),
]
assert select_best(candidates) == 1
def test_clean_later_round_wins_tie():
candidates = [(None, [], make_review()), (None, [WARNING], make_review())]
assert select_best(candidates) == 1
def test_fewer_errors_preferred_when_all_have_errors():
candidates = [(None, [ERROR], make_review(matches=False)), (None, [ERROR, ERROR], make_review(matches=False))]
assert select_best(candidates) == 0
def test_mixed_units_trigger_revision():
# beobachtet: Rendite/Kosten in Euro, Risiko als dimensionslose Summe w_i^2 sigma_i^2 (Faktor 100.000 zu klein)
assert make_review(units_consistent=False).needs_revision
# --- Grund jeder Korrekturrunde im Log (vorher nur „Probleme gefunden“) ---
def test_revision_reasons_name_every_trigger():
from checks.review import needs_work, revision_reasons
from core.model_schema import CheckIssue, CheckSeverity
fehlend = {"unclear_point": "Einkaufspreis nicht beziffert", "assumption": "-", "alternative": "-", "impact": "-"}
review = make_review(matches=False, verdict="weaker_than_task", missing=[fehlend])
issues = [CheckIssue(severity=CheckSeverity.ERROR, code="MIXED_OBJECTIVE_UNITS", message="Risiko dimensionslos"),
CheckIssue(severity=CheckSeverity.WARNING, code="MISSING_DATA", message="kein Wert für p")]
gruende = revision_reasons(review, issues)
assert needs_work(review, issues)
assert any(g.startswith("Zielgröße passt nicht") for g in gruende)
assert any("weaker_than_task" in g for g in gruende)
assert any("Einkaufspreis nicht beziffert" in g for g in gruende)
assert any("MIXED_OBJECTIVE_UNITS" in g for g in gruende)
assert not any("MISSING_DATA" in g for g in gruende) # löst keine Runde aus, gehört nicht in den Grund
def test_clean_state_has_no_revision_reasons():
from checks.review import revision_reasons
assert revision_reasons(make_review(), []) == []
def test_review_prompt_allows_both_forms_of_decision_independent_costs():
# wagner_whitin_dijkstra: Einkaufspreis ohne Zahl – je 1–2 Korrekturrunden, weil Regel 4 beide Formen beanstanden konnte
from pathlib import Path
prompt = (Path(__file__).parent.parent / "checks" / "Pruef-Prompt.txt").read_text(encoding="utf-8")
assert "entscheidungsunabhängige Terme" in prompt and "Verlange keine Umstellung" in prompt
def test_review_prompt_checks_parameter_units_against_explicit_definitions():
# portfoliomanagement §3: l_i, u_i als Schranken für den investierten Betrag definiert,
# Modell trug unit='Stück' ein – von keiner Prüfung gefangen. Die neue Regel verlangt
# Schritt 1b und schränkt die Ausschlussliste ein.
from pathlib import Path
prompt = (Path(__file__).parent.parent / "checks" / "Pruef-Prompt.txt").read_text(encoding="utf-8")
assert "Parametereinheiten gegen ausdrückliche Definitionen" in prompt
assert "iderspricht die unit" in prompt # Groß-/Kleinschreibung unabhängig
assert "GEGEN EINE NICHT AUSDRÜCKLICHE TEXTDEFINITION" in prompt
# die Ausschlussliste darf nicht mehr pauschal „Einheit je Parameter“ ausschließen
assert "Einheit je Parameter GEGEN EINE" in prompt