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:
parent
29d058d8f1
commit
f101b63947
5 changed files with 149 additions and 13 deletions
|
|
@ -191,3 +191,14 @@ def test_line_breaks_of_the_task_are_kept():
|
|||
from reporting.report import task_html
|
||||
seite = task_html("Pfeile:\n1 → 2: 3\n2 → 4: 5")
|
||||
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
19
tests/test_lint.py
Normal 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
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from checks.model_checks import check_spec, has_errors
|
||||
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.")
|
||||
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"}
|
||||
|
||||
|
||||
@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
|
||||
|
|
|
|||
|
|
@ -820,3 +820,36 @@ def test_missing_value_without_declared_assumption_is_repaired_or_unreliable():
|
|||
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]
|
||||
|
|
|
|||
|
|
@ -109,3 +109,13 @@ def test_resolve_keeps_earlier_reliability_issues(tmp_path):
|
|||
neu = resolve_result(str(pfad))
|
||||
assert neu.reliability_issues == ["Zielfunktion passt nicht zur Frage"] # alter Zulässigkeitsbefund entfällt
|
||||
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: ")
|
||||
|
|
|
|||
Loading…
Reference in a new issue