This commit is contained in:
team3
2026-07-04 12:21:45 +02:00
parent 2f5d5b9ca1
commit 8d8f6c8e51
43 changed files with 1920 additions and 236 deletions

View File

@@ -1012,12 +1012,13 @@ async def test_supplement_material_mode_for_source_topics(board_env, tmp_path, m
# ── Anker-Gate: Quorum-Titel ohne Korpus-Beleg (Reader-Ko-Halluzination) ────────────
async def _anker_env(db, tmp_path, monkeypatch, titel_map):
async def _anker_env(db, tmp_path, monkeypatch, titel_map, desc_map=None):
(tmp_path / "korpus.txt").write_text("Der Graph ist zusammenhängend und endlich.", encoding="utf-8")
monkeypatch.setattr(bi, "source_folder", lambda t: tmp_path)
async def fake_members(topic, cid):
return [{"title": titel_map[cid], "description": "", "readers": ["r1", "r2"], "supplement": False}]
return [{"title": titel_map[cid], "description": (desc_map or {}).get(cid, ""),
"readers": ["r1", "r2"], "supplement": False}]
monkeypatch.setattr(bi, "_member_rows", fake_members)
monkeypatch.setattr(bi, "_rep", lambda rows: rows[0])
@@ -1028,11 +1029,12 @@ async def _anker_env(db, tmp_path, monkeypatch, titel_map):
async def test_anker_gate_rejects_unbelegtes(testdb, tmp_path, monkeypatch):
"""Titel ohne Korpus-Anker → Beleg-Judge; „nein" → rejected/kein-beleg.
"""Titel ohne Korpus-Anker, aber mit Evidenz → Beleg-Judge; „nein" → rejected/kein-beleg.
Titel MIT Anker geht ohne Judge nach naming."""
db = testdb
ctx, cards = await _anker_env(db, tmp_path, monkeypatch,
{"c1": "Graph Zusammenhang", "c2": "Königsberger Brückenproblem"})
{"c1": "Graph Zusammenhang", "c2": "Königsberger Brückenproblem"},
{"c2": "Der Graph ist endlich."}) # Evidenz da → Judge entscheidet
async def fake_slot(ctx2, label, *, key, prompt, role, capabilities, payload, timeout):
assert "Brückenproblem" in prompt and "Zusammenhang" not in prompt # nur der Anker-lose
@@ -1045,11 +1047,27 @@ async def test_anker_gate_rejects_unbelegtes(testdb, tmp_path, monkeypatch):
assert c2["stage"] == "rejected" and c2["payload"]["reason"] == "kein-beleg"
async def test_anker_gate_fail_open(testdb, tmp_path, monkeypatch):
"""Judge-Ausfall → Titel bleibt (2-Reader-Rückhalt)."""
async def test_anker_gate_leeres_pack_hart_nein(testdb, tmp_path, monkeypatch):
"""KEIN distinktives Token im Korpus → deterministisch rejected, Judge läuft NICHT
(der Judge winkte 3 Kanon-Titel auf Schein-Auszügen durch)."""
db = testdb
ctx, cards = await _anker_env(db, tmp_path, monkeypatch, {"c9": "Königsberger Brückenproblem"})
async def never_slot(*a, **kw):
raise AssertionError("Judge darf bei leerem Evidence-Pack nicht laufen")
monkeypatch.setattr(bi, "run_single_slot", never_slot)
await bi._proc_consensus_gate(ctx, _mk_flow(tmp_path), cards)
c9 = await db.kanban_get_card(TOPIC, B, "c9")
assert c9["stage"] == "rejected" and c9["payload"]["reason"] == "kein-beleg"
async def test_anker_gate_fail_open(testdb, tmp_path, monkeypatch):
"""Judge-Ausfall bei VORHANDENER Evidenz → Titel bleibt (2-Reader-Rückhalt)."""
db = testdb
ctx, cards = await _anker_env(db, tmp_path, monkeypatch, {"c9": "Königsberger Brückenproblem"},
{"c9": "Der Graph ist endlich."})
async def broken_slot(*a, **kw):
return "failed", None
@@ -1063,3 +1081,103 @@ def test_hat_anker_ziffern_suffix():
assert bi._hat_anker("ΔTSP1-Algorithmus", ctoks) # tsp1 → tsp
assert not bi._hat_anker("Königsberger Brückenproblem", ctoks)
assert not bi._hat_anker("Algorithmus Verfahren", ctoks) # nur Stopwörter → kein Anker
async def test_reset_subblocks_loescht_globale_dateien(testdb, tmp_path):
"""Reset auf Spalte subblocks: DB-Spiegel UND globale Sidecar-Dateien + ab-*-Resume-Slots
weg — Reste des Vor-Laufs würden sonst in den frischen Lauf zurückmergen."""
db = testdb
await db.kanban_upsert_card(TOPIC, "artefacts", "alpha", "ablock", "artefacts", {"title": "Alpha"})
await db.put_subblock(TOPIC, "alpha", "s1", "Alpha", "S1", status="consensus")
arbeit = tmp_path / "arbeit"
(arbeit / "ab-alpha").mkdir(parents=True)
(arbeit / "ab-alpha" / "facts.json").write_text("{}", encoding="utf-8")
files = {"arbeit": arbeit}
for k in ("sidecar", "facts", "sub_roh", "question_pattern", "artefakte"):
files[k] = tmp_path / f"{k}.json"
files[k].write_text("{}", encoding="utf-8")
moved = await bi.reset_board_from_stage(TOPIC, "artefacts", "subblocks", files)
assert moved == 1
assert not await db.list_subblocks(TOPIC)
assert not (arbeit / "ab-alpha").exists()
assert all(not files[k].exists() for k in ("sidecar", "facts", "sub_roh", "question_pattern", "artefakte"))
# ── Naming-Abstraktion: freier Name nur mit Anker ───────────────────────────────────
def test_naming_schema_varianten():
assert bi._naming_schema({"best": 2}, 3) == (2, None, False)
assert bi._naming_schema({"best": 1, "name": "Kurzer Titel"}, 3) == (1, "Kurzer Titel", False)
assert bi._naming_schema({"ok": True}, 3) == (None, None, True)
assert bi._naming_schema({"best": 9}, 3) is None
assert bi._naming_schema({"best": 1, "name": "x" * 90}, 3) == (1, None, False) # zu lang
def test_name_verankert_thema_subset():
rows = [{"title": "Bubblesort-Schleife und Tauschoperation", "description": "innere Schleife"},
{"title": "Bubblesort Durchläufe", "description": ""}]
assert bi._name_verankert("Bubblesort Tauschoperation", rows, None)
assert not bi._name_verankert("Königsberger Brückenproblem", rows, None) # fremde Begriffe
assert not bi._name_verankert("und der", rows, None) # nur Stopwörter → kein Anker
def test_name_verankert_korpus():
ctoks = {"partition", "problem", "vollständigkeit"}
rows = [{"title": "irrelevant", "description": ""}]
assert bi._name_verankert("Partition-Problem", rows, ctoks)
assert not bi._name_verankert("Rucksackproblem Optimierung", rows, ctoks)
async def test_naming_vergibt_verankerten_namen(testdb, tmp_path, monkeypatch):
"""Judge liefert best+name; verankerter Name gewinnt, unverankerter fällt auf Member zurück."""
db = testdb
antwort = {"val": {"best": 1, "name": "Bubblesort Grundprinzip"}}
async def fake_members(topic, cid):
return [{"norm": "bubblesort - grundprinzip und ablauf (kap. 2)",
"title": "Bubblesort - Grundprinzip und Ablauf (Kap. 2)",
"description": "Sortieren durch Tauschen", "readers": ["r1"], "sources": []},
{"norm": "sortieren durch tauschen", "title": "Sortieren durch Tauschen",
"description": "", "readers": ["r2"], "sources": []}]
async def fake_slot(ctx, label, *, key, prompt, role, capabilities, payload, timeout):
import json as _json
return "ok", payload((0, _json.dumps(antwort["val"]), ""))
monkeypatch.setattr(bi, "_member_rows", fake_members)
monkeypatch.setattr(bi, "run_single_slot", fake_slot)
await db.kanban_upsert_card(TOPIC, B, "c1", "cluster", "naming", {})
ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False)
await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c1", "payload": {}})
card = await db.kanban_get_card(TOPIC, B, "c1")
assert card["payload"]["title"] == "Bubblesort Grundprinzip"
# unverankerter Name → Member-Titel gewinnt
antwort["val"] = {"best": 2, "name": "Vergleichsbasierte Sortierverfahren"}
await db.kanban_upsert_card(TOPIC, B, "c2", "cluster", "naming", {})
await bi._name_one(ctx, _mk_flow(tmp_path), {"card_id": "c2", "payload": {}})
card = await db.kanban_get_card(TOPIC, B, "c2")
assert card["payload"]["title"] == "Sortieren durch Tauschen"
async def test_namecheck_ok_behaelt_titel(testdb, tmp_path, monkeypatch):
"""Check-Judge bestätigt mit ok:true → Titel und Beschreibung bleiben unverändert."""
db = testdb
async def fake_members(topic, cid):
return [{"norm": "a", "title": "A", "description": "da", "readers": ["r1"], "sources": []},
{"norm": "b", "title": "B", "description": "db", "readers": ["r2"], "sources": []}]
async def fake_slot(ctx, label, *, key, prompt, role, capabilities, payload, timeout):
assert "Eigener Titel" in prompt # current_title steht im Prompt
return "ok", payload((0, '{"ok": true}', ""))
monkeypatch.setattr(bi, "_member_rows", fake_members)
monkeypatch.setattr(bi, "run_single_slot", fake_slot)
payload = {"title": "Eigener Titel", "description": "Eigene Beschreibung", "main_norm": "a"}
await db.kanban_upsert_card(TOPIC, B, "c9", "cluster", "naming_check", payload)
ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False)
await bi._namecheck_one(ctx, _mk_flow(tmp_path), {"card_id": "c9", "payload": payload})
block = await db.kanban_get_card(TOPIC, B, "b-c9")
assert block["payload"]["title"] == "Eigener Titel"
assert block["payload"]["description"] == "Eigene Beschreibung"