test_clarify_tool.py
1 """Tests for tools/clarify_tool.py - Interactive clarifying questions.""" 2 3 import json 4 from typing import List, Optional 5 6 import pytest 7 8 from tools.clarify_tool import ( 9 clarify_tool, 10 check_clarify_requirements, 11 MAX_CHOICES, 12 CLARIFY_SCHEMA, 13 ) 14 15 16 class TestClarifyToolBasics: 17 """Basic functionality tests for clarify_tool.""" 18 19 def test_simple_question_with_callback(self): 20 """Should return user response for simple question.""" 21 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 22 assert question == "What color?" 23 assert choices is None 24 return "blue" 25 26 result = json.loads(clarify_tool("What color?", callback=mock_callback)) 27 assert result["question"] == "What color?" 28 assert result["choices_offered"] is None 29 assert result["user_response"] == "blue" 30 31 def test_question_with_choices(self): 32 """Should pass choices to callback and return response.""" 33 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 34 assert question == "Pick a number" 35 assert choices == ["1", "2", "3"] 36 return "2" 37 38 result = json.loads(clarify_tool( 39 "Pick a number", 40 choices=["1", "2", "3"], 41 callback=mock_callback 42 )) 43 assert result["question"] == "Pick a number" 44 assert result["choices_offered"] == ["1", "2", "3"] 45 assert result["user_response"] == "2" 46 47 def test_empty_question_returns_error(self): 48 """Should return error for empty question.""" 49 result = json.loads(clarify_tool("", callback=lambda q, c: "ignored")) 50 assert "error" in result 51 assert "required" in result["error"].lower() 52 53 def test_whitespace_only_question_returns_error(self): 54 """Should return error for whitespace-only question.""" 55 result = json.loads(clarify_tool(" \n\t ", callback=lambda q, c: "ignored")) 56 assert "error" in result 57 58 def test_no_callback_returns_error(self): 59 """Should return error when no callback is provided.""" 60 result = json.loads(clarify_tool("What do you want?")) 61 assert "error" in result 62 assert "not available" in result["error"].lower() 63 64 65 class TestClarifyToolChoicesValidation: 66 """Tests for choices parameter validation.""" 67 68 def test_choices_trimmed_to_max(self): 69 """Should trim choices to MAX_CHOICES.""" 70 choices_passed = [] 71 72 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 73 choices_passed.extend(choices or []) 74 return "picked" 75 76 many_choices = ["a", "b", "c", "d", "e", "f", "g"] 77 clarify_tool("Pick one", choices=many_choices, callback=mock_callback) 78 79 assert len(choices_passed) == MAX_CHOICES 80 81 def test_empty_choices_become_none(self): 82 """Empty choices list should become None (open-ended).""" 83 choices_received = ["marker"] 84 85 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 86 choices_received.clear() 87 if choices is not None: 88 choices_received.extend(choices) 89 return "answer" 90 91 clarify_tool("Open question?", choices=[], callback=mock_callback) 92 assert choices_received == [] # Was cleared, nothing added 93 94 def test_choices_with_only_whitespace_stripped(self): 95 """Whitespace-only choices should be stripped out.""" 96 choices_received = [] 97 98 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 99 choices_received.extend(choices or []) 100 return "answer" 101 102 clarify_tool("Pick", choices=["valid", " ", "", "also valid"], callback=mock_callback) 103 assert choices_received == ["valid", "also valid"] 104 105 def test_invalid_choices_type_returns_error(self): 106 """Non-list choices should return error.""" 107 result = json.loads(clarify_tool( 108 "Question?", 109 choices="not a list", # type: ignore 110 callback=lambda q, c: "ignored" 111 )) 112 assert "error" in result 113 assert "list" in result["error"].lower() 114 115 def test_choices_converted_to_strings(self): 116 """Non-string choices should be converted to strings.""" 117 choices_received = [] 118 119 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 120 choices_received.extend(choices or []) 121 return "answer" 122 123 clarify_tool("Pick", choices=[1, 2, 3], callback=mock_callback) # type: ignore 124 assert choices_received == ["1", "2", "3"] 125 126 127 class TestClarifyToolCallbackHandling: 128 """Tests for callback error handling.""" 129 130 def test_callback_exception_returns_error(self): 131 """Should return error if callback raises exception.""" 132 def failing_callback(question: str, choices: Optional[List[str]]) -> str: 133 raise RuntimeError("User cancelled") 134 135 result = json.loads(clarify_tool("Question?", callback=failing_callback)) 136 assert "error" in result 137 assert "Failed to get user input" in result["error"] 138 assert "User cancelled" in result["error"] 139 140 def test_callback_receives_stripped_question(self): 141 """Callback should receive trimmed question.""" 142 received_question = [] 143 144 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 145 received_question.append(question) 146 return "answer" 147 148 clarify_tool(" Question with spaces \n", callback=mock_callback) 149 assert received_question[0] == "Question with spaces" 150 151 def test_user_response_stripped(self): 152 """User response should be stripped of whitespace.""" 153 def mock_callback(question: str, choices: Optional[List[str]]) -> str: 154 return " response with spaces \n" 155 156 result = json.loads(clarify_tool("Q?", callback=mock_callback)) 157 assert result["user_response"] == "response with spaces" 158 159 160 class TestCheckClarifyRequirements: 161 """Tests for the requirements check function.""" 162 163 def test_always_returns_true(self): 164 """clarify tool has no external requirements.""" 165 assert check_clarify_requirements() is True 166 167 168 class TestClarifySchema: 169 """Tests for the OpenAI function-calling schema.""" 170 171 def test_schema_name(self): 172 """Schema should have correct name.""" 173 assert CLARIFY_SCHEMA["name"] == "clarify" 174 175 def test_schema_has_description(self): 176 """Schema should have a description.""" 177 assert "description" in CLARIFY_SCHEMA 178 assert len(CLARIFY_SCHEMA["description"]) > 50 179 180 def test_schema_question_required(self): 181 """Question parameter should be required.""" 182 assert "question" in CLARIFY_SCHEMA["parameters"]["required"] 183 184 def test_schema_choices_optional(self): 185 """Choices parameter should be optional.""" 186 assert "choices" not in CLARIFY_SCHEMA["parameters"]["required"] 187 188 def test_schema_choices_max_items(self): 189 """Schema should specify max items for choices.""" 190 choices_spec = CLARIFY_SCHEMA["parameters"]["properties"]["choices"] 191 assert choices_spec.get("maxItems") == MAX_CHOICES 192 193 def test_max_choices_is_four(self): 194 """MAX_CHOICES constant should be 4.""" 195 assert MAX_CHOICES == 4