tests/test_analysis_ui_fixes.py
Rama main · réplica de lectura del repositorio original.
"""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)