| """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 == [] |
|
|
| 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", |
| 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) |
| 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 |
|
|