import pytest from hackathon_advisor.dashboard_chat_contracts import ( CHAT_TOOL_SPECS, chat_tool_schemas, heuristic_chat_call, parse_native_tool_call, resolve_chat_tool_call, strip_function_blocks, ) from hackathon_advisor.tool_contracts import ToolContractError def test_chat_tool_schemas_are_openai_style() -> None: schemas = chat_tool_schemas() assert len(schemas) == len(CHAT_TOOL_SPECS) == 9 for schema in schemas: assert schema["type"] == "function" assert schema["function"]["parameters"]["type"] == "object" def test_parse_native_tool_call_reads_params() -> None: call = parse_native_tool_call( 'voice agents' ) assert call.name == "search_projects" assert call.arguments == {"query": "voice agents"} def test_parse_native_tool_call_handles_cdata_and_surrounding_prose() -> None: call = parse_native_tool_call( 'Let me look that up.\n' " Done." ) assert call.name == "show_cluster" assert call.arguments == {"label": "Voice / Chatbot & ASR"} def test_parse_native_tool_call_without_params() -> None: call = parse_native_tool_call('') assert call.name == "list_quests" assert call.arguments == {} def test_parse_native_tool_call_rejects_garbage() -> None: with pytest.raises(ToolContractError): parse_native_tool_call('unclosed') with pytest.raises(ToolContractError): parse_native_tool_call("just prose, no call") with pytest.raises(ToolContractError): parse_native_tool_call('missing name') def test_resolve_marks_plain_prose_as_none() -> None: resolution = resolve_chat_tool_call("Hello! Ask me about the atlas.", fallback_query="hello") assert resolution.status == "none" assert resolution.call is None assert resolution.errors == () def test_resolve_accepts_valid_call() -> None: resolution = resolve_chat_tool_call( 'Tiny Titan', fallback_query="tiny titan", ) assert resolution.status == "valid" assert resolution.call is not None assert resolution.call.name == "show_quest" def test_resolve_defaults_unknown_tool_through_intent_router() -> None: resolution = resolve_chat_tool_call( '', fallback_query="who completed the most quests?", ) assert resolution.status == "defaulted" assert resolution.call is not None assert resolution.call.name == "top_projects_by_quests" assert resolution.errors def test_resolve_defaults_malformed_xml_to_search() -> None: resolution = resolve_chat_tool_call( 'a < b', fallback_query="projects about voice", ) assert resolution.status == "defaulted" assert resolution.call is not None assert resolution.call.name == "search_projects" assert resolution.call.arguments["query"] == "projects about voice" def test_resolve_rejects_missing_required_argument() -> None: resolution = resolve_chat_tool_call( '', fallback_query="show me the voice cluster", ) assert resolution.status == "defaulted" assert resolution.errors def test_heuristic_chat_call_routes_intents() -> None: assert heuristic_chat_call("who completed the most quests").name == "top_projects_by_quests" assert heuristic_chat_call("what clusters exist").name == "list_clusters" assert heuristic_chat_call("list the quests please").name == "list_quests" assert heuristic_chat_call("what changed recently").name == "recent_activity" assert heuristic_chat_call("what is everyone building").name == "atlas_overview" assert heuristic_chat_call("knitting helpers").name == "search_projects" assert heuristic_chat_call("").name == "atlas_overview" def test_data_intent_call_separates_detail_from_listing() -> None: from hackathon_advisor.dashboard_chat_contracts import data_intent_call detail = data_intent_call("what is in the Dream / Oracle cluster?") assert detail is not None and detail.name == "show_cluster" assert "Dream / Oracle" in detail.arguments["label"] quest_detail = data_intent_call("tell me about the Tiny Titan quest") assert quest_detail is not None and quest_detail.name == "show_quest" assert data_intent_call("what clusters exist").name == "list_clusters" assert data_intent_call("find projects about voice").name == "search_projects" assert data_intent_call("how many voice apps").name == "search_projects" assert data_intent_call("what is the coolest project?").name == "atlas_overview" assert data_intent_call("which project is most liked").name == "atlas_overview" assert data_intent_call("hello there") is None assert data_intent_call("thanks!") is None def test_data_intent_call_routes_project_reading() -> None: from hackathon_advisor.dashboard_chat_contracts import data_intent_call readme = data_intent_call("read the readme of Jawbreaker") assert readme is not None and readme.name == "show_project" how = data_intent_call("how does Jawbreaker work?") assert how is not None and how.name == "show_project" about = data_intent_call("tell me about Jawbreaker") assert about is not None and about.name == "show_project" assert about.arguments["project"] == "tell me about Jawbreaker" # cluster/quest detail intents keep their own tools assert data_intent_call("tell me about the Tiny Titan quest").name == "show_quest" assert data_intent_call("what is in the Dream / Oracle cluster?").name == "show_cluster" def test_smalltalk_intent_only_matches_greetings_and_followups() -> None: from hackathon_advisor.dashboard_chat_contracts import smalltalk_intent assert smalltalk_intent("hello!") assert smalltalk_intent("hey there") assert smalltalk_intent("thanks") assert smalltalk_intent("why") assert smalltalk_intent("are you sure?") assert smalltalk_intent("who are you") assert smalltalk_intent("") assert not smalltalk_intent("how many voice apps") assert not smalltalk_intent("what is the coolest project?") assert not smalltalk_intent("voice agents for elderly care") assert not smalltalk_intent("knitting helpers") def test_strip_function_blocks_removes_stray_calls() -> None: text = 'Here you go. v The map shows it.' assert strip_function_blocks(text) == "Here you go. The map shows it.".strip() assert strip_function_blocks("plain prose") == "plain prose"