"""run_pipeline ohne LLM: Neustart bei Fremdproblem und Kennzeichnung unzuverlässiger Ergebnisse."""
from benchmark.benchmark import result_correct
from core.model_schema import (
Answer,
AssumptionReview,
LLMUsage,
OptimizationModelSpecification,
SolverCode,
SolverStatus,
SubQuestion,
TaskUnderstanding,
)
from TextToModel import run_pipeline
from test_model_checks import make_spec
from test_review import make_review
from test_script_export import STRUCTURED_CODE
# Standardverstaendnis: eine Frage, keine Zahlen - reicht fuer Tests, die das Verstehen nicht pruefen
EINFACHES_VERSTAENDNIS = TaskUnderstanding(
is_optimization_task=True, reason="-", facts=[], ignored_numbers=[],
sub_questions=[SubQuestion(id="1", quote="-", kind="rechnen", needs_solver=True)])
class FakeSession:
"""Liefert vorgegebene Antworten je Antwort-Typ in fester Reihenfolge."""
def __init__(self, specs, reviews, codes=(), understandings=None, readings=()):
from core.model_schema import FigureReading
self.queues = {
OptimizationModelSpecification: list(specs),
AssumptionReview: list(reviews),
SolverCode: list(codes),
TaskUnderstanding: list(understandings if understandings is not None else []),
FigureReading: list(readings),
}
self.contents = [] # (Stufe, Inhalt der letzten Nachricht) – prüft, wer Bilder zu sehen bekommt
self.usage = LLMUsage(model="fake", provider="fake")
self.calls = {kind: 0 for kind in self.queues}
self.steps = []
self.tasks = []
self.letzter_prompt = ""
def overrides(self):
return {}
def create(self, response_model, messages, step="unbenannt"):
self.letzter_prompt = messages[-1]["content"]
self.contents.append((step, messages[-1]["content"]))
if step == "Extraktion":
self.tasks.append(messages[-1]["content"])
self.calls[response_model] += 1
self.usage.calls += 1
self.steps.append(step)
warteschlange = self.queues[response_model]
if response_model is TaskUnderstanding and not warteschlange:
return EINFACHES_VERSTAENDNIS # Standard, solange ein Test nichts anderes vorgibt
antwort = warteschlange.pop(0)
if isinstance(antwort, Exception): # gescheiterter Aufruf, z. B. LLMUnavailable
raise antwort
return antwort
HALLUCINATED = make_spec(executive_summary="Produktionsplanung mit Drehen und Fräsen") # Fremdproblem
GOOD = make_spec(executive_summary="Rohstoffeinkauf mit minimalen Kosten")
MISMATCH = make_review(matches=False)
OK = make_review()
def test_clean_first_attempt_needs_no_restart():
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=1)
assert result.reliable and result.restarts == 0 and result.revisions == 0
assert result.solver_run.status == SolverStatus.OPTIMAL
def test_restart_recovers_from_hallucinated_problem():
# Versuch 1: Fremdproblem, beide Korrekturrunden erfolglos; Neustart liefert das richtige Modell
session = FakeSession(
specs=[HALLUCINATED, HALLUCINATED, HALLUCINATED, GOOD],
reviews=[MISMATCH, MISMATCH, MISMATCH, OK],
codes=[SolverCode(code=STRUCTURED_CODE)],
)
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=1)
assert result.restarts == 1
assert result.reliable and result.reliability_issues == []
assert result.specification.executive_summary == GOOD.executive_summary
assert result.solver_run.status == SolverStatus.OPTIMAL and result.solver_run.objective_value == 9
def test_persistent_mismatch_is_unreliable_and_not_solved():
# beobachtet im Benchmark: Fremdproblem wurde als "optimal, 2550" ausgegeben
session = FakeSession(specs=[HALLUCINATED] * 6, reviews=[MISMATCH] * 6)
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=1)
assert result.restarts == 1
assert not result.reliable
assert any("Zielfunktion passt nicht" in issue for issue in result.reliability_issues)
assert result.solver_run is None and result.solver_code is None
assert session.calls[SolverCode] == 0 # kein Solver-Code für ein falsches Problem
assert not result_correct(result.solver_run, {"objective_value": 9, "tolerance": 1e-6})
def test_better_earlier_attempt_wins_over_worse_restart():
# Neustart ist schlechter (Zielgröße falsch) als Versuch 1 (nur Check-Error) -> Versuch 1 verwenden
stack_error = make_spec(executive_summary="richtig, aber Stack falsch", problem_class="Quadratic Programming (QP)")
session = FakeSession(
specs=[stack_error, stack_error, stack_error, HALLUCINATED, HALLUCINATED, HALLUCINATED],
reviews=[OK, OK, OK, MISMATCH, MISMATCH, MISMATCH],
codes=[SolverCode(code=STRUCTURED_CODE)],
)
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=1)
assert result.specification.executive_summary == "richtig, aber Stack falsch"
assert not result.reliable # STACK_MISMATCH bleibt bestehen
assert any("STACK_MISMATCH" in issue for issue in result.reliability_issues)
assert result.solver_run is not None # Zielgröße passt -> Solver läuft, Ergebnis aber als unzuverlässig markiert
def test_persistent_contradicted_assumption_is_unreliable():
# beobachtet am Portfoliomanagement: Prüfung erkannte x als Stückzahl statt als Betrag
# (contradicts_task), aber die Korrektur reparierte es nicht. Der Solver lief als reliable=True.
# Ein verbleibendes contradicts_task nach allen Runden muss das Ergebnis unzuverlässig machen.
from test_review import make_review
contradicts = make_review(verdict="contradicts_task")
session = FakeSession(specs=[HALLUCINATED] * 3, reviews=[contradicts] * 3,
codes=[SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=0)
assert not result.reliable
assert any("Prüfung: Annahme contradicts_task" in i for i in result.reliability_issues)
def test_no_restart_when_disabled():
session = FakeSession(specs=[HALLUCINATED] * 3, reviews=[MISMATCH] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=0)
assert result.restarts == 0 and not result.reliable
def test_report_shows_prominent_warning_for_unreliable_result():
from reporting.report import build_document, to_html, to_markdown
session = FakeSession(specs=[HALLUCINATED] * 3, reviews=[MISMATCH] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=0)
doc = build_document(result)
assert [b[0] for b in doc[:4]] == ["h1", "list", "warn", "toc"] # Titel → Laufdaten → Warnkasten → Inhalt
assert '
⚠ Ergebnis unzuverlässig' in to_html(doc)
assert "> **⚠ Ergebnis unzuverlässig" in to_markdown(doc)
assert "Der Solver wurde nicht gestartet." in doc[2][1]
def test_report_shows_the_rejected_reading_of_each_assumption():
from reporting.report import build_document, to_markdown
spec = make_spec(open_assumptions=[{
"unclear_point": "„aufeinanderfolgend“",
"assumption": "Kalendertage, auch über das Wochenende",
"alternative": "Nur Werktage; verworfen, weil der Text durchgehenden Betrieb nennt",
"impact": "Restriktion Ruhezeit greift auch am Wochenende",
}])
session = FakeSession([spec], [OK], [SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, solve_model=False)
markdown = to_markdown(build_document(result))
assert "Verworfene Lesart" in markdown
assert "verworfen, weil der Text durchgehenden Betrieb nennt" in markdown
def test_pipeline_labels_every_llm_call_for_the_timing_breakdown():
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)])
run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=1)
assert session.steps == ["Verstehen", "Extraktion", "Prüfung", "Solver-Code"]
def test_revision_and_repair_steps_are_labelled():
session = FakeSession(specs=[HALLUCINATED, GOOD], reviews=[MISMATCH, OK],
codes=[SolverCode(code="x = 1"), SolverCode(code=STRUCTURED_CODE)])
run_pipeline("Aufgabe", session=session, max_revisions=1, max_restarts=0)
assert session.steps == ["Verstehen", "Extraktion", "Prüfung", "Korrektur", "Prüfung", "Solver-Code", "Solver-Reparatur"]
# --- Zulässigkeitsprüfung im Zusammenspiel ---
VIOLATING_CODE = '''
def solve_model():
"""Liefert eine Lösung, die das Budget um 8 Cent überschreitet (beobachteter Fall)."""
return 100000.08
def main():
"""Meldet die Lösung samt Restriktionen zur Nachprüfung."""
invested = solve_model()
report("optimal", 12041.74, {"x": invested},
constraints=[{"name": "Bedarf", "lhs": invested, "sense": "<=", "rhs": 100000.0}],
variables=[{"name": "x", "value": invested, "var_type": "continuous", "lb": 0}])
if __name__ == "__main__":
main()
'''
def test_infeasible_solution_triggers_repair_and_is_marked_unreliable():
session = FakeSession([GOOD], [OK], codes=[SolverCode(code=VIOLATING_CODE)] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0, solver_retries=2)
assert session.calls[SolverCode] == 3 # Erstversuch + 2 Reparaturen
assert result.solver_run.feasibility and "Bedarf" in result.solver_run.feasibility[0]
assert not result.reliable
assert any("Zulässigkeitsprüfung" in issue for issue in result.reliability_issues)
def test_feasible_solution_stays_reliable():
session = FakeSession([GOOD], [OK], codes=[SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.solver_run.feasibility == []
assert result.reliable and session.calls[SolverCode] == 1
# --- Gegenprobe bei "unlösbar" ---
INFEASIBLE_CODE = '''
def main():
"""Meldet das Modell als unlösbar."""
report("infeasible", None, {})
if __name__ == "__main__":
main()
'''
def test_infeasible_is_rechecked_and_can_be_refuted():
# beobachtet bei qwen3-235b: "unlösbar" war ein Modellierungsfehler, die Aufgabe ist lösbar
session = FakeSession(specs=[GOOD, GOOD], reviews=[OK, OK],
codes=[SolverCode(code=INFEASIBLE_CODE), SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.solver_run.status == SolverStatus.OPTIMAL and result.solver_run.objective_value == 9
assert not result.infeasible_confirmed and result.restarts == 1
assert "HINWEIS" in session.tasks[1] and "unabhängige Modellierung" in session.tasks[1]
def test_infeasible_confirmed_by_second_modelling():
session = FakeSession(specs=[GOOD, GOOD], reviews=[OK, OK],
codes=[SolverCode(code=INFEASIBLE_CODE)] * 2)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.solver_run.status == SolverStatus.INFEASIBLE
assert result.infeasible_confirmed and result.reliable
def test_infeasible_without_recheck_is_flagged(monkeypatch):
import TextToModel
monkeypatch.setattr(TextToModel, "INFEASIBLE_RECHECK", 0)
session = FakeSession([GOOD], [OK], codes=[SolverCode(code=INFEASIBLE_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert not result.reliable and any("ohne Gegenprobe" in i for i in result.reliability_issues)
# --- Wiedereinstieg ab einer Stufe (continue_pipeline / CLI --von) ---
def stored_result(tmp_path, spec=GOOD, review=OK):
"""Ergebnis eines früheren Laufs als Datei – Grundlage für --von."""
session = FakeSession([spec], [review], [SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Rohstoffeinkauf", session=session, max_revisions=0, max_restarts=0, solve_model=False)
path = tmp_path / "ergebnis.json"
path.write_text(result.model_dump_json(indent=2), encoding="utf-8")
return path
def test_continue_from_solver_skips_extraction_and_review(tmp_path):
from TextToModel import continue_pipeline
path = stored_result(tmp_path)
session = FakeSession([], [], [SolverCode(code=STRUCTURED_CODE)])
result = continue_pipeline(str(path), "solver", session=session)
assert session.steps == ["Solver-Code"] # kein Extraktions-, kein Prüfaufruf
assert result.specification.executive_summary == GOOD.executive_summary
assert result.task == "Rohstoffeinkauf" # Aufgabentext kommt aus dem Ergebnis
assert result.solver_run.status == SolverStatus.OPTIMAL
def test_continue_from_review_keeps_specification_but_checks_again(tmp_path):
from TextToModel import continue_pipeline
path = stored_result(tmp_path)
session = FakeSession([], [OK], [SolverCode(code=STRUCTURED_CODE)])
result = continue_pipeline(str(path), "pruefung", session=session)
assert session.steps == ["Prüfung", "Solver-Code"] # Extraktion entfällt
assert result.specification.executive_summary == GOOD.executive_summary
def test_continue_from_review_can_still_revise(tmp_path):
from TextToModel import continue_pipeline
path = stored_result(tmp_path)
session = FakeSession([GOOD], [MISMATCH, OK], [SolverCode(code=STRUCTURED_CODE)])
result = continue_pipeline(str(path), "pruefung", session=session, max_revisions=1)
assert session.steps == ["Prüfung", "Korrektur", "Prüfung", "Solver-Code"]
assert result.revisions == 1
def test_continue_from_solver_refuses_when_objective_does_not_match(tmp_path):
# gespeicherte Zielgrößen-Abweichung bleibt bindend: kein Solver, kein LLM-Aufruf
from TextToModel import continue_pipeline
path = stored_result(tmp_path, review=MISMATCH)
session = FakeSession([], [], [])
result = continue_pipeline(str(path), "solver", session=session)
assert session.steps == []
assert result.solver_run is None and not result.reliable
def test_unknown_stage_is_rejected(tmp_path):
import pytest
from TextToModel import continue_pipeline
path = stored_result(tmp_path)
with pytest.raises(ValueError, match="Unbekannte Stufe"):
continue_pipeline(str(path), "extraktion", session=FakeSession([], [], []))
def test_warnings_reach_the_revision_prompt():
# Warnungen loesen keine Runde aus, muessen aber mitkorrigiert werden, wenn ohnehin eine stattfindet
from core.model_schema import CheckIssue, CheckSeverity
from modeling.extraction import revise_model
session = FakeSession([GOOD], [], [])
revise_model(session, "Aufgabe", GOOD, OK, issues=[
CheckIssue(severity=CheckSeverity.WARNING, code="UNEXPLAINED_JARGON", message="Fachbegriffe: Fluss"),
CheckIssue(severity=CheckSeverity.ERROR, code="STACK_MISMATCH", message="passt nicht"),
])
prompt = session.letzter_prompt
assert "STACK_MISMATCH" in prompt.split("HINWEISE")[0] # Fehler im Fehlerteil
assert "UNEXPLAINED_JARGON" in prompt.split("HINWEISE")[1] # Warnung im Hinweisteil
# --- Benchmark haelt fest, womit gemessen wurde ---
def test_benchmark_records_the_prompt_state():
from benchmark.benchmark import PROMPT_FILES, prompt_state
stand = prompt_state()
assert {"prompts", "commit", "dirty"} <= set(stand)
assert len(stand["prompts"]) == 12 and stand["commit"]
assert "modeling/Routing-und-Extraktions-Prompt.txt" in PROMPT_FILES
assert "core/model_schema.py" in PROMPT_FILES # Feldbeschreibungen wirken wie Prompt-Anweisungen
assert "understanding/Verstehen-Prompt.txt" in PROMPT_FILES
def test_deviating_settings_are_recorded(monkeypatch):
# Ohne die Umgebung liesse sich ein A/B-Lauf (Stufe abgeschaltet, anderes Modell) nicht zuordnen
from benchmark import benchmark
benchmark.prompt_state.cache_clear()
monkeypatch.setenv("UNDERSTAND_RETRIES", "-1")
monkeypatch.setenv("LLM_MODEL_SOLVER", "billig/modell")
stand = benchmark.prompt_state()
assert stand["config"] == {"UNDERSTAND_RETRIES": "-1", "LLM_MODEL_SOLVER": "billig/modell"}
benchmark.prompt_state.cache_clear()
def test_prompt_digest_changes_with_the_prompt(tmp_path):
from benchmark.benchmark import prompt_digest
a, b = tmp_path / "eins.txt", tmp_path / "zwei.txt"
a.write_text("Regel A"), b.write_text("Regel B")
vorher = prompt_digest([a, b])
assert prompt_digest([a, b]) == vorher # gleicher Inhalt, gleiche Kennung
b.write_text("Regel B, nun schaerfer")
assert prompt_digest([a, b]) != vorher # eine geaenderte Regel aendert die Kennung
assert prompt_digest([b, a]) != prompt_digest([a, b]) # Reihenfolge ist Teil der Kennung
def test_unexplained_jargon_triggers_a_revision_but_stays_reliable():
# Erklaeren ist Kernziel: die Warnung ist eine Runde wert, macht das Ergebnis aber nicht unzuverlaessig
from checks.model_checks import check_spec
unerklaert = make_spec(
executive_summary="Der Betrieb entscheidet ueber Mengen.",
constraints=[{"identifier": "Flusserhaltung", "latex_formula": "\\sum_j x_j = 1", "description": "-"}])
assert "UNEXPLAINED_JARGON" in {i.code for i in check_spec(unerklaert)}
session = FakeSession(specs=[unerklaert, GOOD], reviews=[OK, OK], codes=[SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=1, max_restarts=0)
assert session.steps == ["Verstehen", "Extraktion", "Prüfung", "Korrektur", "Prüfung", "Solver-Code"]
assert result.revisions == 1
assert result.reliable # Warnung, kein Fehler
def test_clean_model_still_needs_no_revision():
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=2, max_restarts=0)
assert result.revisions == 0
# --- Verstehensstufe im Ablauf ---
def test_understanding_runs_first_and_is_kept_in_the_result():
from core.model_schema import Fact
verstanden = TaskUnderstanding(
is_optimization_task=True, reason="Kostenminimierung", ignored_numbers=[],
sub_questions=[SubQuestion(id="a", quote="Wie viel?", kind="rechnen", needs_solver=True)],
facts=[Fact(value="12", unit="€", meaning="Deckungsbeitrag je Stueck", quote="12 € je Stueck")])
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)], understandings=[verstanden])
result = run_pipeline("Der Deckungsbeitrag ist 12 € je Stueck.", session=session,
max_revisions=0, max_restarts=0)
assert session.steps[0] == "Verstehen"
assert result.understanding.facts[0].value == "12"
assert [q.id for q in result.understanding.sub_questions] == ["a"]
def test_incomplete_understanding_is_asked_again():
# erste Antwort uebersieht die Zahl, zweite erfasst sie
from core.model_schema import Fact
luecke = TaskUnderstanding(is_optimization_task=True, reason="-", facts=[], ignored_numbers=[],
sub_questions=[SubQuestion(id="1", quote="-", kind="rechnen", needs_solver=True)])
vollstaendig = luecke.model_copy(update={
"facts": [Fact(value="12", unit="€", meaning="Deckungsbeitrag", quote="12 € je Stueck")]})
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)],
understandings=[luecke, vollstaendig])
result = run_pipeline("12 € je Stueck.", session=session, max_revisions=0, max_restarts=0)
assert session.steps[:2] == ["Verstehen", "Verstehen"] # genau ein Nachfassen
assert result.understanding.facts[0].value == "12"
def test_report_shows_questions_and_numbers():
from core.model_schema import Fact
from reporting.report import build_document, to_markdown
verstanden = TaskUnderstanding(
is_optimization_task=True, reason="-",
sub_questions=[SubQuestion(id="a", quote="Formulieren Sie das Problem!", kind="formulieren", needs_solver=False),
SubQuestion(id="b", quote="Loesen Sie es!", kind="rechnen", needs_solver=True)],
facts=[Fact(value="8", unit="€", meaning="Kosten im ersten Jahr", quote="Zeile 0: 8 | 18 | 31")],
ignored_numbers=[])
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)], understandings=[verstanden])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
markdown = to_markdown(build_document(result))
assert "Gestellte Fragen" in markdown and "Formulieren Sie das Problem!" in markdown
assert "Gegebene Zahlen" in markdown and "Kosten im ersten Jahr" in markdown
def test_report_puts_the_answers_before_the_numbers():
from reporting.report import build_document, to_markdown
code = '''
def main():
"""Loest und beantwortet."""
report("optimal", 9.0, {"x": 1.0},
headline="Die Kosten betragen 9 Geldeinheiten.",
tables=[{"title": "Zielwert-Zerlegung", "columns": ["Bestandteil", "Wert"],
"objective_column": "Wert", "rows": [["Kosten", 9.0]]}],
constraints=[{"name": "Bedarf", "lhs": 4.0, "sense": ">=", "rhs": 4.0}],
variables=[{"name": "x", "value": 1.0, "var_type": "continuous", "lb": 0}],
answers=[{"question_id": "a", "text": "Das Modell ist ein lineares Programm."},
{"question_id": "b", "text": "Die minimalen Kosten betragen 9 Geldeinheiten."}])
if __name__ == "__main__":
main()
'''
verstanden = TaskUnderstanding(
is_optimization_task=True, reason="-", facts=[], ignored_numbers=[],
sub_questions=[SubQuestion(id="a", quote="Formulieren Sie das Modell!", kind="formulieren", needs_solver=False),
SubQuestion(id="b", quote="Berechnen Sie die Kosten!", kind="rechnen", needs_solver=True)])
session = FakeSession([GOOD], [OK], [SolverCode(code=code)], understandings=[verstanden])
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0)
markdown = to_markdown(build_document(result))
assert "Antworten auf die Aufgabe" in markdown
assert markdown.index("Formulieren Sie das Modell!") < markdown.index("| Status |")
assert result.solver_run.warnings == [] # Abnahme ohne Befund
def test_unanswered_sub_question_is_a_warning_not_a_wrong_number():
code = '''
def main():
"""Beantwortet nur die zweite Frage."""
report("optimal", 9.0, {"x": 1.0},
constraints=[{"name": "Bedarf", "lhs": 4.0, "sense": ">=", "rhs": 4.0}],
variables=[{"name": "x", "value": 1.0, "var_type": "continuous", "lb": 0}],
answers=[{"question_id": "b", "text": "Die minimalen Kosten betragen 9 Geldeinheiten."}])
if __name__ == "__main__":
main()
'''
verstanden = TaskUnderstanding(
is_optimization_task=True, reason="-", facts=[], ignored_numbers=[],
sub_questions=[SubQuestion(id="a", quote="Zeichnen Sie den Graphen!", kind="zeichnen", needs_solver=False),
SubQuestion(id="b", quote="Berechnen Sie die Kosten!", kind="rechnen", needs_solver=True)])
session = FakeSession([GOOD], [OK], [SolverCode(code=code)] * 3, understandings=[verstanden])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert any("Teilfragen ohne Antwort" in w for w in result.solver_run.warnings)
assert result.solver_run.objective_value == 9.0 # die Zahl bleibt richtig
def test_report_shows_data_profile_and_scenarios(tmp_path):
from reporting.report import build_document, to_markdown
datei = tmp_path / "werte.csv"
datei.write_text("name,menge\nA,3\nB,4\n")
code = '''
DATA = {"faktor": 1.0}
def main():
"""Meldet zwei Datenvarianten."""
report("optimal", 7.0, {"x": 1.0},
tables=[{"title": "Zielwert-Zerlegung", "columns": ["Bestandteil", "Wert"],
"objective_column": "Wert", "rows": [["Kosten", 7.0]]}],
constraints=[{"name": "Bedarf", "lhs": 4.0, "sense": ">=", "rhs": 4.0}],
variables=[{"name": "x", "value": 1.0, "var_type": "continuous", "lb": 0}],
scenarios=[{"name": "Doppelt", "description": "Faktor verdoppelt",
"data_override": {"faktor": 2.0}, "objective_value": 7.0}])
if __name__ == "__main__":
main()
'''
session = FakeSession([GOOD], [OK], [SolverCode(code=code)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0, data_files=[datei])
markdown = to_markdown(build_document(result))
assert "Beigelegte Daten" in markdown and "werte.csv (2 Zeilen)" in markdown
assert "Verglichene Konfigurationen" in markdown and "Doppelt" in markdown
assert result.data_files[0].rows == 2
assert result.solver_run.scenarios[0].verified_value == 7.0 # ohne LLM nachgerechnet
# --- Benchmark misst "Aufgabe erfuellt", nicht nur "Zahl richtig" ---
def test_benchmark_counts_unanswered_questions_as_not_fulfilled(monkeypatch):
from benchmark import benchmark
from core.model_schema import Answer, SubQuestion, TaskUnderstanding
def ergebnis(antworten, warnungen=()):
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)], understandings=[TaskUnderstanding(
is_optimization_task=True, reason="-", facts=[], ignored_numbers=[],
sub_questions=[SubQuestion(id=f, quote="-", kind="rechnen", needs_solver=True) for f in ("a", "b")])])
r = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
r.solver_run.answers = [Answer(question_id=k, text="-") for k in antworten]
r.solver_run.warnings = list(warnungen)
return r
fall = {"name": "test", "task": "Aufgabe", "expected": {"objective_value": 9, "tolerance": 1e-6}}
for antworten, warnungen, erwartet in [
(["a", "b"], [], True), # alles beantwortet
(["b"], ["Teilfragen ohne Antwort: a"], False), # eine Frage offen
(["a", "b"], ["Antwort zu 'b' nennt Zahlen …"], False), # unbelegte Zahl
]:
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: ergebnis(antworten, warnungen))
zeile = benchmark.run_case(fall, "fake", 1)
assert zeile["objective_ok"] is True # die Zahl stimmt immer
assert zeile["task_ok"] is erwartet, (antworten, warnungen)
def test_benchmark_case_data_files_are_collected(tmp_path, monkeypatch):
from benchmark import benchmark
fall = tmp_path / "cases" / "mit_daten"
fall.mkdir(parents=True)
(fall / "task.txt").write_text("Aufgabe")
(fall / "expected.json").write_text('{"objective_value": 1, "tolerance": 1e-6}')
(fall / "werte.csv").write_text("a,b\n1,2\n")
monkeypatch.setattr(benchmark, "CASES_DIR", tmp_path / "cases")
faelle = benchmark.load_cases()
assert [p.split("/")[-1] for p in faelle[0]["data_files"]] == ["werte.csv"]
# Spezifikation ohne Kostendaten, Programm erfindet sie per Zufallsgenerator (Muster "Portfolio Nr. 2")
OHNE_DATEN = make_spec(parameters=[
{"symbol": "c_j", "dimension": "Vektor", "unit": "€/t", "description": "Kosten", "extracted_values": ""},
{"symbol": "b", "dimension": "Skalar", "unit": "t", "description": "Bedarf", "extracted_values": "4"}])
ERFUNDENE_DATEN = STRUCTURED_CODE.replace(
"COSTS = [3, 2]", "import numpy as np\nCOSTS = [float(v) for v in np.random.default_rng(0).integers(2, 4, 2)]")
def test_undeclared_generated_data_is_repaired():
session = FakeSession([OHNE_DATEN], [OK], [
SolverCode(code=ERFUNDENE_DATEN),
SolverCode(code=ERFUNDENE_DATEN, data_assumptions=["c_j: zufällig erzeugt (Seed 0), keine Daten im Text"])])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
assert session.calls[SolverCode] == 2 # der Befund hat eine Reparatur ausgelöst
assert result.reliable and result.solver_run.undeclared_data == []
def test_persistently_undeclared_data_is_unreliable():
# Nachmessung "Portfolio Nr. 2": data_assumptions leer, Antwort behauptete geladene Kursdaten
session = FakeSession([OHNE_DATEN], [OK], [SolverCode(code=ERFUNDENE_DATEN)] * 2)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
assert not result.reliable
assert any(p.startswith("Datenherkunft") for p in result.reliability_issues)
# Beobachtet: eine reine Gleichung (x! - 2x^2 + 1337 = 3629937) wurde nur mit Log-Warnung weiter modelliert
KEINE_AUFGABE = TaskUnderstanding(
is_optimization_task=False, reason="Eine Gleichung soll gelöst werden; es gibt nichts zu entscheiden.",
facts=[], ignored_numbers=[],
sub_questions=[SubQuestion(id="1", quote="x! - 2x**2 + 1337 = 3629937", kind="rechnen", needs_solver=True)])
def test_non_optimization_task_is_rejected_without_modeling(tmp_path):
from reporting.report import write_report
session = FakeSession(specs=[], reviews=[], understandings=[KEINE_AUFGABE])
result = run_pipeline("x! - 2x**2 + 1337 = 3629937", session=session)
assert result.rejected and not result.reliable
assert result.specification is None and result.solver_run is None
assert session.steps == ["Verstehen"] # kein Nachfassen, keine Extraktion, kein Solver
assert "Keine Optimierungsaufgabe" in result.reliability_issues[0]
for endung in ("md", "html"):
ziel = tmp_path / f"abgelehnt.{endung}"
write_report(result, str(ziel))
text = ziel.read_text(encoding="utf-8")
assert "Aufgabe abgelehnt" in text and "nichts zu entscheiden" in text
def test_rejected_result_cannot_be_continued_and_counts_as_failure(tmp_path, monkeypatch):
import pytest
from benchmark import benchmark
from TextToModel import continue_pipeline
session = FakeSession(specs=[], reviews=[], understandings=[KEINE_AUFGABE])
result = run_pipeline("x! = 120", session=session)
gespeichert = tmp_path / "abgelehnt.json"
gespeichert.write_text(result.model_dump_json())
with pytest.raises(ValueError, match="abgelehnt"):
continue_pipeline(str(gespeichert), "solver", session=session)
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: result)
zeile = benchmark.run_case({"name": "gleichung", "task": "x! = 120", "expected": {}}, "fake", 1)
assert zeile["solver_status"] == "abgelehnt" and zeile["task_ok"] is False
benchmark.print_table([zeile]) # die Ergebnistabelle verkraftet die Zeile
def _mit_szenario(wert):
return STRUCTURED_CODE.replace(
" )\n\n\nif __name__",
' scenarios=[{"name": "Ausgewogen", "description": "-", "data_override": {"faktor": 2}, '
f'"objective_value": {wert}}}],\n )\n\n\nif __name__')
def test_scenario_without_result_is_repaired():
# beobachtet (Portfolio Nr. 2): Zeitlimit ohne Lösung wurde als „keine Lösung" gemeldet
session = FakeSession([GOOD], [OK], [SolverCode(code=_mit_szenario(None)), SolverCode(code=_mit_szenario(8.0))])
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
assert session.calls[SolverCode] == 2 # der Befund hat eine Reparatur ausgelöst
assert result.reliable and result.solver_run.unsolved_scenarios == []
def test_persistent_scenario_without_result_is_unreliable():
session = FakeSession([GOOD], [OK], [SolverCode(code=_mit_szenario(None))] * 2)
result = run_pipeline("Minimiere die Kosten.", session=session, max_revisions=0, max_restarts=0, solver_retries=1)
assert not result.reliable
assert any(p.startswith("Szenario ohne Ergebnis") for p in result.reliability_issues)
# --- Nebenpfade: Verstehensstufe aus, Gegenprobe und --von mit vollem Kontext ---
def test_pipeline_runs_with_understanding_stage_disabled(monkeypatch):
# UNDERSTAND_RETRIES=-1 war der A/B-Arm der Verstehensstufe; das Ablehnungstor griff dann auf None zu
import TextToModel
monkeypatch.setattr(TextToModel, "UNDERSTAND_RETRIES", -1)
session = FakeSession([GOOD], [OK], [SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.understanding is None and not result.rejected
assert session.steps[0] == "Extraktion"
assert result.solver_run.status == SolverStatus.OPTIMAL
def test_infeasible_recheck_keeps_understanding_and_data_files(tmp_path, monkeypatch):
# Ohne Datendateien fand das Programm der Gegenprobe seine CSV nicht, ohne Verständnis fehlten die Antworten
import TextToModel
aufrufe = []
echtes_solve = TextToModel.solve
def protokoll(*args, **kwargs):
aufrufe.append(kwargs)
return echtes_solve(*args, **kwargs)
monkeypatch.setattr(TextToModel, "solve", protokoll)
csv = tmp_path / "werte.csv"
csv.write_text("a,b\n1,2\n", encoding="utf-8")
session = FakeSession(specs=[GOOD, GOOD], reviews=[OK, OK],
codes=[SolverCode(code=INFEASIBLE_CODE), SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0, data_files=[str(csv)])
assert result.restarts == 1 and len(aufrufe) == 2
gegenprobe = aufrufe[1]
assert gegenprobe["understanding"] is not None
assert list(gegenprobe["data_files"]) == [str(csv)] and "werte.csv" in gegenprobe["data_prompt"]
assert "werte.csv" in session.tasks[1] # auch die neue Extraktion sieht das Datenprofil
def test_continue_from_review_keeps_understanding(tmp_path):
from TextToModel import continue_pipeline
path = stored_result(tmp_path)
session = FakeSession([], [OK], [SolverCode(code=STRUCTURED_CODE)])
result = continue_pipeline(str(path), "pruefung", session=session)
assert result.understanding is not None
assert [q.id for q in result.understanding.sub_questions] == ["1"]
assert "ZU BEANTWORTENDE TEILFRAGEN" in session.letzter_prompt # der Solver bekommt die Teilfragen
def test_review_and_revision_see_the_data_profile():
from checks.review import review_assumptions
from modeling.extraction import revise_model
session = FakeSession([GOOD], [OK], [])
review_assumptions(session, "Aufgabe", GOOD, "BEIGELEGTE DATEN: kurse.csv")
assert "kurse.csv" in session.letzter_prompt
revise_model(session, "Aufgabe", GOOD, OK, data_prompt="BEIGELEGTE DATEN: kurse.csv")
assert "kurse.csv" in session.letzter_prompt
def test_repair_message_names_every_kind_of_finding():
# Vorher erfuhr das LLM nur von der ersten Befundart und verbrauchte für die zweite einen eigenen Versuch
from solving.solver_stage import solve
code = '''
import numpy as np
def main():
"""Verletzt die Restriktion und erzeugt Zahlen per Zufall, ohne sie auszuweisen."""
np.random.seed(0)
report("optimal", 9.0, {"x": 1.0},
constraints=[{"name": "Bedarf", "lhs": 1.0, "sense": ">=", "rhs": 4.0}],
variables=[{"name": "x", "value": 1.0, "var_type": "continuous", "lb": 0, "ub": None}])
if __name__ == "__main__":
main()
'''
session = FakeSession([], [], [SolverCode(code=code), SolverCode(code=code)])
solve(session, "Aufgabe", GOOD, retries=1)
assert session.steps == ["Solver-Code", "Solver-Reparatur"]
assert "hält das Modell nicht ein" in session.letzter_prompt
assert "Eingabedaten nicht ausgewiesen" in session.letzter_prompt
# --- Zielwert-Zerlegung in der Solver-Stufe ---
OHNE_ZERLEGUNG = STRUCTURED_CODE.split(" tables=[")[0] + " )\n\n\nif __name__ == \"__main__\":\n main()\n"
def test_missing_decomposition_is_repaired():
assert "tables" not in OHNE_ZERLEGUNG
session = FakeSession([GOOD], [OK], [SolverCode(code=OHNE_ZERLEGUNG), SolverCode(code=STRUCTURED_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert session.steps.count("Solver-Reparatur") == 1
assert "Zielwert-Zerlegung" in session.letzter_prompt
assert result.solver_run.attempts == 2 and result.reliable
def test_persistently_missing_decomposition_is_only_a_warning():
# Die Lösung ist geprüft zulässig – nur ihr Zielwert ist nicht aus Bestandteilen belegt
from solving.solver_stage import DECOMPOSITION_MISSING
session = FakeSession([GOOD], [OK], [SolverCode(code=OHNE_ZERLEGUNG)] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.reliable and result.solver_run.objective_value == 9
assert any(w.startswith(DECOMPOSITION_MISSING) for w in result.solver_run.warnings)
def test_untracked_files_do_not_mark_the_measurement_dirty(monkeypatch):
# Eine abgelegte Aufgabe in tasks/ ließ bisher jede Messung als „nicht eingecheckte Änderungen“ erscheinen
import subprocess
from benchmark import benchmark
aufrufe = []
def git(befehl, **_):
aufrufe.append(befehl)
return subprocess.CompletedProcess(befehl, 0, stdout="abc1234\n" if "rev-parse" in befehl else "")
monkeypatch.setattr(benchmark.subprocess, "run", git)
benchmark.prompt_state.cache_clear()
try:
stand = benchmark.prompt_state()
finally:
benchmark.prompt_state.cache_clear()
assert stand["dirty"] is False
assert any("--untracked-files=no" in befehl for befehl in aufrufe)
# --- Benchmarkfälle „ehrlich scheitern“ (expected["behavior"]) ---
def _ergebnisse():
"""Drei Ausgänge derselben Pipeline: abgelehnt, mit ausgewiesenen Annahmen gelöst, unzuverlässig."""
abgelehnt = run_pipeline("4x - 7 = 25", session=FakeSession([], [], understandings=[KEINE_AUFGABE]))
mit_annahme = run_pipeline("Aufgabe", session=FakeSession(
[GOOD], [OK], [SolverCode(code=STRUCTURED_CODE, data_assumptions=["Bedarf G3 = 0 Kisten (nicht genannt)"])]),
max_revisions=0, max_restarts=0)
unzuverlaessig = run_pipeline("Aufgabe", session=FakeSession([HALLUCINATED] * 3, [MISMATCH] * 3),
max_revisions=2, max_restarts=0)
return abgelehnt, mit_annahme, unzuverlaessig
def test_observed_behaviors():
from benchmark.benchmark import observed_behaviors
abgelehnt, mit_annahme, unzuverlaessig = _ergebnisse()
assert observed_behaviors(abgelehnt) == {"rejected"}
assert observed_behaviors(mit_annahme) == {"assumed_data"}
assert observed_behaviors(unzuverlaessig) == {"unreliable"}
def test_honest_failure_cases_are_scored_by_behavior(monkeypatch):
from benchmark import benchmark
abgelehnt, mit_annahme, unzuverlaessig = _ergebnisse()
fehlende_daten = {"behavior": ["assumed_data", "unreliable"]}
for ergebnis, expected, treffer in [
(abgelehnt, {"behavior": ["rejected"]}, True),
(mit_annahme, {"behavior": ["rejected"]}, False), # Gleichung modelliert statt abgelehnt
(mit_annahme, fehlende_daten, True), # Annahme ausgewiesen
(unzuverlaessig, fehlende_daten, True), # als unzuverlässig gekennzeichnet
(abgelehnt, fehlende_daten, False), # lösbare Aufgabe abgelehnt
]:
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: ergebnis)
zeile = benchmark.run_case({"name": "ehrlich", "task": "-", "expected": expected}, "fake", 1)
assert zeile["objective_ok"] is treffer, (zeile["behavior"], expected)
# mit ausgewiesener Annahme und beantworteter Teilfrage ist die Aufgabe erfüllt
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: mit_annahme)
mit_annahme.solver_run.answers = [Answer(question_id="1", text="Kosten 9 bei angenommenem Bedarf.")]
mit_annahme.solver_run.warnings = []
assert benchmark.run_case({"name": "e", "task": "-", "expected": fehlende_daten}, "fake", 1)["task_ok"] is True
def test_unknown_behavior_is_refused():
import pytest
from benchmark.benchmark import behavior_matches
abgelehnt = _ergebnisse()[0]
with pytest.raises(ValueError, match="Unbekanntes Verhalten"):
behavior_matches(abgelehnt, {"behavior": ["ehrlich"]})
def test_honest_failure_cases_are_well_formed():
from benchmark.benchmark import BEHAVIORS, load_cases
faelle = [f for f in load_cases() if "behavior" in f["expected"]]
assert len(faelle) >= 4
for fall in faelle:
assert fall["expected"]["behavior"] and set(fall["expected"]["behavior"]) <= set(BEHAVIORS)
assert "objective_value" not in fall["expected"] # kein Zielwert: bewertet wird das Verhalten
def test_expected_rejection_has_no_routing_to_score(monkeypatch):
from benchmark import benchmark
abgelehnt = _ergebnisse()[0]
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: abgelehnt)
erwartet = benchmark.run_case({"name": "g", "task": "-", "expected": {"behavior": ["rejected"]}}, "fake", 1)
falsch = benchmark.run_case({"name": "lp", "task": "-", "expected": {"objective_value": 9, "tolerance": 1e-6}}, "fake", 1)
assert erwartet["class_ok"] is None and falsch["class_ok"] is False # eine unerwartete Ablehnung bleibt ein Fehler
def test_missing_value_without_declared_assumption_is_repaired_or_unreliable():
# ehrlich_fehlender_bedarf: fehlender Wert still eingesetzt ⇒ Reparatur; bleibt es ⇒ unzuverlässig
from core.model_schema import ParameterDefinition
fehlend = GOOD.model_copy(update={"parameters": GOOD.parameters + [ParameterDefinition(
symbol="d_3", dimension="Skalar", unit="Kisten", description="Bedarf G3",
extracted_values="im Text nicht genannt")]})
ausgewiesen = SolverCode(code=STRUCTURED_CODE, data_assumptions=["d_3 = 0 Kisten angenommen, im Text nicht genannt"])
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE), ausgewiesen])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert session.steps.count("Solver-Reparatur") == 1 and result.reliable
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE)] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert not result.reliable and any(i.startswith("Datenherkunft") for i in result.reliability_issues)
# --- Solver-Fehler macht das Ergebnis unzuverlässig ---
KAPUTT = "def main():\n raise RuntimeError('Modell nicht lösbar gebaut')\n\n\nif __name__ == '__main__':\n main()\n"
def test_solver_error_after_all_repairs_is_unreliable():
# bisher blieb reliable=True: CLI-Exit 0, Report ohne Warnkasten
session = FakeSession([GOOD], [OK], [SolverCode(code=KAPUTT)] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.solver_run.status == SolverStatus.ERROR
assert not result.reliable
assert any(i.startswith("Solver-Fehler: ") and "Modell nicht lösbar gebaut" in i for i in result.reliability_issues)
def test_crash_is_not_an_honest_failure():
from benchmark.benchmark import observed_behaviors
session = FakeSession([GOOD], [OK], [SolverCode(code=KAPUTT)] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert observed_behaviors(result) == set()
def test_second_repair_knows_the_first_error():
# Die Reparatur sah nur den letzten Fehler – und konnte den vorletzten wieder einführen
from solving.solver_stage import solve
erster = "def main():\n raise RuntimeError('Fehler A: Index')\n\n\nif __name__ == '__main__':\n main()\n"
zweiter = "def main():\n raise RuntimeError('Fehler B: Typ')\n\n\nif __name__ == '__main__':\n main()\n"
session = FakeSession([], [], [SolverCode(code=erster), SolverCode(code=zweiter), SolverCode(code=STRUCTURED_CODE)])
solve(session, "Aufgabe", GOOD, retries=2)
reparaturen = [inhalt for stufe, inhalt in session.contents if stufe == "Solver-Reparatur"]
assert "FRÜHERE FEHLVERSUCHE" not in reparaturen[0] # beim ersten gibt es noch keinen Verlauf
assert "Fehler B: Typ" in reparaturen[1] and "Versuch 1: " in reparaturen[1] and "Fehler A: Index" in reparaturen[1]
# --- LLM nach allen Wiederholungen gescheitert: unzuverlässiges Ergebnis statt Absturz ---
def _llm_weg(step):
from core.llm import LLMUnavailable
return LLMUnavailable(step, RuntimeError("The output is incomplete due to a max_tokens length limit."))
def test_llm_failure_in_solver_stage_keeps_the_model():
# beobachtet: hard_portfolio_kardinalitaet, viermal abgeschnitten ⇒ Absturz nach 1162 s, Kosten verloren
session = FakeSession([GOOD], [OK], [_llm_weg("Solver-Code")])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert not result.reliable and result.specification is not None and result.review is not None
assert result.solver_run.status == SolverStatus.ERROR and result.solver_code is None
assert result.reliability_issues[0].startswith("Solver-Fehler: LLM-Aufruf 'Solver-Code'")
assert result.usage is not None
def test_llm_failure_before_the_model_is_a_paid_non_result(tmp_path):
from benchmark.benchmark import observed_behaviors
from reporting.report import write_report
session = FakeSession([_llm_weg("Extraktion")], [])
result = run_pipeline("Aufgabe", session=session)
assert not result.reliable and not result.rejected and result.specification is None
assert result.reliability_issues[0].startswith("LLM-Fehler: ") and result.usage.calls == 2
assert observed_behaviors(result) == set() # ein Absturz ist kein ehrliches Scheitern
write_report(result, str(tmp_path / "r.md"))
text = (tmp_path / "r.md").read_text(encoding="utf-8")
assert "Lauf abgebrochen" in text and "abgelehnt" not in text
def test_benchmark_counts_an_llm_failure_as_failure_with_costs(monkeypatch):
from benchmark import benchmark
session = FakeSession([_llm_weg("Extraktion")], [])
monkeypatch.setattr(benchmark, "run_pipeline", lambda *a, **k: run_pipeline("Aufgabe", session=session))
zeile = benchmark.run_case({"name": "t", "task": "Aufgabe", "expected": {"objective_value": 9, "tolerance": 1e-6}},
"fake", 1)
assert zeile["solver_status"] == "LLM-Fehler" and zeile["llm_calls"] == 2 and "error" not in zeile
assert zeile["objective_ok"] is False and zeile["task_ok"] is False
def test_llm_failure_in_the_infeasible_recheck_keeps_the_first_result():
session = FakeSession(specs=[GOOD, _llm_weg("Extraktion")], reviews=[OK], codes=[SolverCode(code=INFEASIBLE_CODE)])
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0)
assert result.solver_run.status == SolverStatus.INFEASIBLE and not result.infeasible_confirmed
assert not result.reliable and "Gegenprobe scheiterte am LLM" in result.reliability_issues[-1]