fix: Solver-Fehler gilt als unzuverlässig; Aufräumrunde ohne LLM

- Status error nach allen Reparaturen -> reliable=False („Solver-Fehler: …“, auch bei
  --resolve); vorher Exit-Code 0 ohne Warnkasten. Benchmark zählt einen Absturz nicht
  als ehrliches Scheitern.
- Report: „längster Weg“ bei maximize; Harness druckt „Gefundener Weg“.
- check_requested_method: Verneinung kurz davor im selben Satz („ohne/nicht/statt/
  anders als Dijkstra“) ist keine Forderung.
- Reparatur-Prompt enthält die Fehlertexte aller früheren Versuche.
- ruff (F, E9) als Test tests/test_lint.py, Konfiguration in pyproject.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-18 18:15:22 +02:00
commit f101b63947
13 changed files with 149 additions and 13 deletions

View file

@ -270,6 +270,7 @@ Der Report folgt dem Weg von der Aufgabe zur Lösung. Ganz oben stehen die **Lau
| Zulässigkeitsprüfung | Die gemeldete Lösung verletzt Restriktionen, Schranken oder Ganzzahligkeit. | `--von solver` versuchen. | | Zulässigkeitsprüfung | Die gemeldete Lösung verletzt Restriktionen, Schranken oder Ganzzahligkeit. | `--von solver` versuchen. |
| Datenherkunft | Das Programm hat Eingabedaten erzeugt, ohne sie auszuweisen. | Daten beilegen (`--daten`) oder fehlende Werte in den Text schreiben. | | Datenherkunft | Das Programm hat Eingabedaten erzeugt, ohne sie auszuweisen. | Daten beilegen (`--daten`) oder fehlende Werte in den Text schreiben. |
| Szenario ohne Ergebnis | Eine verglichene Konfiguration hat keinen Wert, ist aber nicht als unlösbar belegt (z. B. Zeitlimit). | `--von solver`. | | Szenario ohne Ergebnis | Eine verglichene Konfiguration hat keinen Wert, ist aber nicht als unlösbar belegt (z. B. Zeitlimit). | `--von solver`. |
| Solver-Fehler | Das erzeugte Programm ist auch nach allen Reparaturversuchen gescheitert (Absturz, Zeitlimit, kein Ergebnis). Die Fehlermeldung steht im Abschnitt „Ergebnis“. | `--von solver`; bei Zeitlimit `SOLVER_TIMEOUT` erhöhen. |
| Unlösbar, Gegenprobe uneindeutig | Der Solver meldet „unlösbar“, eine unabhängige zweite Modellierung konnte das weder bestätigen noch widerlegen. | Aufgabe auf widersprüchliche Bedingungen prüfen. | | Unlösbar, Gegenprobe uneindeutig | Der Solver meldet „unlösbar“, eine unabhängige zweite Modellierung konnte das weder bestätigen noch widerlegen. | Aufgabe auf widersprüchliche Bedingungen prüfen. |
### Besondere Ergebnisse ### Besondere Ergebnisse

View file

@ -213,6 +213,11 @@ def _assess_run(result: PipelineResult) -> None:
"""Abnahme eines Solver-Laufs: Befunde, die das Ergebnis unzuverlässig machen, in `result` übernehmen.""" """Abnahme eines Solver-Laufs: Befunde, die das Ergebnis unzuverlässig machen, in `result` übernehmen."""
run = result.solver_run run = result.solver_run
log(f"Solver: {run.status.value}, Zielwert: {run.objective_value}" + (f"\n {run.error}" if run.error else "")) log(f"Solver: {run.status.value}, Zielwert: {run.objective_value}" + (f"\n {run.error}" if run.error else ""))
if run.status == SolverStatus.ERROR:
# Auch nach allen Reparaturen kein Ergebnis – bisher blieb reliable=True und die CLI endete mit 0
result.reliable = False
result.reliability_issues.append(f"Solver-Fehler: {solver_error_summary(run)}")
log("ERGEBNIS UNZUVERLÄSSIG – das Solver-Programm ist auch nach allen Reparaturversuchen gescheitert.")
if run.feasibility: if run.feasibility:
result.reliable = False result.reliable = False
result.reliability_issues += [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility] result.reliability_issues += [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility]
@ -233,6 +238,12 @@ def _assess_run(result: PipelineResult) -> None:
log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).") log(f" Mit angenommenen Daten gelöst ({len(result.solver_code.data_assumptions)} Annahmen).")
def solver_error_summary(run) -> str:
"""Erste aussagekräftige Zeile des Fehlers – der volle Text steht im Report unter „Ergebnis“."""
zeilen = [z.strip() for z in (run.error or "").splitlines() if z.strip()]
return (zeilen[-1] if zeilen else "das Programm meldete selbst Status 'error'")[:200]
def _log_usage(session, result: PipelineResult) -> None: def _log_usage(session, result: PipelineResult) -> None:
result.usage = session.usage result.usage = session.usage
cost = f", Kosten ${session.usage.cost_usd:.4f}" if session.usage.cost_usd is not None else "" cost = f", Kosten ${session.usage.cost_usd:.4f}" if session.usage.cost_usd is not None else ""
@ -394,8 +405,11 @@ def resolve_result(result_json: str, data_file: str | None = None, solver_timeou
run.warnings += check_multistart(run, result.specification.objective.sense.value) run.warnings += check_multistart(run, result.specification.objective.sense.value)
result.solver_run = run result.solver_run = run
# Frühere Befunde (Zielgröße, Abbildung, Datenherkunft) gelten weiter – nur die Zulässigkeit ist neu geprüft # Frühere Befunde (Zielgröße, Abbildung, Datenherkunft) gelten weiter – nur Lauf und Zulässigkeit sind neu geprüft
result.reliability_issues = ([p for p in result.reliability_issues if not p.startswith("Zulässigkeitsprüfung: ")] result.reliability_issues = ([p for p in result.reliability_issues
if not p.startswith(("Zulässigkeitsprüfung: ", "Solver-Fehler: "))]
+ ([f"Solver-Fehler: {solver_error_summary(run)}"]
if run.status == SolverStatus.ERROR else [])
+ [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility]) + [f"Zulässigkeitsprüfung: {p}" for p in run.feasibility])
result.reliable = not result.reliability_issues result.reliable = not result.reliability_issues
result.usage = None # keine LLM-Nutzung in diesem Lauf result.usage = None # keine LLM-Nutzung in diesem Lauf

View file

@ -105,7 +105,8 @@ def observed_behaviors(result) -> set:
gezeigt = set() gezeigt = set()
if result.rejected: if result.rejected:
return {"rejected"} return {"rejected"}
if not result.reliable: # Ein Absturz ist kein Eingeständnis: nur Befunde außer „Solver-Fehler“ zählen als ehrlich unzuverlässig
if not result.reliable and any(not i.startswith("Solver-Fehler: ") for i in result.reliability_issues):
gezeigt.add("unreliable") gezeigt.add("unreliable")
if result.solver_code is not None and result.solver_code.data_assumptions: if result.solver_code is not None and result.solver_code.data_assumptions:
gezeigt.add("assumed_data") gezeigt.add("assumed_data")

View file

@ -503,12 +503,22 @@ REQUESTED_METHOD = {
} }
# „ohne Dijkstra“, „nicht mit dem Simplex“, „statt Branch-and-Bound“: Erwähnung, keine Forderung. Nur im selben
# Satz und kurz davor – „Lösen Sie mit Dijkstra. Ohne Rechenweg gibt es keine Punkte.“ bleibt eine Forderung.
NEGATION_BEFORE = re.compile(r"\b(?:ohne|nicht|keine?[nmrs]?|statt|anstelle|anstatt|außer|ausser|anders als)\b[^.!?;\n]{0,30}$")
def _requested(name: str, text: str) -> bool:
"""Kommt das Verfahren mindestens einmal ohne vorangehende Verneinung vor?"""
return any(not NEGATION_BEFORE.search(text[:m.start()]) for m in re.finditer(re.escape(name), text))
def check_requested_method(spec: OptimizationModelSpecification, task: str) -> List[CheckIssue]: def check_requested_method(spec: OptimizationModelSpecification, task: str) -> List[CheckIssue]:
"""Ein im Aufgabentext ausdrücklich verlangtes Verfahren muss auch verwendet werden.""" """Ein im Aufgabentext ausdrücklich verlangtes Verfahren muss auch verwendet werden."""
text = (task or "").lower() text = (task or "").lower()
issues = [] issues = []
for name, stacks in REQUESTED_METHOD.items(): for name, stacks in REQUESTED_METHOD.items():
if name in text and spec.target_stack not in stacks: if _requested(name, text) and spec.target_stack not in stacks:
issues.append(CheckIssue( issues.append(CheckIssue(
severity=CheckSeverity.ERROR, code="REQUESTED_METHOD_IGNORED", severity=CheckSeverity.ERROR, code="REQUESTED_METHOD_IGNORED",
message=f"Die Aufgabe verlangt ausdrücklich '{name}', das Modell nutzt aber " message=f"Die Aufgabe verlangt ausdrücklich '{name}', das Modell nutzt aber "

View file

@ -1,3 +1,11 @@
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = [".", "tests"] pythonpath = [".", "tests"]
[tool.ruff]
extend-exclude = ["output", "benchmark/results"]
[tool.ruff.lint]
# Fehlerklassen, die tatsächlich Fehler sind: unbenutzte/undefinierte Namen (F), Syntaxfehler (E9).
# Stilregeln bewusst nicht – der Code folgt eigenen Konventionen (deutsche Namen, lange Zeilen).
select = ["F", "E9"]

View file

@ -128,20 +128,24 @@ def split_math(text: str) -> list:
return _join_text(parts) return _join_text(parts)
def _graph_section(run) -> list: def _graph_section(run, sense: str = "minimize") -> list:
"""Zeichnung des Graphen mit hervorgehobenem Weg, darüber die Bedeutung von Knoten und Bogen.""" """Zeichnung des Graphen mit hervorgehobenem Weg, darüber die Bedeutung von Knoten und Bogen.
Bei maximize (längster Weg, z. B. kritischer Pfad) heißt der Weg nicht „kürzester“.
"""
g = run.graph g = run.graph
if g is None: if g is None:
return [] return []
weg = "längste" if sense == "maximize" else "kürzeste"
doc = [("p", f"Knoten des Graphen: {g.node_meaning} Ein Bogen: {g.arc_meaning}")] doc = [("p", f"Knoten des Graphen: {g.node_meaning} Ein Bogen: {g.arc_meaning}")]
zeichnung = charts.graph_chart([n for n in g.nodes], [(a.source, a.target, a.weight) for a in g.arcs], zeichnung = charts.graph_chart([n for n in g.nodes], [(a.source, a.target, a.weight) for a in g.arcs],
path=list(g.path), title="Graph mit kürzestem Weg") path=list(g.path), title=f"Graph mit {weg}m Weg")
if zeichnung: if zeichnung:
doc.append(("svg", zeichnung)) doc.append(("svg", zeichnung))
if g.path and run.objective_value is not None: if g.path and run.objective_value is not None:
auf_weg = set(zip(g.path, g.path[1:])) auf_weg = set(zip(g.path, g.path[1:]))
summanden = " + ".join(_fmt_number(a.weight) for a in g.arcs if (a.source, a.target) in auf_weg) summanden = " + ".join(_fmt_number(a.weight) for a in g.arcs if (a.source, a.target) in auf_weg)
doc.append(("p", "Hervorgehoben ist der kürzeste Weg " + " → ".join(g.path) doc.append(("p", f"Hervorgehoben ist der {weg} Weg " + " → ".join(g.path)
+ f" mit der Länge {summanden} = {_fmt_number(run.objective_value)}.")) + f" mit der Länge {summanden} = {_fmt_number(run.objective_value)}."))
if g.steps: if g.steps:
doc += [("h4", "Markierungsverlauf des Dijkstra-Algorithmus"), doc += [("h4", "Markierungsverlauf des Dijkstra-Algorithmus"),
@ -182,7 +186,7 @@ def _result_section(result) -> list:
f"{len(run.variables)} Variablen wurden aus der Lösung nachgerechnet und eingehalten.")) f"{len(run.variables)} Variablen wurden aus der Lösung nachgerechnet und eingehalten."))
# Die Erklärung („Wie das Modell die Aufgabe sieht“) steht als eigener Abschnitt vor dem Modell, also schon # Die Erklärung („Wie das Modell die Aufgabe sieht“) steht als eigener Abschnitt vor dem Modell, also schon
# vor diesem Ergebnis – die Fachbegriffe der Tabellen sind damit eingeführt, bevor sie auftauchen. # vor diesem Ergebnis – die Fachbegriffe der Tabellen sind damit eingeführt, bevor sie auftauchen.
doc += _graph_section(run) doc += _graph_section(run, spec.objective.sense.value)
for table in run.tables: for table in run.tables:
doc += [("h3", table.title), doc += [("h3", table.title),
("table", table.columns, [[_fmt_cell(c) for c in row] for row in table.rows])] ("table", table.columns, [[_fmt_cell(c) for c in row] for row in table.rows])]

View file

@ -13,3 +13,4 @@ pandas>=2.2
markdown-it-py>=3.0 # Aufgabentext im HTML-Report (bisher nur indirekt über rich) markdown-it-py>=3.0 # Aufgabentext im HTML-Report (bisher nur indirekt über rich)
numpy numpy
pytest pytest
ruff # tests/test_lint.py: unbenutzte und undefinierte Namen

View file

@ -115,7 +115,7 @@ def report(status, objective_value, solution, tables=None, headline=None, constr
print(" Ein Knoten ist: " + str(graph.get("node_meaning", ""))) print(" Ein Knoten ist: " + str(graph.get("node_meaning", "")))
print(" Ein Bogen ist: " + str(graph.get("arc_meaning", ""))) print(" Ein Bogen ist: " + str(graph.get("arc_meaning", "")))
if graph.get("path"): if graph.get("path"):
print(" Kürzester Weg: " + " → ".join(str(k) for k in graph["path"])) print(" Gefundener Weg: " + " → ".join(str(k) for k in graph["path"])) # kürzester oder längster
for schritt in graph.get("steps") or []: for schritt in graph.get("steps") or []:
print(f" Marke endgültig: {{schritt.get('node')}} = {{schritt.get('label')}}") print(f" Marke endgültig: {{schritt.get('node')}} = {{schritt.get('label')}}")
if solution: if solution:
@ -644,7 +644,7 @@ def solver_prompt() -> str:
def generate_solver_code( def generate_solver_code(
session: LLMSession, task: str, spec: OptimizationModelSpecification, session: LLMSession, task: str, spec: OptimizationModelSpecification,
previous: Optional[SolverCode] = None, previous_error: Optional[str] = None, understanding=None, previous: Optional[SolverCode] = None, previous_error: Optional[str] = None, understanding=None,
data_prompt: str = "", data_prompt: str = "", earlier_errors=(),
) -> SolverCode: ) -> SolverCode:
content = f"AUFGABENTEXT:\n{task}\n\nMODELLSPEZIFIKATION:\n{spec.model_dump_json(indent=2)}" content = f"AUFGABENTEXT:\n{task}\n\nMODELLSPEZIFIKATION:\n{spec.model_dump_json(indent=2)}"
if data_prompt: if data_prompt:
@ -655,9 +655,13 @@ def generate_solver_code(
content += ("\n\nZU BEANTWORTENDE TEILFRAGEN (für jede genau ein Eintrag in answers, " content += ("\n\nZU BEANTWORTENDE TEILFRAGEN (für jede genau ein Eintrag in answers, "
f"question_id wie angegeben):\n{fragen}") f"question_id wie angegeben):\n{fragen}")
if previous is not None: if previous is not None:
# Ältere Fehlversuche nur als Fehlertext (nicht der ganze Code): sonst behebt die Reparatur den letzten Fehler
# und führt den vorletzten wieder ein – die Versuche gehen im Kreis.
frueher = "".join(f"\n- Versuch {n}: {fehler[:400]}" for n, fehler in enumerate(earlier_errors, 1))
content += ( content += (
f"\n\nDER VORHERIGE CODE IST FEHLGESCHLAGEN.\nCODE:\n{previous.code}\n\nFEHLER:\n{previous_error}\n\n" f"\n\nDER VORHERIGE CODE IST FEHLGESCHLAGEN.\nCODE:\n{previous.code}\n\nFEHLER:\n{previous_error}\n\n"
"Behebe den Fehler. Das Modell selbst darf dabei nicht verändert werden." + (f"FRÜHERE FEHLVERSUCHE (schon einmal aufgetreten – nicht wieder einführen):{frueher}\n\n" if frueher else "")
+ "Behebe den Fehler. Das Modell selbst darf dabei nicht verändert werden."
) )
return session.create( return session.create(
SolverCode, SolverCode,
@ -1176,6 +1180,7 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
zerlegung = check_objective_decomposition(run, spec) zerlegung = check_objective_decomposition(run, spec)
verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec) verschwiegen = check_answers_disclose_assumptions(run, solver_code.code, solver_code, spec)
attempt = 1 attempt = 1
fehlerverlauf = [] # Fehler aller bisherigen Versuche, für die Reparatur
while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios while (run.status == SolverStatus.ERROR or run.feasibility or run.undeclared_data or run.unsolved_scenarios
or einheiten or zerlegung or verschwiegen) and attempt <= retries: or einheiten or zerlegung or verschwiegen) and attempt <= retries:
# Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch # Alle Befundarten zugleich melden – sonst kostet jede weitere Art einen eigenen Reparaturversuch
@ -1190,7 +1195,9 @@ def solve(session: LLMSession, task: str, spec: OptimizationModelSpecification,
if befunde) if befunde)
log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}") log(f"Solver-Code fehlgeschlagen – Reparaturversuch {attempt}/{retries}: {fehler[:200]}")
solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler, solver_code = generate_solver_code(session, task, spec, previous=solver_code, previous_error=fehler,
understanding=understanding, data_prompt=data_prompt) understanding=understanding, data_prompt=data_prompt,
earlier_errors=fehlerverlauf)
fehlerverlauf.append(fehler)
solver_code.code = clean_generated_code(solver_code.code) solver_code.code = clean_generated_code(solver_code.code)
run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt) run = _check_and_run(solver_code.code, timeout, spec, understanding, task, data_files, data_prompt)
run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec, data_files) run.undeclared_data = check_generated_data(solver_code.code, solver_code, spec, data_files)

View file

@ -191,3 +191,14 @@ def test_line_breaks_of_the_task_are_kept():
from reporting.report import task_html from reporting.report import task_html
seite = task_html("Pfeile:\n1 → 2: 3\n2 → 4: 5") seite = task_html("Pfeile:\n1 → 2: 3\n2 → 4: 5")
assert seite.count("<br") == 2 assert seite.count("<br") == 2
def test_longest_path_is_not_called_shortest():
from core.model_schema import GraphSolution, SolverRun, SolverStatus
from reporting.report import _graph_section
run = SolverRun(status=SolverStatus.OPTIMAL, objective_value=29, graph=GraphSolution.model_validate({
"nodes": ["0", "1", "3"], "path": ["0", "1", "3"], "node_meaning": "-", "arc_meaning": "-",
"arcs": [{"source": "0", "target": "1", "weight": 8}, {"source": "1", "target": "3", "weight": 21}]}))
assert "kürzeste" in str(_graph_section(run)) and "längste" not in str(_graph_section(run))
lang = str(_graph_section(run, "maximize"))
assert "Hervorgehoben ist der längste Weg" in lang and "kürzest" not in lang

19
tests/test_lint.py Normal file
View file

@ -0,0 +1,19 @@
"""Statische Prüfung mit ruff: unbenutzte Imports und undefinierte Namen fallen im Test auf, nicht erst im Review.
Anlass: bei der Fehlerdurchsicht am 18.09.2026 fand ruff 8 tote Imports und einen undefinierten Namen in einem
Testhelfer; nichts davon hatte ein Test bemerkt. Regeln in pyproject.toml ([tool.ruff.lint]).
"""
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
def test_ruff_finds_nothing():
pytest.importorskip("ruff")
lauf = subprocess.run([sys.executable, "-m", "ruff", "check", "--no-cache", "--output-format", "concise", str(ROOT)],
capture_output=True, text=True, cwd=ROOT)
assert lauf.returncode == 0, lauf.stdout + lauf.stderr

View file

@ -1,3 +1,5 @@
import pytest
from checks.model_checks import check_spec, has_errors from checks.model_checks import check_spec, has_errors
from core.model_schema import OptimizationModelSpecification from core.model_schema import OptimizationModelSpecification
@ -495,3 +497,18 @@ def test_heading_markers_win_over_numbered_lists():
"### Teil 1: Formulierung\nKlassifizieren Sie das Problem.\n\n### Teil 2: Lösung\nLösen Sie das Modell.") "### Teil 1: Formulierung\nKlassifizieren Sie das Problem.\n\n### Teil 2: Lösung\nLösen Sie das Modell.")
assert question_ids_in(text) == {"1", "2"} assert question_ids_in(text) == {"1", "2"}
assert question_ids_in("1. Berechnen Sie x.\n2. Deuten Sie y.\n3. Vergleichen Sie.") == {"1", "2", "3"} assert question_ids_in("1. Berechnen Sie x.\n2. Deuten Sie y.\n3. Vergleichen Sie.") == {"1", "2", "3"}
@pytest.mark.parametrize("text, verlangt", [
("Lösen Sie das Problem ohne den Dijkstra-Algorithmus, als Flussmodell.", False),
("Anders als beim Dijkstra-Verfahren sind hier negative Kosten möglich – nicht mit Dijkstra rechnen.", False),
("Verwenden Sie statt Dijkstra ein lineares Programm.", False),
("b) Lösen Sie es mit dem Dijkstra-Algorithmus!", True),
("Lösen Sie mit Dijkstra. Ohne Rechenweg gibt es keine Punkte.", True), # Verneinung im nächsten Satz
("Nicht jede Kante ist befahrbar. Bestimmen Sie den Weg mit Dijkstra.", True),
])
def test_negated_method_mention_is_not_a_request(text, verlangt):
spec = make_spec(problem_class="Mixed-Integer Linear Programming (MILP)", target_stack="HiGHS",
variables=[{"name": "x_{ij}", "var_type": "binary", "domain": "{0,1}", "description": "-"}])
gemeldet = "REQUESTED_METHOD_IGNORED" in {i.code for i in check_spec(spec, text)}
assert gemeldet is verlangt

View file

@ -820,3 +820,36 @@ def test_missing_value_without_declared_assumption_is_repaired_or_unreliable():
session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE)] * 3) session = FakeSession([fehlend], [OK], [SolverCode(code=STRUCTURED_CODE)] * 3)
result = run_pipeline("Aufgabe", session=session, max_revisions=0, max_restarts=0) 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) 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]

View file

@ -109,3 +109,13 @@ def test_resolve_keeps_earlier_reliability_issues(tmp_path):
neu = resolve_result(str(pfad)) neu = resolve_result(str(pfad))
assert neu.reliability_issues == ["Zielfunktion passt nicht zur Frage"] # alter Zulässigkeitsbefund entfällt assert neu.reliability_issues == ["Zielfunktion passt nicht zur Frage"] # alter Zulässigkeitsbefund entfällt
assert not neu.reliable assert not neu.reliable
def test_resolve_with_a_failing_program_is_unreliable(tmp_path):
import json
from TextToModel import resolve_result
daten = tmp_path / "daten.json"
daten.write_text(json.dumps({"kosten": "keine Zahl"}), encoding="utf-8")
neu = resolve_result(str(gespeichertes_ergebnis(tmp_path)), str(daten))
assert neu.solver_run.status == SolverStatus.ERROR
assert not neu.reliable and neu.reliability_issues[0].startswith("Solver-Fehler: ")