text_to_solver_program/tests/test_resolve.py
dschlueter f101b63947 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>
2026-09-18 18:15:22 +02:00

121 lines
4.8 KiB
Python
Raw Permalink 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.

"""Punkt 1: dasselbe Programm mit geänderten Daten neu rechnen – ohne LLM-Aufruf."""
import pytest
from core.model_schema import SolverStatus
from solving.solver_stage import run_solver_code
# Programm nach der Vorgabe des Solver-Prompts: alle Zahlen stehen in DATA
DATA_CODE = '''
from scipy.optimize import linprog
DATA = {
"kosten": [3, 2], # Kosten je Tonne [Tsd. €]
"bedarf": 4, # Mindestmenge [t]
"obergrenze": 3, # je Rohstoff lieferbar [t]
}
def solve_model():
"""Baut das LP aus DATA auf und löst es."""
grenze = DATA["obergrenze"]
return linprog(DATA["kosten"], A_ub=[[-1, -1]], b_ub=[-DATA["bedarf"]],
bounds=[(0, grenze), (0, grenze)], method="highs")
def main():
"""Löst das Modell und meldet Ergebnis samt Restriktionen."""
res = solve_model()
x, y = float(res.x[0]), float(res.x[1])
report("optimal" if res.status == 0 else "error", float(res.fun), {"x": x, "y": y},
constraints=[{"name": "Bedarf", "lhs": x + y, "sense": ">=", "rhs": DATA["bedarf"]}],
variables=[{"name": "x", "value": x, "var_type": "continuous", "lb": 0, "ub": DATA["obergrenze"]}])
if __name__ == "__main__":
main()
'''
def test_run_without_override_uses_own_data():
run = run_solver_code(DATA_CODE)
assert run.status == SolverStatus.OPTIMAL
assert run.objective_value == pytest.approx(9) # 1t * 3 + 3t * 2
assert run.data["bedarf"] == 4
def test_changed_data_is_used_without_touching_the_code():
run = run_solver_code(DATA_CODE, data={"bedarf": 5})
assert run.status == SolverStatus.OPTIMAL
assert run.objective_value == pytest.approx(12) # 2t * 3 + 3t * 2
assert run.data["bedarf"] == 5 and run.data["kosten"] == [3, 2] # nur der geänderte Wert wird ersetzt
def test_constraints_follow_the_changed_data():
run = run_solver_code(DATA_CODE, data={"bedarf": 5})
assert run.constraints[0].rhs == 5
def test_infeasible_data_is_reported_not_crashing():
run = run_solver_code(DATA_CODE, data={"bedarf": 99})
assert run.status in (SolverStatus.ERROR, SolverStatus.INFEASIBLE)
# --- CLI-Modus: gespeichertes Ergebnis neu rechnen ---
def gespeichertes_ergebnis(tmp_path):
from core.model_schema import PipelineResult, SolverCode
from test_model_checks import make_spec
from test_review import make_review
result = PipelineResult(task="Aufgabe", specification=make_spec(), review=make_review(), revisions=0,
solver_code=SolverCode(code=DATA_CODE))
pfad = tmp_path / "ergebnis.json"
pfad.write_text(result.model_dump_json(indent=2), encoding="utf-8")
return pfad
def test_resolve_without_data_reproduces_the_result(tmp_path):
from TextToModel import resolve_result
neu = resolve_result(str(gespeichertes_ergebnis(tmp_path)))
assert neu.solver_run.objective_value == pytest.approx(9)
assert neu.reliable and neu.usage is None # kein LLM-Aufruf
def test_resolve_with_changed_data(tmp_path):
import json
from TextToModel import resolve_result
daten = tmp_path / "daten.json"
daten.write_text(json.dumps({"bedarf": 5}), encoding="utf-8")
neu = resolve_result(str(gespeichertes_ergebnis(tmp_path)), str(daten))
assert neu.solver_run.objective_value == pytest.approx(12)
assert neu.solver_run.data["bedarf"] == 5
def test_resolve_keeps_specification_and_checks_feasibility(tmp_path):
from TextToModel import resolve_result
neu = resolve_result(str(gespeichertes_ergebnis(tmp_path)))
assert neu.specification.problem_class.value.startswith("Linear")
assert neu.solver_run.feasibility == []
def test_resolve_keeps_earlier_reliability_issues(tmp_path):
# Neurechnen prüft nur die Zulässigkeit neu – eine falsche Zielgröße bleibt ein Befund
from core.model_schema import PipelineResult
from TextToModel import resolve_result
pfad = gespeichertes_ergebnis(tmp_path)
alt = PipelineResult.model_validate_json(pfad.read_text(encoding="utf-8"))
alt.reliable = False
alt.reliability_issues = ["Zielfunktion passt nicht zur Frage", "Zulässigkeitsprüfung: alter Befund"]
pfad.write_text(alt.model_dump_json(), encoding="utf-8")
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: ")