tests/test_research_modes.py
Rama main · réplica de lectura del repositorio original.
"""
Tests for the research mode table (single source of truth).
Guarantees:
- Every mode ID resolves exactly to its route; unknown IDs fall back
to web research.
- Display labels are never used for routing: the UI routes on the
stable ID, so labels can be translated without changing behaviour.
- The institution mode is offered only with an institution profile.
- ANALYSIS_MODE_MAP and ANALYSIS_USE_CASE_ORDER are derived from the
same table and match the use-case registry.
Deliberately Gradio-free: `src.ui.research_modes` has no UI dependency.
"""
import ast
import unittest
from pathlib import Path
from src.institution import InstitutionProfile
from src.ui.research_modes import (
ANALYSIS_MODE_MAP,
ANALYSIS_USE_CASE_ORDER,
DEFAULT_RESEARCH_MODE,
RESEARCH_MODE_ORDER,
RESEARCH_MODES,
mode_choices,
resolve_research_route,
)
class TestResearchModeTable(unittest.TestCase):
def test_ids_unique(self):
self.assertEqual(len(RESEARCH_MODE_ORDER), len(set(RESEARCH_MODE_ORDER)))
def test_default_is_web(self):
self.assertEqual(resolve_research_route(DEFAULT_RESEARCH_MODE), ("web", None))
def test_every_id_resolves_exactly(self):
for mode_id, _label, route in RESEARCH_MODES:
self.assertEqual(resolve_research_route(mode_id), route, mode_id)
def test_labels_do_not_resolve(self):
# A display label must never act as a routing key.
for _mode_id, label, _route in RESEARCH_MODES:
if label.startswith("🌐"):
continue
self.assertEqual(resolve_research_route(label), ("web", None), label)
def test_unknown_and_empty_default_to_web(self):
for bogus in ["", None, "unknown", "Literatur", "legacy"]:
self.assertEqual(resolve_research_route(bogus), ("web", None), bogus)
def test_literature_modes_route_to_distinct_kinds(self):
self.assertEqual(resolve_research_route("literature_check"), ("literature_check", None))
self.assertEqual(resolve_research_route("literature_finder"), ("analysis", "literature_finder"))
self.assertEqual(resolve_research_route("literature_review"), ("analysis", "literature_review"))
class TestModeChoices(unittest.TestCase):
def test_without_profile_no_institution_mode(self):
ids = [v for _, v in mode_choices(InstitutionProfile())]
self.assertNotIn("institution", ids)
self.assertEqual(ids[0], DEFAULT_RESEARCH_MODE)
def test_with_profile_label_uses_short_name(self):
prof = InstitutionProfile(name="Example University", short_name="EU",
domains=("example.edu",))
choices = dict((v, l) for l, v in mode_choices(prof))
self.assertIn("institution", choices)
self.assertIn("EU", choices["institution"])
self.assertNotIn("{institution}", choices["institution"])
def test_choices_are_label_id_pairs(self):
for label, value in mode_choices(InstitutionProfile()):
self.assertIn(value, RESEARCH_MODE_ORDER)
self.assertIsInstance(label, str)
class TestUiRoutesOnIds(unittest.TestCase):
"""The UI must not compare mode values against label text."""
def test_no_label_literals_compared_in_gradio_app(self):
src = Path("src/ui/gradio_app.py").read_text()
labels = {label for _, label, _ in RESEARCH_MODES}
fragments = {"Tiefenerklärung", "Literatur", "Web-Recherche"}
tree = ast.parse(src)
offenders = []
for node in ast.walk(tree):
if isinstance(node, ast.Compare):
for c in [node.left, *node.comparators]:
if isinstance(c, ast.Constant) and isinstance(c.value, str) and (
c.value in labels or c.value in fragments):
offenders.append((node.lineno, c.value))
self.assertEqual(offenders, [], "mode routing must use resolve_research_route()")
class TestDerivedStructures(unittest.TestCase):
def test_analysis_map_keys_are_ids(self):
for mode_id in ANALYSIS_MODE_MAP:
self.assertIn(mode_id, RESEARCH_MODE_ORDER)
def test_analysis_order_matches_table_order(self):
expected = [uc for _, _, (kind, uc) in RESEARCH_MODES if kind == "analysis"]
self.assertEqual(ANALYSIS_USE_CASE_ORDER, expected)
def test_analysis_use_cases_exist_in_registry(self):
from src.pipeline.analysis_pipeline import USE_CASE_REGISTRY
for uc in ANALYSIS_USE_CASE_ORDER:
self.assertIn(uc, USE_CASE_REGISTRY, uc)
if __name__ == "__main__":
unittest.main()
class TestTemplates(unittest.TestCase):
def test_template_ids_are_not_labels(self):
from src.pipeline.models import DEFAULT_TEMPLATE, OUTPUT_TEMPLATES, template_choices
self.assertIn(DEFAULT_TEMPLATE, OUTPUT_TEMPLATES)
for label, tid in template_choices():
self.assertIn(tid, OUTPUT_TEMPLATES)
self.assertNotEqual(label, tid)
self.assertRegex(tid, r"^[a-z_]+$")