Ctrl K
ComunidadGuías…

tests/test_analysis_ui_fixes.py

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

Archivo original ↗Preguntar a KiBot
"""Tests for UI behaviour of the decision analysis:
  1. progress lines show node names (not "▶️ Starting: `?` ✅ `?` done")
  2. confirmation gate only when the checkbox is ticked
  3. the report does not start with the complete request as H1
  4. the chat label is not "Decision analysis: decision_analysis"
"""

import tests.conftest  # noqa: F401  (dependency stubs)

from src.pipeline.analysis_pipeline import _report_title
from src.pipeline.dag import NodeResult, NodeStatus
from src.pipeline.models import SubTask, TaskPlan
from src.ui.analysis_runner import (
    format_analysis_event,
    make_chat_summary,
    shorten_for_display,
)
from src.ui.components.plan_preview import should_show_preview

LONG_REQUEST = (
    "Recherchiere ein datenklassengetriebenes KI-Strategiekonzept für ein "
    "großes universitäres Institut mit ca. 1.400 Mitarbeitenden "
    "(200 Professuren, 330 Support-Mitarbeiter, 200 Wissenschaftliche "
    "Mitarbeiter, 700 Doktoranden), das derzeit fragmentiert operiert."
)


# ── 1. Progress events ────────────────────────────────────────

def test_node_start_shows_name_not_questionmark():
    """The DAG sends {"name": ...}; the formatter must accept it, not only {"node": ...}."""
    out = format_analysis_event("node_start", {"name": "layer_1"})
    assert "layer_1" in out and "?" not in out


def test_node_done_accepts_noderesult_object():
    """node_done delivers a NodeResult, not a dict."""
    nr = NodeResult(status=NodeStatus.OK, node_name="layer_1",
                    duration_seconds=2.5,
                    metadata={"done": 4, "executed": 4, "failed": 0})
    out = format_analysis_event("node_done", nr)
    assert "layer_1" in out and "?" not in out
    assert "4/4" in out, "layer metadata should become visible"
    assert "2500 ms" in out


def test_node_failed_shows_error():
    nr = NodeResult(status=NodeStatus.FAILED, node_name="layer_2",
                    error=RuntimeError("Timeout nach 300s"))
    out = format_analysis_event("node_failed", nr)
    assert "layer_2" in out and "Timeout" in out


def test_node_event_without_payload_is_harmless():
    assert format_analysis_event("node_start", None)
    assert format_analysis_event("node_start", {})


def test_explicit_node_key_still_works():
    """Backwards compatible: whoever already sends 'node' is not broken."""
    out = format_analysis_event("node_start", {"node": "eigener_name"})
    assert "eigener_name" in out


# ── 2. Gate only with the checkbox ticked ───────────────────────────

def _decision_plan():
    return TaskPlan(use_case="decision_analysis", tasks=[
        SubTask(id=f"CELL_{i}", phase="cell_evaluation", description="d",
                prompt_template="t") for i in range(4)
    ] + [SubTask(id="REC", phase="recommendation", description="d",
                 prompt_template="t")])


def test_heuristic_alone_would_open_the_gate():
    """Shows why the checkbox is needed: the heuristic applies to every analysis."""
    assert should_show_preview(_decision_plan(), force=False) is True


def test_gate_logic_matches_the_checkbox():
    """Production condition: `show_gate and should_show_preview(plan)`."""
    plan = _decision_plan()
    assert (False and should_show_preview(plan)) is False
    assert (True and should_show_preview(plan)) is True


def test_trivial_plan_stays_closed_even_when_checked():
    """As in the research path: the heuristic filters out trivial plans."""
    trivial = TaskPlan(use_case="x", tasks=[
        SubTask(id="A", phase="p", description="d", prompt_template="t")])
    assert (True and should_show_preview(trivial)) is False


# ── 3. Report title ─────────────────────────────────────────────

def test_title_is_shortened():
    title = _report_title(LONG_REQUEST)
    assert len(title) <= 82
    assert title.endswith("…")
    assert title.startswith("Recherchiere ein datenklassengetriebenes")


def test_title_breaks_at_word_boundary():
    assert not _report_title(LONG_REQUEST).rstrip(" …").endswith("-")
    assert "  " not in _report_title(LONG_REQUEST)


def test_title_uses_only_first_line():
    assert _report_title("Erste Zeile\nZweite Zeile") == "Erste Zeile"


def test_short_title_untouched():
    assert _report_title("Welches CRM-System?") == "Welches CRM-System?"


def test_title_handles_empty():
    assert _report_title("") == "" and _report_title(None) == ""


# ── 4. Chat summary ───────────────────────────────────────

def test_summary_uses_decision_field():
    """decision_analysis must not fall back to the use-case name."""
    out = make_chat_summary("decision_analysis", {
        "decision": LONG_REQUEST, "options": "A\nB", "criteria": "K"})
    assert out != "decision_analysis"
    assert out.startswith("Recherchiere ein")


def test_summary_covers_all_use_case_first_fields():
    cases = {
        "decision_analysis": "decision",
        "research_design": "research_question",
        "grant_proposal": "research_question",
        "peer_review": "manuscript_summary",
        "literature_review": "topic",
        "literature_finder": "topic",
        "explainer": "topic",
    }
    for use_case, field in cases.items():
        out = make_chat_summary(use_case, {field: "Ein sinnvoller Inhalt"})
        assert out == "Ein sinnvoller Inhalt", f"{use_case}/{field}"


def test_summary_falls_back_to_any_text_before_use_case_name():
    out = make_chat_summary("decision_analysis", {"unbekannt": "Irgendein Text"})
    assert out == "Irgendein Text"


def test_summary_last_resort_is_use_case():
    assert make_chat_summary("decision_analysis", {}) == "decision_analysis"


def test_shorten_helper():
    assert shorten_for_display("kurz") == "kurz"
    assert shorten_for_display("a" * 200, 20).endswith("…")
    assert shorten_for_display("") == ""


# ── 5. Plan gate in the research mode ────────────────

def test_research_plan_formatter_accepts_production_arguments():
    """gradio_app calls with search_stats/mode/academic_only.

    The plan gate must accept these keyword arguments (otherwise it fails
    with an unexpected keyword argument as soon as "Confirm plan" is
    ticked).
    """
    from src.institution import InstitutionProfile, set_profile
    from src.pipeline.models import ResearchPlan
    from src.ui.components.plan_preview import format_research_plan_markdown

    set_profile(InstitutionProfile(name="Example University", short_name="EU",
                                   domains=("example.edu",)))
    try:
        out = format_research_plan_markdown(
            ResearchPlan(questions=[]),
            {"langs": ["de", "en"], "n_terms": 12},
            mode="institution",
            academic_only=True,
        )
    finally:
        set_profile(None)
    assert "Research plan" in out
    assert "EU research" in out
    assert "scholarly sources" in out
    assert "de, en" in out


def test_research_plan_formatter_without_context():
    from src.pipeline.models import ResearchPlan
    from src.ui.components.plan_preview import format_research_plan_markdown

    out = format_research_plan_markdown(ResearchPlan(questions=[]))
    assert "Research plan" in out
    assert "Mode" not in out


def test_research_plan_formatter_handles_none():
    from src.ui.components.plan_preview import format_research_plan_markdown
    assert format_research_plan_markdown(None)