"""Guide-QA: Fehler-Injektion auf Mini-Guide-Karten — deterministisch, ohne LLM.""" import guide_qa as gq import qa def _card(block, md): return {"block": block, "block_norm": block.casefold(), "md": md} AUSF = ("\n\n- m\n\n" "Einstieg in den Block.\n" "\n" "Ein Kantenzug verbindet Knoten über Kanten im Graphen.\n") def test_ausfuehrlich_extrahiert_lerntext(): assert gq._ausfuehrlich(AUSF).startswith("\nEinstieg") assert gq._ausfuehrlich("nur text") == "nur text" def test_marker_fehlend(): cards = [_card("Alpha", AUSF)] rel = {"alpha": {"kantenzug definition", "fehlender aspekt"}} out = gq.marker_fehlend(cards, rel) assert out == ["Alpha · fehlender aspekt"] def test_ziel_ohne_anker(): cards = [_card("Alpha", AUSF)] ziele = [{"block_norm": "alpha", "ziel_id": "z1", "text": "Kantenzug im Graphen erklären"}, {"block_norm": "alpha", "ziel_id": "z2", "text": "Adjazenzmatrix aufstellen können"}] out = gq.ziel_ohne_anker(cards, ziele) assert len(out) == 1 and "z2" in out[0] def test_laengen_ausreisser(): duenn = _card("Alpha", "\nkurz") ok = _card("Beta", "\n" + "x" * 500) out = gq.laengen_ausreisser([duenn, ok], {"alpha": {"s1"}, "beta": {"s1"}}) assert [x["block"] for x in out] == ["Alpha"] def test_redundanz_findet_absatz_doppel(): a = "Der Kantenzug verbindet Knoten über mehrere Kanten und darf Knoten wiederholen. " * 3 b = "Der Kantenzug verbindet Knoten über mehrere Kanten und darf Knoten wiederholen, genau. " * 3 c = "Völlig anderes Thema: Matrizen, Determinanten und lineare Abbildungen im Vektorraum. " * 3 cards = [_card("Alpha", f"\n{a}\n\n{c}"), _card("Beta", f"\n{b}")] out = gq.redundanz(cards) assert len(out) == 1 and out[0]["a"].startswith("Alpha") def test_lesbarkeit_fail_open(monkeypatch): def kaputt(md_by_num): raise RuntimeError("Modell fehlt") monkeypatch.setattr(gq.readability, "rate_sections", kaputt) assert gq.lesbarkeit([_card("Alpha", AUSF)]) == [] def test_note_guide_kalibrierung(): """Gewicht = Punktabzug bei 100 %: 10 % fachlich falsch × 3.0 → 7.0; ungemessen zählt nicht.""" assert qa.note({"fachlich_falsch": 0.1}, gq.NOTE_GEWICHTE_GUIDE) == 7.0 ohne = {"marker_fehlend": 0.0, "ziel_ohne_anker": 0.0} assert qa.note(ohne, gq.NOTE_GEWICHTE_GUIDE) == 10.0 def test_marker_escaping_tolerant(): """Escapte Titel (h\\~2\\~o) sind kein „Marker fehlt" — Writer und DB escapen verschieden.""" md = "\n\nText." cards = [{"block": "Hoch", "block_norm": "hoch", "md": md}] rel = {"hoch": {gq._norm_title("^ und ~ müssen escaped werden (h~2~o)")}} assert gq.marker_fehlend(cards, rel) == [] async def test_fachlich_falsch_braucht_beide_bestaetiger(monkeypatch): """Befund zählt nur, wenn BEIDE Bestätiger zustimmen — Einzel-/Zweifach-Urteile ließen die Note desselben Guides zwischen 2.0 und 6.6 springen.""" import agents calls = {"n": 0} async def fake_agent(key, prompt, timeout, **kw): calls["n"] += 1 if "-fakten-3-" in key: # Bestätiger 2: nur #1 bleibt return 0, '{"relevant": {"1": "ja"}}', "" if "-fakten-2-" in key: # Bestätiger 1: #1 und #2 return 0, '{"relevant": {"1": "ja", "2": "ja"}}', "" return 0, '{"relevant": {"1": "ja", "2": "ja", "3": "nein"}}', "" # Pass 1 monkeypatch.setattr(agents, "run_agent", fake_agent) cards = [_card("Alpha", AUSF), _card("Beta", AUSF), _card("Gamma", AUSF)] out = await gq._fachlich_falsch("t", cards) assert out == ["Alpha"] and calls["n"] == 3 async def test_fachlich_falsch_ohne_verdacht_kein_zweiter_pass(monkeypatch): import agents calls = {"n": 0} async def fake_agent(key, prompt, timeout, **kw): calls["n"] += 1 return 0, '{"relevant": {"1": "nein", "2": "nein"}}', "" monkeypatch.setattr(agents, "run_agent", fake_agent) out = await gq._fachlich_falsch("t", [_card("Alpha", AUSF), _card("Beta", AUSF)]) assert out == [] and calls["n"] == 1