Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions tests/tools/test_clarify_tool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""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