"""Tests for tools/clarify_tool.py - Interactive clarifying questions.""" import json from typing import List, Optional import pytest from tools.clarify_tool import ( clarify_tool, check_clarify_requirements, MAX_CHOICES, CLARIFY_SCHEMA, ) class TestClarifyToolBasics: """Basic functionality tests for clarify_tool.""" def test_simple_question_with_callback(self): """Should return user response for simple question.""" def mock_callback(question: str, choices: Optional[List[str]]) -> str: assert question == "What color?" assert choices is None return "blue" result = json.loads(clarify_tool("What color?", callback=mock_callback)) assert result["question"] == "What color?" assert result["choices_offered"] is None assert result["user_response"] == "blue" def test_question_with_choices(self): """Should pass choices to callback and return response.""" def mock_callback(question: str, choices: Optional[List[str]]) -> str: assert question == "Pick a number" assert choices == ["1", "2", "3"] return "2" result = json.loads(clarify_tool( "Pick a number", choices=["1", "2", "3"], callback=mock_callback )) assert result["question"] == "Pick a number" assert result["choices_offered"] == ["1", "2", "3"] assert result["user_response"] == "2" def test_empty_question_returns_error(self): """Should return error for empty question.""" result = json.loads(clarify_tool("", callback=lambda q, c: "ignored")) assert "error" in result assert "required" in result["error"].lower() def test_whitespace_only_question_returns_error(self): """Should return error for whitespace-only question.""" result = json.loads(clarify_tool(" \n\t ", callback=lambda q, c: "ignored")) assert "error" in result def test_no_callback_returns_error(self): """Should return error when no callback is provided.""" result = json.loads(clarify_tool("What do you want?")) assert "error" in result assert "not available" in result["error"].lower() class TestClarifyToolChoicesValidation: """Tests for choices parameter validation.""" def test_choices_trimmed_to_max(self): """Should trim choices to MAX_CHOICES.""" choices_passed = [] def mock_callback(question: str, choices: Optional[List[str]]) -> str: choices_passed.extend(choices or []) return "picked" many_choices = ["a", "b", "c", "d", "e", "f", "g"] clarify_tool("Pick one", choices=many_choices, callback=mock_callback) assert len(choices_passed) == MAX_CHOICES def test_empty_choices_become_none(self): """Empty choices list should become None (open-ended).""" choices_received = ["marker"] def mock_callback(question: str, choices: Optional[List[str]]) -> str: choices_received.clear() if choices is not None: choices_received.extend(choices) return "answer" clarify_tool("Open question?", choices=[], callback=mock_callback) assert choices_received == [] # Was cleared, nothing added def test_choices_with_only_whitespace_stripped(self): """Whitespace-only choices should be stripped out.""" choices_received = [] def mock_callback(question: str, choices: Optional[List[str]]) -> str: choices_received.extend(choices or []) return "answer" clarify_tool("Pick", choices=["valid", " ", "", "also valid"], callback=mock_callback) assert choices_received == ["valid", "also valid"] def test_invalid_choices_type_returns_error(self): """Non-list choices should return error.""" result = json.loads(clarify_tool( "Question?", choices="not a list", # type: ignore callback=lambda q, c: "ignored" )) assert "error" in result assert "list" in result["error"].lower() def test_choices_converted_to_strings(self): """Non-string choices should be converted to strings.""" choices_received = [] def mock_callback(question: str, choices: Optional[List[str]]) -> str: choices_received.extend(choices or []) return "answer" clarify_tool("Pick", choices=[1, 2, 3], callback=mock_callback) # type: ignore assert choices_received == ["1", "2", "3"] class TestClarifyToolCallbackHandling: """Tests for callback error handling.""" def test_callback_exception_returns_error(self): """Should return error if callback raises exception.""" def failing_callback(question: str, choices: Optional[List[str]]) -> str: raise RuntimeError("User cancelled") result = json.loads(clarify_tool("Question?", callback=failing_callback)) assert "error" in result assert "Failed to get user input" in result["error"] assert "User cancelled" in result["error"] def test_callback_receives_stripped_question(self): """Callback should receive trimmed question.""" received_question = [] def mock_callback(question: str, choices: Optional[List[str]]) -> str: received_question.append(question) return "answer" clarify_tool(" Question with spaces \n", callback=mock_callback) assert received_question[0] == "Question with spaces" def test_user_response_stripped(self): """User response should be stripped of whitespace.""" def mock_callback(question: str, choices: Optional[List[str]]) -> str: return " response with spaces \n" result = json.loads(clarify_tool("Q?", callback=mock_callback)) assert result["user_response"] == "response with spaces" class TestCheckClarifyRequirements: """Tests for the requirements check function.""" def test_always_returns_true(self): """clarify tool has no external requirements.""" assert check_clarify_requirements() is True class TestClarifySchema: """Tests for the OpenAI function-calling schema.""" def test_schema_name(self): """Schema should have correct name.""" assert CLARIFY_SCHEMA["name"] == "clarify" def test_schema_has_description(self): """Schema should have a description.""" assert "description" in CLARIFY_SCHEMA assert len(CLARIFY_SCHEMA["description"]) > 50 def test_schema_question_required(self): """Question parameter should be required.""" assert "question" in CLARIFY_SCHEMA["parameters"]["required"] def test_schema_choices_optional(self): """Choices parameter should be optional.""" assert "choices" not in CLARIFY_SCHEMA["parameters"]["required"] def test_schema_choices_max_items(self): """Schema should specify max items for choices.""" choices_spec = CLARIFY_SCHEMA["parameters"]["properties"]["choices"] assert choices_spec.get("maxItems") == MAX_CHOICES def test_max_choices_is_four(self): """MAX_CHOICES constant should be 4.""" assert MAX_CHOICES == 4