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