update
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user