CoolFace
Apppublic

build-small-hackathon/hackathon-advisor

sourceHugging Facemitupdated 3mo agoView on Hugging Face
16likes
test_dashboard_chat.py488 linesDownload Raw Back to tests
1from hackathon_advisor.dashboard_chat import DashboardChatEngine2from tests.test_dashboard_repository import analyzed_repository, not_analyzed_repository3 4 5class ScriptedRunner:6    """ChatRunner double that replays prepared model outputs and records calls."""7 8    backend = "scripted"9    model_id = "scripted-test-model"10    supports_thinking = False11 12    def __init__(self, outputs: list[object]) -> None:13        self.outputs = list(outputs)14        self.calls: list[dict] = []15 16    def stream(self, messages, *, tools=None, max_new_tokens, enable_thinking=False):17        self.calls.append(18            {19                "messages": messages,20                "tools": tools,21                "max_new_tokens": max_new_tokens,22                "enable_thinking": enable_thinking,23            }24        )25        output = self.outputs.pop(0)26        pieces = output if isinstance(output, list) else [output]27        for count, piece in enumerate(pieces, start=1):28            yield count, piece29 30 31class ThinkingScriptedRunner(ScriptedRunner):32    """ScriptedRunner whose outputs start inside a <think> block (native thinking)."""33 34    supports_thinking = True35 36 37def run_turn(runner, repository, message, history=None):38    engine = DashboardChatEngine(runner, lambda: repository)39    return list(engine.turn_stream(message, history))40 41 42def events_of(events, event_type):43    return [event for event in events if event["type"] == event_type]44 45 46def test_tool_turn_streams_verified_data_before_prose() -> None:47    runner = ScriptedRunner(48        [49            '<function name="top_projects_by_quests"></function>',50            "Project 9 leads with two quests.",51        ]52    )53 54    events = run_turn(runner, analyzed_repository(), "who completed the most quests?")55 56    tool_call = events_of(events, "tool_call")[0]57    assert tool_call["status"] == "valid"58    assert tool_call["name"] == "top_projects_by_quests"59 60    tool_result = events_of(events, "tool_result")[0]61    assert tool_result["data"]["rows"][0]["id"] == "build-small-hackathon/project-9"62    assert tool_result["map_action"]["type"] == "highlight_projects"63 64    types = [event["type"] for event in events]65    assert types.index("tool_result") < types.index("token")66 67    done = events_of(events, "done")[0]68    assert done["response"] == "Project 9 leads with two quests."69    assert done["tool"] == "top_projects_by_quests"70    assert done["history"][-2:] == [71        {"role": "user", "content": "who completed the most quests?"},72        {"role": "assistant", "content": "Project 9 leads with two quests."},73    ]74 75 76def test_model_digest_strips_urls_and_ids() -> None:77    runner = ScriptedRunner(78        [79            '<function name="top_projects_by_quests"></function>',80            "Project 9 leads.",81        ]82    )83 84    run_turn(runner, analyzed_repository(), "quest leaderboard")85 86    answer_call = runner.calls[1]87    assert answer_call["tools"] is None88    digest = answer_call["messages"][-1]["content"]89    assert "url" not in digest90    assert "huggingface.co" not in digest91    assert 'title: "Project 9"' in digest92    assert "quest_count: 2" in digest93 94 95def test_model_digest_trims_long_listings_and_bm25_total() -> None:96    repository = analyzed_repository()97    runner = ScriptedRunner(98        ['<function name="search_projects"><param name="query">project</param></function>', "Found a few."]99    )100    run_turn(runner, repository, "find project planners")101    search_digest = runner.calls[1]["messages"][-1]["content"]102    assert "total:" not in search_digest103 104    runner = ScriptedRunner(['<function name="list_clusters"></function>', "A few clusters."])105    run_turn(runner, repository, "what clusters exist")106    cluster_digest = runner.calls[1]["messages"][-1]["content"]107    # Only the largest cluster is restatable; the full list lives on the UI cards.108    assert cluster_digest.count("label:") == 1109    assert f"cluster_count: {repository.list_clusters()['cluster_count']}" in cluster_digest110 111 112def test_not_analyzed_quests_skip_the_answer_pass() -> None:113    runner = ScriptedRunner(['<function name="top_projects_by_quests"></function>'])114 115    events = run_turn(runner, not_analyzed_repository(), "who completed the most quests?")116 117    skipped = events_of(events, "answer_skipped")[0]118    assert skipped["reason"] == "quests_not_analyzed"119    assert events_of(events, "token") == []120    assert len(runner.calls) == 1  # pass 2 never ran121    assert events_of(events, "done")[0]["response"] == skipped["text"]122 123 124def test_empty_search_skips_the_answer_pass() -> None:125    runner = ScriptedRunner(126        ['<function name="search_projects"><param name="query">zzzz qqqq</param></function>']127    )128 129    events = run_turn(runner, analyzed_repository(), "find zzzz qqqq")130 131    skipped = events_of(events, "answer_skipped")[0]132    assert skipped["reason"] == "no_search_results"133    assert "zzzz qqqq" in skipped["text"]134    assert len(runner.calls) == 1135 136 137def test_unknown_cluster_uses_templated_sentence() -> None:138    runner = ScriptedRunner(139        ['<function name="show_cluster"><param name="label">flying castles</param></function>']140    )141 142    events = run_turn(runner, analyzed_repository(), "show me the flying castles cluster")143 144    skipped = events_of(events, "answer_skipped")[0]145    assert skipped["reason"] == "unknown_cluster"146    assert "flying castles" in skipped["text"]147    assert events_of(events, "tool_result")[0]["map_action"] is None148 149 150def test_show_cluster_emits_filter_map_action() -> None:151    repository = analyzed_repository()152    label = repository.list_clusters()["clusters"][0]["label"]153    runner = ScriptedRunner(154        [155            f'<function name="show_cluster"><param name="label">{label}</param></function>',156            "That cluster groups the local-first planners.",157        ]158    )159 160    events = run_turn(runner, repository, f"what is in {label}?")161 162    map_action = events_of(events, "tool_result")[0]["map_action"]163    assert map_action == {"type": "filter_cluster", "label": label}164 165 166def test_show_cluster_map_action_carries_canonical_label_for_fuzzy_input() -> None:167    """The UI resolves filter_cluster by exact label match, so the engine must168    forward the repository's canonical label, never the user's fuzzy input."""169    repository = analyzed_repository()170    canonical = repository.list_clusters()["clusters"][0]["label"]171    fuzzy = canonical.split()[0].lower()172    assert fuzzy != canonical173    runner = ScriptedRunner(174        [175            f'<function name="show_cluster"><param name="label">{fuzzy}</param></function>',176            "Here is that cluster.",177        ]178    )179 180    events = run_turn(runner, repository, f"what is in the {fuzzy} cluster?")181 182    map_action = events_of(events, "tool_result")[0]["map_action"]183    assert map_action == {"type": "filter_cluster", "label": canonical}184 185 186def test_plain_prose_routes_to_dedicated_smalltalk_pass() -> None:187    runner = ScriptedRunner(188        [189            "Hello! Happy to help.",190            "Hi! Ask me what everyone is building.",191        ]192    )193 194    events = run_turn(runner, analyzed_repository(), "hello there")195 196    assert events_of(events, "tool_call")[0]["status"] == "none"197    assert events_of(events, "tool_result") == []198    done = events_of(events, "done")[0]199    assert done["tool"] == ""200    assert done["response"] == "Hi! Ask me what everyone is building."201    assert runner.calls[1]["tools"] is None202 203 204def test_unmatched_data_question_defaults_to_search_not_smalltalk() -> None:205    """Regression: 'how many voice apps' once slipped into ungrounded small talk."""206    runner = ScriptedRunner(207        [208            "I'm not sure, but I can provide more information if you'd like.",209            "The closest matches are shown on the cards.",210        ]211    )212 213    events = run_turn(runner, analyzed_repository(), "how many voice apps")214 215    tool_call = events_of(events, "tool_call")[0]216    assert tool_call["status"] == "defaulted"217    assert tool_call["name"] == "search_projects"218    assert tool_call["arguments"]["query"] == "how many voice apps"219 220 221def test_short_followup_stays_on_smalltalk_path() -> None:222    runner = ScriptedRunner(223        [224            "Because it scored well.",225            "I should look that up rather than guess - ask me about the quest leaderboard!",226        ]227    )228 229    events = run_turn(runner, analyzed_repository(), "are you sure?")230 231    assert events_of(events, "tool_call")[0]["status"] == "none"232    assert events_of(events, "tool_result") == []233 234 235def test_show_project_streams_readme_and_highlights_the_dot() -> None:236    runner = ScriptedRunner(237        [238            '<function name="show_project"><param name="project">Project 4</param></function>',239            "Project 4 is an offline planner; its app file loads gradio.",240        ]241    )242 243    events = run_turn(runner, analyzed_repository(), "how does Project 4 work?")244 245    tool_result = events_of(events, "tool_result")[0]246    assert tool_result["tool"] == "show_project"247    assert "README evidence for project 4" in tool_result["data"]["readme_excerpt"]248    assert tool_result["map_action"] == {249        "type": "highlight_projects",250        "ids": ["build-small-hackathon/project-4"],251    }252    digest = runner.calls[1]["messages"][-1]["content"]253    assert "README evidence for project 4" in digest254    assert "import gradio" in digest255    assert events_of(events, "done")[0]["tool"] == "show_project"256 257 258def test_show_project_falls_back_to_search_for_unknown_names() -> None:259    runner = ScriptedRunner(260        [261            '<function name="show_project"><param name="project">offline planner thing</param></function>',262            "The closest matches are on the cards.",263        ]264    )265 266    events = run_turn(runner, analyzed_repository(), "tell me about the offline planner thing")267 268    tool_result = events_of(events, "tool_result")[0]269    assert tool_result["tool"] == "search_projects"270    assert tool_result["data"]["results"]271    assert events_of(events, "done")[0]["tool"] == "search_projects"272 273 274def test_prose_answer_to_data_question_is_routed_by_intent() -> None:275    runner = ScriptedRunner(276        [277            "I do not have access to quest completion data.",278            "Project 9 leads with two quests.",279        ]280    )281 282    events = run_turn(runner, analyzed_repository(), "who completed the most quests?")283 284    tool_call = events_of(events, "tool_call")[0]285    assert tool_call["status"] == "defaulted"286    assert tool_call["name"] == "top_projects_by_quests"287    assert events_of(events, "tool_result")[0]["data"]["rows"]288    assert events_of(events, "done")[0]["response"] == "Project 9 leads with two quests."289 290 291def test_malformed_call_degrades_through_intent_router() -> None:292    runner = ScriptedRunner(293        [294            '<function name="nonexistent_tool"></function>',295            "Project 9 leads the quest board.",296        ]297    )298 299    events = run_turn(runner, analyzed_repository(), "who completed the most quests")300 301    tool_call = events_of(events, "tool_call")[0]302    assert tool_call["status"] == "defaulted"303    assert tool_call["name"] == "top_projects_by_quests"304    assert tool_call["errors"]305 306 307def test_stray_function_block_is_cut_from_the_answer() -> None:308    runner = ScriptedRunner(309        [310            '<function name="atlas_overview"></function>',311            ["The field has ten projects. ", '<function name="x">', "</function> extra"],312        ]313    )314 315    events = run_turn(runner, analyzed_repository(), "overview please")316 317    done = events_of(events, "done")[0]318    assert done["response"] == "The field has ten projects."319    for token in events_of(events, "token"):320        assert "<function" not in token["text"]321 322 323def test_thinking_trace_streams_separately_from_the_tool_call() -> None:324    runner = ThinkingScriptedRunner(325        [326            [327                "The user wants the leaderboard. I could emit ",328                '<function name="x"> here but the right tool is top_projects_by_quests.',329                "</th",330                'ink>\n\n<function name="top_projects_by_quests"></function>',331            ],332            ["Let me restate the digest.</think>\n\nProject 9 leads with two quests."],333        ]334    )335 336    events = run_turn(runner, analyzed_repository(), "who completed the most quests?")337 338    thinking_pass1 = [e for e in events if e["type"] == "thinking" and e["pass"] == 1]339    assert "".join(e["text"] for e in thinking_pass1).startswith("The user wants the leaderboard")340    # <function inside the REASONING must not be mistaken for the call itself.341    tool_call = events_of(events, "tool_call")[0]342    assert tool_call["status"] == "valid"343    assert tool_call["name"] == "top_projects_by_quests"344 345    thinking_pass2 = [e for e in events if e["type"] == "thinking" and e["pass"] == 2]346    assert "".join(e["text"] for e in thinking_pass2) == "Let me restate the digest."347    done = events_of(events, "done")[0]348    assert done["response"] == "Project 9 leads with two quests."349    for token in events_of(events, "token"):350        assert "</think>" not in token["text"]351    # Thinking never leaks into the durable history.352    assert all("Let me restate" not in entry["content"] for entry in done["history"])353 354 355def test_thinking_marker_split_across_pieces_is_handled() -> None:356    runner = ThinkingScriptedRunner(357        [358            ["step one ", "step two</t", "hink>", '\n\n<function name="list_quests"></function>'],359            ["ok</think>\n\nThe quests are on the cards."],360        ]361    )362 363    events = run_turn(runner, analyzed_repository(), "list the quests")364 365    thinking = "".join(e["text"] for e in events if e["type"] == "thinking" and e["pass"] == 1)366    assert thinking == "step one step two"367    assert events_of(events, "tool_call")[0]["name"] == "list_quests"368 369 370def test_truncated_thinking_degrades_gracefully() -> None:371    """A 4096-token cut inside <think> leaves no answer text; the turn must still372    resolve (intent backstop on pass 1, templated sentence on pass 2)."""373    runner = ThinkingScriptedRunner(374        [375            ["I am still reasoning about which tool to"],  # no </think>, no call376            ["and the digest says</think>\n\nProject 9 leads."],377        ]378    )379 380    events = run_turn(runner, analyzed_repository(), "who completed the most quests?")381 382    tool_call = events_of(events, "tool_call")[0]383    assert tool_call["status"] == "defaulted"384    assert tool_call["name"] == "top_projects_by_quests"385    assert events_of(events, "done")[0]["response"] == "Project 9 leads."386 387 388def test_chat_generations_use_thinking_and_4096_budget() -> None:389    runner = ScriptedRunner(390        [391            '<function name="atlas_overview"></function>',392            "Ten projects in view.",393        ]394    )395 396    events = run_turn(runner, analyzed_repository(), "overview")397 398    for call in runner.calls:399        assert call["enable_thinking"] is True400        assert call["max_new_tokens"] >= 4096401    for progress in events_of(events, "model_progress"):402        assert progress["max_tokens"] >= 4096403 404 405def test_history_drops_repeated_assistant_answers() -> None:406    """Regression: a greedy 1B echoes any sentence that appears twice in history."""407    runner = ScriptedRunner(408        [409            '<function name="atlas_overview"></function>',410            "Ten projects in view.",411        ]412    )413    looping_history = [414        {"role": "user", "content": "why"},415        {"role": "assistant", "content": "I should look that up."},416        {"role": "user", "content": "are you sure?"},417        {"role": "assistant", "content": "I should look that up."},418        {"role": "user", "content": "really?"},419        {"role": "assistant", "content": "I should look that up."},420    ]421 422    run_turn(runner, analyzed_repository(), "overview", looping_history)423 424    pass1_messages = runner.calls[0]["messages"]425    repeated = [m for m in pass1_messages if m.get("content") == "I should look that up."]426    assert len(repeated) == 1427 428 429def test_grounded_answer_pass_sees_no_history() -> None:430    """Echoes regression: with history in the prompt, a greedy 1B repeats prior431    answers instead of reading the digest. Facts come from the digest alone."""432    runner = ScriptedRunner(433        [434            '<function name="atlas_overview"></function>',435            "Ten projects in view.",436        ]437    )438    long_history = []439    for index in range(6):440        long_history.append({"role": "user", "content": f"question {index}"})441        long_history.append({"role": "assistant", "content": f"answer {index}"})442 443    run_turn(runner, analyzed_repository(), "overview", long_history)444 445    answer_messages = runner.calls[1]["messages"]446    roles = [m["role"] for m in answer_messages]447    assert roles == ["system", "user", "assistant", "tool"]448 449 450def test_overview_digest_leads_with_most_liked_projects() -> None:451    runner = ScriptedRunner(452        [453            '<function name="atlas_overview"></function>',454            "Project 9 is the most liked.",455        ]456    )457 458    run_turn(runner, analyzed_repository(), "what is the coolest project?")459 460    digest = runner.calls[1]["messages"][-1]["content"]461    assert digest.startswith("most_liked_projects:")462    assert "most_completed_quests:" in digest463 464 465def test_history_is_normalized_and_capped() -> None:466    runner = ScriptedRunner(467        [468            '<function name="atlas_overview"></function>',469            "Ten projects in view.",470        ]471    )472    junk_history = [473        {"role": "user", "content": "old question"},474        {"role": "assistant", "content": "old answer"},475        {"role": "tool", "content": "should be dropped"},476        "not even a dict",477        {"role": "user", "content": ""},478    ]479 480    events = run_turn(runner, analyzed_repository(), "overview", junk_history)481 482    pass1_messages = runner.calls[0]["messages"]483    roles = [message["role"] for message in pass1_messages]484    assert roles == ["system", "user", "assistant", "user"]485    done = events_of(events, "done")[0]486    assert all(entry["role"] in ("user", "assistant") for entry in done["history"])487    assert len(done["history"]) <= 12488