|
3 | 3 | import httpx |
4 | 4 | import pytest |
5 | 5 | import respx |
| 6 | +from hypothesis import given, settings |
| 7 | +from hypothesis import strategies as st |
6 | 8 |
|
7 | 9 | from stackone_ai import StackOneTool, Tools |
8 | 10 | from stackone_ai.meta_tools import ( |
|
12 | 14 | ) |
13 | 15 | from stackone_ai.models import ExecuteConfig, ToolParameters |
14 | 16 |
|
| 17 | +# Hypothesis strategies for PBT |
| 18 | +# Score threshold strategy |
| 19 | +score_threshold_strategy = st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False) |
| 20 | + |
| 21 | +# Hybrid alpha strategy (can be outside [0, 1] to test clamping) |
| 22 | +hybrid_alpha_strategy = st.floats(min_value=-10.0, max_value=10.0, allow_nan=False, allow_infinity=False) |
| 23 | + |
| 24 | +# Limit strategy |
| 25 | +limit_strategy = st.integers(min_value=1, max_value=100) |
| 26 | + |
| 27 | + |
| 28 | +def _create_sample_tools() -> list[StackOneTool]: |
| 29 | + """Helper function to create sample tools (for use in PBT tests).""" |
| 30 | + tools = [] |
| 31 | + |
| 32 | + # Create HiBob tools |
| 33 | + for action in ["create", "list", "update", "delete"]: |
| 34 | + for entity in ["employee", "department", "timeoff"]: |
| 35 | + tool_name = f"hibob_{action}_{entity}" |
| 36 | + execute_config = ExecuteConfig( |
| 37 | + name=tool_name, |
| 38 | + method="POST" if action in ["create", "update"] else "GET", |
| 39 | + url=f"https://api.example.com/hibob/{entity}", |
| 40 | + headers={}, |
| 41 | + ) |
| 42 | + |
| 43 | + parameters = ToolParameters( |
| 44 | + type="object", |
| 45 | + properties={ |
| 46 | + "id": {"type": "string", "description": "Entity ID"}, |
| 47 | + "data": {"type": "object", "description": "Entity data"}, |
| 48 | + }, |
| 49 | + ) |
| 50 | + |
| 51 | + tool = StackOneTool( |
| 52 | + description=f"{action.capitalize()} {entity} in HiBob system", |
| 53 | + parameters=parameters, |
| 54 | + _execute_config=execute_config, |
| 55 | + _api_key="test_key", |
| 56 | + ) |
| 57 | + tools.append(tool) |
| 58 | + |
| 59 | + # Create BambooHR tools |
| 60 | + for action in ["create", "list", "search"]: |
| 61 | + for entity in ["candidate", "job", "application"]: |
| 62 | + tool_name = f"bamboohr_{action}_{entity}" |
| 63 | + execute_config = ExecuteConfig( |
| 64 | + name=tool_name, |
| 65 | + method="POST" if action == "create" else "GET", |
| 66 | + url=f"https://api.example.com/bamboohr/{entity}", |
| 67 | + headers={}, |
| 68 | + ) |
| 69 | + |
| 70 | + parameters = ToolParameters( |
| 71 | + type="object", |
| 72 | + properties={ |
| 73 | + "query": {"type": "string", "description": "Search query"}, |
| 74 | + "filters": {"type": "object", "description": "Filter criteria"}, |
| 75 | + }, |
| 76 | + ) |
| 77 | + |
| 78 | + tool = StackOneTool( |
| 79 | + description=f"{action.capitalize()} {entity} in BambooHR system", |
| 80 | + parameters=parameters, |
| 81 | + _execute_config=execute_config, |
| 82 | + _api_key="test_key", |
| 83 | + ) |
| 84 | + tools.append(tool) |
| 85 | + |
| 86 | + return tools |
| 87 | + |
15 | 88 |
|
16 | 89 | @pytest.fixture |
17 | 90 | def sample_tools(): |
@@ -132,6 +205,36 @@ def test_search_limit(self, sample_tools): |
132 | 205 |
|
133 | 206 | assert len(results) <= 3 |
134 | 207 |
|
| 208 | + @given(min_score=score_threshold_strategy, limit=limit_strategy) |
| 209 | + @settings(max_examples=50) |
| 210 | + def test_search_with_min_score_pbt(self, min_score: float, limit: int): |
| 211 | + """PBT: Test that min_score filtering always works correctly.""" |
| 212 | + # Create tools inside test to avoid fixture issues with Hypothesis |
| 213 | + tools = _create_sample_tools() |
| 214 | + index = ToolIndex(tools) |
| 215 | + |
| 216 | + results = index.search("employee", limit=limit, min_score=min_score) |
| 217 | + |
| 218 | + # All results must meet the score threshold |
| 219 | + for r in results: |
| 220 | + assert r.score >= min_score, f"Score {r.score} < min_score {min_score}" |
| 221 | + |
| 222 | + # Result count should not exceed limit |
| 223 | + assert len(results) <= limit |
| 224 | + |
| 225 | + @given(limit=limit_strategy) |
| 226 | + @settings(max_examples=50) |
| 227 | + def test_search_limit_pbt(self, limit: int): |
| 228 | + """PBT: Test that limit is always respected.""" |
| 229 | + # Create tools inside test to avoid fixture issues with Hypothesis |
| 230 | + tools = _create_sample_tools() |
| 231 | + index = ToolIndex(tools) |
| 232 | + |
| 233 | + results = index.search("employee", limit=limit) |
| 234 | + |
| 235 | + assert len(results) <= limit |
| 236 | + assert len(results) <= len(tools) |
| 237 | + |
135 | 238 |
|
136 | 239 | class TestMetaSearchTool: |
137 | 240 | """Test the meta_search_tools functionality""" |
@@ -309,6 +412,25 @@ def test_hybrid_alpha_parameter(self, sample_tools): |
309 | 412 | index_max = ToolIndex(sample_tools, hybrid_alpha=1.5) |
310 | 413 | assert index_max.hybrid_alpha == 1.0 |
311 | 414 |
|
| 415 | + @given(alpha=hybrid_alpha_strategy) |
| 416 | + @settings(max_examples=100) |
| 417 | + def test_hybrid_alpha_clamping_pbt(self, alpha: float): |
| 418 | + """PBT: Test that hybrid_alpha is always clamped to [0.0, 1.0].""" |
| 419 | + # Create tools inside test to avoid fixture issues with Hypothesis |
| 420 | + tools = _create_sample_tools() |
| 421 | + index = ToolIndex(tools, hybrid_alpha=alpha) |
| 422 | + |
| 423 | + # Should always be clamped to [0.0, 1.0] |
| 424 | + assert 0.0 <= index.hybrid_alpha <= 1.0 |
| 425 | + |
| 426 | + # Verify clamping logic |
| 427 | + if alpha < 0.0: |
| 428 | + assert index.hybrid_alpha == 0.0 |
| 429 | + elif alpha > 1.0: |
| 430 | + assert index.hybrid_alpha == 1.0 |
| 431 | + else: |
| 432 | + assert index.hybrid_alpha == alpha |
| 433 | + |
312 | 434 | def test_hybrid_search_returns_results(self, sample_tools): |
313 | 435 | """Test that hybrid search returns meaningful results""" |
314 | 436 | index = ToolIndex(sample_tools, hybrid_alpha=0.2) |
|
0 commit comments