update
This commit is contained in:
18
backend/tests/conftest.py
Normal file
18
backend/tests/conftest.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import database # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def testdb(tmp_path, monkeypatch):
|
||||
"""Fresh sqlite file per test; resets the module-global connection."""
|
||||
monkeypatch.setattr(database, "DB_PATH", tmp_path / "test.db")
|
||||
database._db = None
|
||||
await database.init_db()
|
||||
yield database
|
||||
await database.close_db()
|
||||
210
backend/tests/test_board_inventory.py
Normal file
210
backend/tests/test_board_inventory.py
Normal file
@@ -0,0 +1,210 @@
|
||||
"""Board 1 end-to-end through the real engine — agents faked, no LLM, no embedding model."""
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
import board_inventory as bi
|
||||
import kanban
|
||||
from pipeline import GenContext
|
||||
|
||||
TOPIC = "t"
|
||||
B = bi.BOARD
|
||||
|
||||
_PATH_RE = re.compile(r"(/\S+\.json)")
|
||||
|
||||
|
||||
def _fake_single_slot(tmp_path):
|
||||
"""Deterministic judge stand-in: writes the expected JSON to the out_path found in the
|
||||
prompt and returns it — keyed off the agent-key naming convention."""
|
||||
|
||||
async def fake(ctx, label, *, key, prompt, role, capabilities, payload, timeout, on_line=None):
|
||||
m = _PATH_RE.search(prompt)
|
||||
out = None
|
||||
if "-pair-" in key:
|
||||
pairs = prompt.count("\nA: ")
|
||||
out = {"pairs": {str(i + 1): "ja" for i in range(pairs)}}
|
||||
elif "-clarify-" in key:
|
||||
keep = [line[2:].split(" — ")[0] for line in prompt.splitlines()
|
||||
if line.startswith("- ")]
|
||||
out = {"keep": keep, "rest": []}
|
||||
elif "-naming-" in key:
|
||||
out = {"best": 1}
|
||||
elif "-filter-recheck-" in key or "-filter-" in key:
|
||||
out = {"fragments": {}, "drop": []}
|
||||
elif "-gruppierung-completion-" in key:
|
||||
out = {"additions": []}
|
||||
elif "-gruppierung-" in key:
|
||||
out = {"umbrellas": []}
|
||||
elif "-supplement" in key:
|
||||
out = {"blocks": [{"title": "Zeta-Konzept", "description": "kanonisch fehlend"}]}
|
||||
if m and out is not None:
|
||||
with open(m.group(1), "w", encoding="utf-8") as f:
|
||||
json.dump(out, f)
|
||||
return "ok", payload(None)
|
||||
return fake
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def board_env(testdb, tmp_path, monkeypatch):
|
||||
import board_artefacts as ba
|
||||
from textkit import _norm_title
|
||||
|
||||
monkeypatch.setattr(bi, "run_single_slot", _fake_single_slot(tmp_path))
|
||||
|
||||
async def no_emb(flow):
|
||||
return False
|
||||
monkeypatch.setattr(bi, "_emb_ok", no_emb)
|
||||
|
||||
async def fake_subblocks(ctx, set_p, files, entries, instructions, wipe=True, ns=""):
|
||||
title = list(entries.values())[0].split(" — ")[0]
|
||||
return {title: ["Sub Eins", "Sub Zwei"]}
|
||||
|
||||
async def fake_facts(ctx, set_p, files, raw, q, folder, instructions, ns=""):
|
||||
facts = {t: {_norm_title(s): {"key_points": [f"Fakt zu {s}"], "cited_facts": []}
|
||||
for s in subs} for t, subs in raw.items()}
|
||||
return facts, {}
|
||||
|
||||
async def fake_levels(ctx, set_p, files, raw, instructions, ns=""):
|
||||
return {t: [{"title": s, "level": "beginner"} for s in subs] for t, subs in raw.items()}
|
||||
|
||||
async def fake_relevance(ctx, set_p, files, sidecar, instructions, ns=""):
|
||||
return {1: "relevant", 2: "peripheral"}
|
||||
|
||||
async def fake_pattern(ctx, set_p, files, sidecar, instructions, ns=""):
|
||||
return {t: [{"subblock": subs[0]["title"], "question": f"Was ist {t}?"}]
|
||||
for t, subs in sidecar.items()}
|
||||
|
||||
async def fake_artefacts(ctx, set_p, files, sidecar, instructions, ns=""):
|
||||
return {"flashcard": [{"block": t, "subblock": subs[0]["title"], "front": "F", "back": "B"}
|
||||
for t, subs in sidecar.items()], "example": []}
|
||||
|
||||
async def fake_outline(ctx, set_p, files, entries, instructions):
|
||||
return {"chapters": [{"title": "Kapitel 1", "numbers": sorted(entries)}]}
|
||||
|
||||
for name, fn in [("_subblocks_block", fake_subblocks), ("_facts_block", fake_facts),
|
||||
("_levels_block", fake_levels), ("_relevance_block", fake_relevance),
|
||||
("_question_pattern_block", fake_pattern), ("_artefacts_block", fake_artefacts),
|
||||
("_outline_block", fake_outline)]:
|
||||
monkeypatch.setattr(ba, name, fn)
|
||||
|
||||
work = tmp_path / "arbeit"
|
||||
work.mkdir()
|
||||
files = {"arbeit": work, "final": tmp_path / "blocks.md",
|
||||
"sub_roh": tmp_path / "sub_roh.json", "sidecar": tmp_path / "subblocks.json",
|
||||
"facts": tmp_path / "facts.json", "question_pattern": tmp_path / "question_pattern.json",
|
||||
"artefakte": tmp_path / "artefakte.json", "outline": tmp_path / "outline.json",
|
||||
"outline_slots": tmp_path / "outline_slots"}
|
||||
ctx = GenContext(topic=TOPIC, provider="claude", is_cancelled=lambda: False)
|
||||
return testdb, ctx, files
|
||||
|
||||
|
||||
async def _seed(db):
|
||||
# 2 consensus titles (2 readers), 1 single find (kept by panel), 1 artifact (pre-reject)
|
||||
await db.kanban_add_title(TOPIC, B, "alpha", "Alpha", "Grundkonzept", "s1", "r1")
|
||||
await db.kanban_add_title(TOPIC, B, "alpha", "Alpha", "Grundkonzept", "s2", "r2")
|
||||
await db.kanban_add_title(TOPIC, B, "beta", "Beta", "Zweites Konzept", "s1", "r1")
|
||||
await db.kanban_add_title(TOPIC, B, "beta", "Beta", "Zweites Konzept", "s2", "r2")
|
||||
await db.kanban_add_title(TOPIC, B, "gamma", "Gamma", "Einzelfund", "s1", "r1")
|
||||
await db.kanban_add_title(TOPIC, B, "aufgabe 3", "Aufgabe 3", "Übungszettel", "s1", "r1")
|
||||
|
||||
|
||||
async def test_board1_full_flow(board_env):
|
||||
db, ctx, files = board_env
|
||||
await _seed(db)
|
||||
import asyncio
|
||||
ok = await asyncio.wait_for(
|
||||
bi.run_boards(ctx, lambda *a, **k: None, files, {"type": "thema"}, None, "", research=False),
|
||||
timeout=30)
|
||||
assert ok
|
||||
done = await db.kanban_cards(TOPIC, board=B, stage="done_block")
|
||||
titles = sorted(c["payload"]["title"] for c in done)
|
||||
# Alpha/Beta/Gamma survive; the supplement's Zeta flows through the whole board too
|
||||
assert titles == ["Alpha", "Beta", "Gamma", "Zeta-Konzept"]
|
||||
rejected = await db.kanban_cards(TOPIC, board=B, stage="rejected")
|
||||
assert [c["payload"]["title"] for c in rejected] == ["Aufgabe 3"]
|
||||
assert rejected[0]["payload"]["reason"] == "pre-reject"
|
||||
# legacy mirror carries the survivors as consensus
|
||||
legacy = {b["title"] for b in await db.list_blocks(TOPIC, status="consensus")}
|
||||
assert legacy == set(titles)
|
||||
# blocks.md written in flow order
|
||||
lines = files["final"].read_text(encoding="utf-8").strip().splitlines()
|
||||
assert len(lines) == 4
|
||||
# reader union survived the pipeline (consensus evidence on the block card)
|
||||
alpha = next(c for c in done if c["payload"]["title"] == "Alpha")
|
||||
assert set(alpha["payload"]["readers"]) == {"r1", "r2"}
|
||||
# board 2: one artefact card per block ran through to done_artefact (+ outline singleton)
|
||||
art_done = await db.kanban_cards(TOPIC, board="artefacts", stage="done_artefact")
|
||||
assert len(art_done) == 5 # 4 blocks + outline card
|
||||
sidecar = json.loads(files["sidecar"].read_text(encoding="utf-8"))
|
||||
assert set(sidecar) == set(titles)
|
||||
assert sidecar["Alpha"][0]["facts"]["key_points"] == ["Fakt zu Sub Eins"]
|
||||
assert sidecar["Alpha"][0]["relevance"] == "relevant"
|
||||
assert sidecar["Alpha"][1]["relevance"] == "peripheral"
|
||||
# DB mirrors: subblocks, question pattern, artefacts, outline
|
||||
subs = await db.list_subblocks(TOPIC, "alpha")
|
||||
assert {s["sub_title"] for s in subs} == {"Sub Eins", "Sub Zwei"}
|
||||
outline = await db.get_outline(TOPIC)
|
||||
assert outline and "Kapitel 1" in outline
|
||||
|
||||
|
||||
async def test_filter_judges_run_parallel(board_env, monkeypatch):
|
||||
"""40 Blöcke → 2 Filter-Chunks: die Judge-Welle muss parallel laufen (Perf-Fix)."""
|
||||
import asyncio
|
||||
db, ctx, files = board_env
|
||||
state = {"cur": 0, "max": 0}
|
||||
base = bi.run_single_slot # instant fake from the fixture
|
||||
|
||||
async def slow(*a, **k):
|
||||
state["cur"] += 1
|
||||
state["max"] = max(state["max"], state["cur"])
|
||||
try:
|
||||
await asyncio.sleep(0.05)
|
||||
return await base(*a, **k)
|
||||
finally:
|
||||
state["cur"] -= 1
|
||||
|
||||
monkeypatch.setattr(bi, "run_single_slot", slow)
|
||||
for i in range(40):
|
||||
await db.kanban_upsert_card(TOPIC, B, f"b-x{i}", "block", "fragment_filter",
|
||||
{"title": f"Block {i}", "description": "d"})
|
||||
ok = await asyncio.wait_for(
|
||||
bi.run_boards(ctx, lambda *a, **k: None, files, {"type": "thema"}, None, "", research=False),
|
||||
timeout=30)
|
||||
assert ok
|
||||
# 40 seeded + 1 supplement candidate (Zeta) flow through to done_block
|
||||
assert await db.kanban_count(TOPIC, "done_block", board=B) == 41
|
||||
assert state["max"] >= 2 # chunk judges ran as one wave, not sequentially
|
||||
|
||||
|
||||
async def test_empty_subblocks_completes_without_deadletter(board_env, monkeypatch):
|
||||
"""Legitim leere Subbausteine ({} statt None) → Karte läuft bis done_artefact durch."""
|
||||
import asyncio
|
||||
import board_artefacts as ba
|
||||
import blocks as blx
|
||||
db, ctx, files = board_env
|
||||
|
||||
async def empty_subs(ctx, set_p, files, entries, instructions, wipe=True, ns=""):
|
||||
return {}
|
||||
monkeypatch.setattr(ba, "_subblocks_block", empty_subs)
|
||||
await db.kanban_upsert_card(TOPIC, "artefacts", "leer", "ablock", "subblocks",
|
||||
{"title": "Leerer Block", "description": "d"})
|
||||
ok = await asyncio.wait_for(
|
||||
bi.run_boards(ctx, lambda *a, **k: None, files, {"type": "thema"}, None, "", research=False),
|
||||
timeout=30)
|
||||
assert ok
|
||||
card = await db.kanban_get_card(TOPIC, "artefacts", "leer")
|
||||
assert card["stage"] == "done_artefact"
|
||||
assert card["retries"] == 0 and not card.get("last_error")
|
||||
assert TOPIC not in blx._blocks_errors # kein globales Banner
|
||||
|
||||
|
||||
async def test_reader_union_folds_exact_dupes(testdb):
|
||||
db = testdb
|
||||
assert await db.kanban_add_title(TOPIC, B, "x", "X", "d", "s1", "r1") is True
|
||||
assert await db.kanban_add_title(TOPIC, B, "x", "X", "d länger", "s2", "r2") is False
|
||||
card = await db.kanban_get_card(TOPIC, B, "x")
|
||||
assert set(card["payload"]["readers"]) == {"r1", "r2"}
|
||||
assert set(card["payload"]["sources"]) == {"s1", "s2"}
|
||||
assert card["payload"]["description"] == "d länger"
|
||||
68
backend/tests/test_guide_board.py
Normal file
68
backend/tests/test_guide_board.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Guide board: schema parsers + card reset semantics (no LLM)."""
|
||||
|
||||
import guide_board as gb
|
||||
|
||||
TOPIC, FMT = "t", "Guide"
|
||||
|
||||
|
||||
def test_ziele_schema():
|
||||
ok = gb._ziele_schema({"ziele": [{"id": "z1", "text": "Erklären, warum X", "sub": "S"},
|
||||
{"id": "z2", "text": "Nennen von Y"}]})
|
||||
assert [z["id"] for z in ok] == ["z1", "z2"]
|
||||
assert gb._ziele_schema({"ziele": []}) is None
|
||||
assert gb._ziele_schema({"ziele": [{"id": "z1", "text": "a"}, {"id": "z1", "text": "b"}]}) \
|
||||
== [{"id": "z1", "text": "a", "sub": ""}] # duplicate ids fold
|
||||
assert gb._ziele_schema("quatsch") is None
|
||||
|
||||
|
||||
def test_gate_schema():
|
||||
assert gb._gate_schema({"ok": True}) == []
|
||||
claims = gb._gate_schema({"claims": [{"text": "Falsch", "grund": "fehlt"}]})
|
||||
assert claims == [{"text": "Falsch", "grund": "fehlt"}]
|
||||
assert gb._gate_schema({}) is None
|
||||
|
||||
|
||||
def test_coverage_schema():
|
||||
res = gb._coverage_schema({"ziele": {"z1": True, "z2": "false"},
|
||||
"luecken": [{"ziel": "z2", "fehlt": "Beweis"}],
|
||||
"ballast": ["Abschweifung"]}, {"z1", "z2"})
|
||||
assert res["ziele"] == {"z1": True, "z2": False}
|
||||
assert res["luecken"][0]["fehlt"] == "Beweis"
|
||||
assert gb._coverage_schema({"ziele": {"z1": True}}, {"z1", "z2"}) is None # z2 missing
|
||||
|
||||
|
||||
def test_problems_schema():
|
||||
assert gb._problems_schema({"ok": True}) == []
|
||||
assert gb._problems_schema({"problems": [{"section": "S", "problem": "zu lang"}]}) == ["zu lang"]
|
||||
assert gb._problems_schema({"problems": []}) is None
|
||||
|
||||
|
||||
async def test_reset_from_stage(testdb):
|
||||
db = testdb
|
||||
await db.upsert_guide_card(TOPIC, FMT, "a", "A")
|
||||
await db.upsert_guide_card(TOPIC, FMT, "b", "B")
|
||||
await db.set_guide_card(TOPIC, FMT, "a", stage="done", md="text", writer_rounds=2)
|
||||
await db.set_guide_card(TOPIC, FMT, "b", stage="coverage", md="text")
|
||||
await db.put_lernziel(TOPIC, "a", "z1", "Ziel")
|
||||
# reset ab writer (idx 2): beide Karten zurück, md geleert, Ziele bleiben
|
||||
moved = await gb.reset_from_stage(TOPIC, FMT, 2)
|
||||
assert moved == 2
|
||||
cards = {c["block_norm"]: c for c in await db.list_guide_cards(TOPIC, FMT)}
|
||||
assert cards["a"]["stage"] == "writer" and cards["a"]["md"] == "" and cards["a"]["writer_rounds"] == 0
|
||||
assert cards["b"]["stage"] == "writer"
|
||||
assert await db.list_lernziele(TOPIC, "a")
|
||||
# reset ab lernziele (idx 0): Ziele weg
|
||||
await gb.reset_from_stage(TOPIC, FMT, 0)
|
||||
assert not await db.list_lernziele(TOPIC, "a")
|
||||
assert (await db.list_guide_cards(TOPIC, FMT))[0]["stage"] == "lernziele"
|
||||
|
||||
|
||||
async def test_done_step(testdb):
|
||||
db = testdb
|
||||
assert await gb.done_step(TOPIC, FMT) == -1
|
||||
await db.upsert_guide_card(TOPIC, FMT, "a", "A")
|
||||
assert await gb.done_step(TOPIC, FMT) == -1 # alles in lernziele
|
||||
await db.set_guide_card(TOPIC, FMT, "a", stage="coverage")
|
||||
assert await gb.done_step(TOPIC, FMT) == 3 # bis fakten_gate fertig
|
||||
await db.set_guide_card(TOPIC, FMT, "a", stage="done")
|
||||
assert await gb.done_step(TOPIC, FMT) == len(gb.GUIDE_STAGES)
|
||||
128
backend/tests/test_kanban.py
Normal file
128
backend/tests/test_kanban.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Engine tests with fake processors (no LLM): flow, barrier, retry/dead-letter, producer race."""
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
import kanban
|
||||
from kanban import Flow, Stage, chain_stages, run_flow
|
||||
|
||||
TOPIC = "t"
|
||||
BOARD = "inventory"
|
||||
|
||||
|
||||
def _advance_proc(db, to_stage):
|
||||
async def proc(cards):
|
||||
await db.kanban_advance_many(TOPIC, BOARD, [(c["card_id"], to_stage) for c in cards])
|
||||
return proc
|
||||
|
||||
|
||||
async def _seed(db, n, stage="s1"):
|
||||
for i in range(n):
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, f"card-{i}", "title", stage, {"title": f"T{i}"})
|
||||
|
||||
|
||||
async def test_cards_flow_through_stages(testdb):
|
||||
db = testdb
|
||||
await _seed(db, 7)
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([
|
||||
Stage(BOARD, "s1", _advance_proc(db, "s2")),
|
||||
Stage(BOARD, "s2", _advance_proc(db, "done")),
|
||||
])
|
||||
await asyncio.wait_for(run_flow(flow, stages), timeout=10)
|
||||
assert await db.kanban_count(TOPIC, "done", board=BOARD) == 7
|
||||
assert await db.kanban_count(TOPIC, ["s1", "s2"], board=BOARD) == 0
|
||||
|
||||
|
||||
async def test_barrier_waits_for_upstream(testdb):
|
||||
db = testdb
|
||||
await _seed(db, 6)
|
||||
upstream_left: list[int] = []
|
||||
|
||||
async def slow_s1(cards):
|
||||
await asyncio.sleep(0.05) # keep upstream busy so an eager barrier would see queued cards
|
||||
await db.kanban_advance_many(TOPIC, BOARD, [(c["card_id"], "gate") for c in cards])
|
||||
|
||||
async def barrier_proc(cards):
|
||||
upstream_left.append(await db.kanban_count(TOPIC, ["s1"], board=BOARD))
|
||||
await db.kanban_advance_many(TOPIC, BOARD, [(c["card_id"], "done") for c in cards])
|
||||
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([
|
||||
Stage(BOARD, "s1", slow_s1),
|
||||
Stage(BOARD, "gate", barrier_proc, barrier=True),
|
||||
])
|
||||
await asyncio.wait_for(run_flow(flow, stages), timeout=10)
|
||||
assert await db.kanban_count(TOPIC, "done", board=BOARD) == 6
|
||||
assert upstream_left and all(n == 0 for n in upstream_left) # barrier never ran with s1 queued
|
||||
|
||||
|
||||
async def test_retry_backoff_then_dead(testdb, monkeypatch):
|
||||
db = testdb
|
||||
monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.02)
|
||||
await _seed(db, 1)
|
||||
attempts = []
|
||||
|
||||
async def failing(cards):
|
||||
attempts.append(cards[0]["retries"])
|
||||
raise RuntimeError("kaputt")
|
||||
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([Stage(BOARD, "s1", failing)])
|
||||
await asyncio.wait_for(run_flow(flow, stages), timeout=10)
|
||||
card = await db.kanban_get_card(TOPIC, BOARD, "card-0")
|
||||
assert card["stage"] == "dead"
|
||||
assert card["retries"] == kanban.MAX_CARD_RETRIES
|
||||
assert "kaputt" in card["last_error"]
|
||||
assert attempts == [0, 1, 2] # backoff between attempts, then dead-letter
|
||||
|
||||
|
||||
async def test_requeue_dead(testdb):
|
||||
db = testdb
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-0", "title", "s1")
|
||||
for _ in range(kanban.MAX_CARD_RETRIES):
|
||||
await db.kanban_fail_card(TOPIC, BOARD, "card-0", "x", kanban.MAX_CARD_RETRIES, 0.0)
|
||||
assert (await db.kanban_get_card(TOPIC, BOARD, "card-0"))["stage"] == "dead"
|
||||
assert await db.kanban_requeue_dead(TOPIC, BOARD, "s1") == 1
|
||||
card = await db.kanban_get_card(TOPIC, BOARD, "card-0")
|
||||
assert card["stage"] == "s1" and card["retries"] == 0
|
||||
|
||||
|
||||
async def test_producer_attach_in_idle_lull(testdb):
|
||||
"""Fix-6 regression: a producer attached while workers sit in the exit grace poll
|
||||
must keep the flow alive and its cards must still be processed."""
|
||||
db = testdb
|
||||
flow = Flow(TOPIC)
|
||||
stages = chain_stages([Stage(BOARD, "s1", _advance_proc(db, "done"))])
|
||||
|
||||
async def producer_a():
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-a", "title", "s1")
|
||||
flow.wake.set()
|
||||
flow.done_producer()
|
||||
|
||||
async def attacher():
|
||||
while await db.kanban_count(TOPIC, "done", board=BOARD) == 0: # wait for card-a done
|
||||
await asyncio.sleep(0.01)
|
||||
flow.add_producer() # synchronous BEFORE the work — the grace poll must see it
|
||||
|
||||
async def producer_b():
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-b", "title", "s1")
|
||||
flow.wake.set()
|
||||
flow.done_producer()
|
||||
await producer_b()
|
||||
|
||||
flow.add_producer() # producer_a, counted before run_flow (sync add)
|
||||
asyncio.get_event_loop().create_task(attacher())
|
||||
await asyncio.wait_for(run_flow(flow, stages, producers=[producer_a()]), timeout=10)
|
||||
assert await db.kanban_count(TOPIC, "done", board=BOARD) == 2
|
||||
|
||||
|
||||
async def test_backoff_delays_pull(testdb, monkeypatch):
|
||||
db = testdb
|
||||
await db.kanban_upsert_card(TOPIC, BOARD, "card-0", "title", "s1")
|
||||
await db.kanban_fail_card(TOPIC, BOARD, "card-0", "x", 5, 0.2)
|
||||
assert await db.kanban_pull(TOPIC, BOARD, "s1", 10) == [] # in backoff → not pullable
|
||||
assert await db.kanban_count(TOPIC, "s1", board=BOARD) == 1 # but still counts as queued
|
||||
await asyncio.sleep(0.25)
|
||||
assert len(await db.kanban_pull(TOPIC, BOARD, "s1", 10)) == 1
|
||||
41
backend/tests/test_roles.py
Normal file
41
backend/tests/test_roles.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Role routing: resolve_role maps (run_provider, role) → (provider, model) across stacks."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import config
|
||||
from config import PROVIDERS, resolve_role
|
||||
|
||||
|
||||
def test_default_quick_routes_to_minimax(monkeypatch):
|
||||
monkeypatch.setitem(config.ROLE_ROUTING, "quick", "minimax")
|
||||
assert resolve_role("claude", "quick") == ("minimax", PROVIDERS["minimax"]["quick"])
|
||||
|
||||
|
||||
def test_default_judge_routes_to_claude(monkeypatch):
|
||||
monkeypatch.setitem(config.ROLE_ROUTING, "judge", "claude")
|
||||
assert resolve_role("minimax", "judge") == ("claude", PROVIDERS["claude"]["judge"])
|
||||
|
||||
|
||||
def test_empty_routing_keeps_run_provider(monkeypatch):
|
||||
monkeypatch.setitem(config.ROLE_ROUTING, "fast", "")
|
||||
assert resolve_role("claude", "fast") == ("claude", PROVIDERS["claude"]["fast"])
|
||||
assert resolve_role("minimax", "fast") == ("minimax", PROVIDERS["minimax"]["fast"])
|
||||
|
||||
|
||||
def test_explicit_model_syntax(monkeypatch):
|
||||
monkeypatch.setitem(config.ROLE_ROUTING, "guide", "claude:claude-opus-4-8")
|
||||
assert resolve_role("minimax", "guide") == ("claude", "claude-opus-4-8")
|
||||
|
||||
|
||||
def test_unknown_target_falls_back_to_run_provider(monkeypatch):
|
||||
monkeypatch.setitem(config.ROLE_ROUTING, "quick", "gibtsnicht")
|
||||
assert resolve_role("claude", "quick") == ("claude", PROVIDERS["claude"]["quick"])
|
||||
|
||||
|
||||
def test_unknown_role_yields_empty_model():
|
||||
provider, model = resolve_role("claude", "nope")
|
||||
assert provider == "claude"
|
||||
assert model == ""
|
||||
Reference in New Issue
Block a user