Ctrl K
ComunidadGuías…

tests/test_research_modes.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
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_]+$")