refactor: Pipeline-Stufen in Ordner mit ihren Prompts
Im Wurzelverzeichnis liegt nur noch TextToModel.py (Ablauf und CLI). Jede Stufe liegt mit Code und Prompt in einem eigenen Ordner: core, understanding, modeling, checks, solving, reporting, data, benchmark; Tests unter tests/, Aufgaben unter tasks/. understand_task, text_to_model/revise_model und review_assumptions/needs_work/select_best sind aus TextToModel.py zu ihren Prompts gewandert, inhaltlich unveraendert. Prompt-Kennung des Benchmarks bleibt 93744636095a. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b74445b8d6
commit
7459690bf5
1 changed files with 315 additions and 279 deletions
138
tests/test_charts.py
Normal file
138
tests/test_charts.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Inline-SVG-Diagramme: gültiges SVG, korrekte Geometrie, Einbindung in den Report."""
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
||||
from reporting import charts
|
||||
from core.model_schema import PipelineResult, ResultTable, SolverRun, SolverStatus
|
||||
from reporting.report import build_document, to_html, to_markdown
|
||||
from test_model_checks import make_spec
|
||||
from test_result_tables import PORTFOLIO_TABLE
|
||||
from test_review import make_review
|
||||
|
||||
LABELS = ["A02", "A08", "A03"]
|
||||
VALUES = [0.03433, 0.03093, 0.02587]
|
||||
|
||||
|
||||
def parse(svg: str):
|
||||
assert svg.startswith("<svg")
|
||||
return ET.fromstring(svg)
|
||||
|
||||
|
||||
def test_bar_chart_is_valid_svg_with_one_bar_per_label():
|
||||
root = parse(charts.bar_chart(LABELS, VALUES, "Portfolio"))
|
||||
assert len(root.findall(".//{http://www.w3.org/2000/svg}path")) == len(LABELS)
|
||||
texts = [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}text")]
|
||||
for label in LABELS:
|
||||
assert label in texts # Direktbeschriftung statt Legende (eine Serie)
|
||||
assert root.get("role") == "img" and "Portfolio" in root.get("aria-label")
|
||||
|
||||
|
||||
def test_bars_are_proportional_and_capped_in_thickness():
|
||||
svg = charts.bar_chart(["groß", "klein"], [1.0, 0.5], "Test")
|
||||
widths = [float(d.split("H")[1].split(" ")[0].split(",")[0]) for d in
|
||||
[p.get("d") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")]]
|
||||
assert widths[0] > widths[1]
|
||||
heights = {float(p.get("d").split("V")[1].split(" ")[0]) for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")}
|
||||
assert charts.BAR_HEIGHT <= 24
|
||||
|
||||
|
||||
def test_negative_values_use_the_other_colour_role():
|
||||
svg = charts.bar_chart(["Ertrag", "Kosten"], [1.0, -0.4], "Test")
|
||||
fills = [p.get("fill") for p in parse(svg).findall(".//{http://www.w3.org/2000/svg}path")]
|
||||
assert fills == ["var(--viz-pos)", "var(--viz-neg)"]
|
||||
|
||||
|
||||
def test_waterfall_has_step_per_term_plus_total():
|
||||
svg = charts.waterfall_chart(["Rendite", "Risiko", "Kosten"], [0.1277, -0.0052, -0.0256],
|
||||
"Zielwert", 0.0969, "Zielwert-Zerlegung", " ")
|
||||
root = parse(svg)
|
||||
fills = [p.get("fill") for p in root.findall(".//{http://www.w3.org/2000/svg}path")]
|
||||
assert fills == ["var(--viz-pos)", "var(--viz-neg)", "var(--viz-neg)", "var(--viz-total)"]
|
||||
assert "Zielwert" in [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}text")]
|
||||
|
||||
|
||||
def test_tooltip_per_mark():
|
||||
root = parse(charts.bar_chart(LABELS, VALUES, "Portfolio"))
|
||||
titles = [t.text for t in root.findall(".//{http://www.w3.org/2000/svg}title")]
|
||||
assert len(titles) == len(LABELS) and titles[0].startswith("A02: ")
|
||||
|
||||
|
||||
def make_result(tables):
|
||||
return PipelineResult(
|
||||
task="Aufgabe", specification=make_spec(), review=make_review(), revisions=0,
|
||||
solver_run=SolverRun(status=SolverStatus.OPTIMAL, objective_value=0.09756, tables=tables,
|
||||
headline="Das Portfolio erreicht 9,76 %.", solution={"w[A02]": 0.3}),
|
||||
)
|
||||
|
||||
|
||||
def test_html_report_embeds_chart_and_markdown_does_not():
|
||||
doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)]))
|
||||
page = to_html(doc)
|
||||
assert '<figure class="viz-root">' in page and "<svg" in page
|
||||
assert "<svg" not in to_markdown(doc)
|
||||
|
||||
|
||||
def test_decomposition_table_gets_waterfall():
|
||||
table = ResultTable(title="Zielwert-Zerlegung", columns=["Bestandteil", "Wert"],
|
||||
rows=[["Rendite", 0.1277], ["Risiko", -0.0052], ["Kosten", -0.0256], ["Zielwert", 0.0969]],
|
||||
objective_column="Wert")
|
||||
page = to_html(build_document(make_result([table])))
|
||||
assert page.count("var(--viz-total)") == 1 # Gesamtbalken nur im Wasserfall
|
||||
|
||||
|
||||
def test_result_comes_first_and_headline_is_lead():
|
||||
doc = build_document(make_result([ResultTable.model_validate(PORTFOLIO_TABLE)]))
|
||||
h2 = [b[1] for b in doc if b[0] == "h2"]
|
||||
assert h2[0] == "Ergebnis" and h2.index("Ergebnis") < h2.index("Mathematisches Modell")
|
||||
assert ("lead", "Das Portfolio erreicht 9,76 %.") in doc
|
||||
assert '<p class="lead">' in to_html(doc)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("table", [
|
||||
ResultTable(title="Ohne Zahlen", columns=["A", "B"], rows=[["x", "1,5 %"]], objective_column="B"),
|
||||
ResultTable(title="Nur eine Zeile", columns=["A", "B"], rows=[["x", 1.0]], objective_column="B"),
|
||||
ResultTable(title="Ohne Beitragsspalte", columns=["A", "B"], rows=[["x", 1.0], ["y", 2.0]]),
|
||||
])
|
||||
def test_no_chart_without_usable_numbers(table):
|
||||
assert "<svg" not in to_html(build_document(make_result([table])))
|
||||
|
||||
|
||||
# --- Graphenzeichnung ---
|
||||
|
||||
def test_graph_chart_is_valid_svg_and_marks_the_path():
|
||||
import xml.etree.ElementTree as ET
|
||||
from reporting.charts import graph_chart
|
||||
svg = graph_chart(["0", "1", "2", "3"],
|
||||
[("0", "1", 8), ("0", "2", 18), ("0", "3", 31), ("1", "2", 10), ("1", "3", 21), ("2", "3", 12)],
|
||||
path=["0", "1", "3"], title="Graph")
|
||||
ET.fromstring(svg)
|
||||
assert svg.count("<circle") == 4 # jeder Knoten einmal
|
||||
assert svg.count("marker-end") == 6 # jeder Bogen einmal, und gerichtet
|
||||
assert svg.count("--viz-pos") >= 2 # Weg und Knoten des Weges hervorgehoben
|
||||
|
||||
|
||||
def test_graph_chart_refuses_unknown_nodes_and_huge_graphs():
|
||||
from reporting.charts import graph_chart
|
||||
assert graph_chart(["a"], [("a", "b", 1)]) == "" # Bogen ins Nichts
|
||||
assert graph_chart([str(i) for i in range(20)], [("0", "1", 1)]) == "" # zu groß zum Zeichnen
|
||||
assert graph_chart([], []) == ""
|
||||
|
||||
|
||||
def test_graph_section_explains_before_it_shows():
|
||||
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"],
|
||||
"arcs": [{"source": "0", "target": "1", "weight": 8}, {"source": "1", "target": "3", "weight": 21}],
|
||||
"path": ["0", "1", "3"],
|
||||
"steps": [{"node": "0", "label": 0}, {"node": "1", "label": 8}, {"node": "3", "label": 29}],
|
||||
"node_meaning": "ein Zeitpunkt – das Ende von Jahr i.",
|
||||
"arc_meaning": "Kauf am Ende von Jahr i und Nutzung bis Ende von Jahr j.",
|
||||
}))
|
||||
doc = _graph_section(run)
|
||||
arten = [block[0] for block in doc]
|
||||
assert arten[0] == "p" and "Ende von Jahr" in doc[0][1] # Erklärung steht vor der Zeichnung
|
||||
assert "svg" in arten
|
||||
assert any(b[0] == "table" and "Markierungsverlauf" in str(doc[arten.index("table") - 1]) for b in doc)
|
||||
assert any("8 + 21 = 29" in str(b) for b in doc) # Rechenweg ausgeschrieben
|
||||
Loading…
Reference in a new issue