File size: 11,360 Bytes
a5784e9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
"""
Tests for FunctionCallingCache - specifically for tool name extraction and validation.
"""

import time
from unittest.mock import MagicMock

import pytest

from api_utils.utils_ext.function_calling_cache import (
    FunctionCallingCache,
    FunctionCallingCacheEntry,
)


class TestFunctionCallingCacheEntry:
    """Tests for FunctionCallingCacheEntry dataclass."""

    def test_default_values(self):
        """Test default values are correct."""
        entry = FunctionCallingCacheEntry(
            tools_digest="abc123",
            toggle_enabled=True,
            declarations_set=True,
            timestamp=time.time(),
        )
        assert entry.tools_digest == "abc123"
        assert entry.toggle_enabled is True
        assert entry.declarations_set is True
        assert entry.model_name is None
        assert entry.tool_names == set()

    def test_with_tool_names(self):
        """Test entry with tool names."""
        names = {"get_weather", "search_web", "calculate"}
        entry = FunctionCallingCacheEntry(
            tools_digest="xyz789",
            toggle_enabled=True,
            declarations_set=True,
            timestamp=time.time(),
            tool_names=names,
        )
        assert entry.tool_names == names
        assert "get_weather" in entry.tool_names
        assert "search_web" in entry.tool_names


class TestExtractToolNames:
    """Tests for _extract_tool_names method."""

    @pytest.fixture
    def cache(self):
        """Create a cache instance."""
        return FunctionCallingCache(logger=MagicMock())

    def test_extract_from_openai_format(self, cache):
        """Test extracting names from OpenAI tool format (nested function.name)."""
        tools = [
            {
                "type": "function",
                "function": {
                    "name": "get_weather",
                    "description": "Get weather info",
                    "parameters": {"type": "object", "properties": {}},
                },
            },
            {
                "type": "function",
                "function": {
                    "name": "search_web",
                    "description": "Search the web",
                    "parameters": {"type": "object", "properties": {}},
                },
            },
        ]
        names = cache._extract_tool_names(tools)
        assert names == {"get_weather", "search_web"}

    def test_extract_from_flat_format(self, cache):
        """Test extracting names from flat format (name at top level)."""
        tools = [
            {"name": "get_weather", "description": "Get weather"},
            {"name": "calculate", "description": "Calculate math"},
        ]
        names = cache._extract_tool_names(tools)
        assert names == {"get_weather", "calculate"}

    def test_extract_from_mixed_format(self, cache):
        """Test extracting names from mixed formats."""
        tools = [
            {"type": "function", "function": {"name": "openai_tool"}},
            {"name": "flat_tool"},
        ]
        names = cache._extract_tool_names(tools)
        assert names == {"openai_tool", "flat_tool"}

    def test_extract_empty_list(self, cache):
        """Test extracting from empty list."""
        names = cache._extract_tool_names([])
        assert names == set()

    def test_extract_invalid_tools(self, cache):
        """Test extracting from invalid tool definitions."""
        tools = [
            None,
            "not a dict",
            {"no_name_key": "value"},
            {"function": "not a dict"},
            {"function": {"no_name": "value"}},
        ]
        names = cache._extract_tool_names(tools)
        assert names == set()

    def test_extract_with_special_characters(self, cache):
        """Test extracting names with special characters."""
        tools = [
            {"name": "gh_grep_searchGitHub"},
            {"name": "tavily_tavily_search"},
            {"name": "context7_get-library-docs"},
        ]
        names = cache._extract_tool_names(tools)
        assert names == {
            "gh_grep_searchGitHub",
            "tavily_tavily_search",
            "context7_get-library-docs",
        }


class TestValidateFunctionName:
    """Tests for validate_function_name method (fuzzy matching)."""

    @pytest.fixture
    def cache_with_tools(self):
        """Create a cache with registered tools."""
        cache = FunctionCallingCache(logger=MagicMock())
        # Manually set up cache with tool names
        cache._cache = FunctionCallingCacheEntry(
            tools_digest="test123",
            toggle_enabled=True,
            declarations_set=True,
            timestamp=time.time(),
            tool_names={
                "gh_grep_searchGitHub",
                "tavily_tavily_search",
                "context7_get-library-docs",
                "chrome_devtools_click",
                "short",
            },
        )
        return cache

    def test_exact_match(self, cache_with_tools):
        """Test exact name match returns the name unchanged."""
        corrected, was_corrected, confidence = cache_with_tools.validate_function_name(
            "gh_grep_searchGitHub"
        )
        assert corrected == "gh_grep_searchGitHub"
        assert was_corrected is False
        assert confidence == 1.0

    def test_prefix_match_truncated_name(self, cache_with_tools):
        """Test fuzzy matching corrects truncated names."""
        # Simulate truncated name from model hallucination
        corrected, was_corrected, confidence = cache_with_tools.validate_function_name(
            "gh_grep_searchGitH"
        )
        assert corrected == "gh_grep_searchGitHub"
        assert was_corrected is True
        assert 0.7 < confidence < 1.0  # High confidence but not exact

    def test_prefix_match_another_truncated(self, cache_with_tools):
        """Test another truncated name gets corrected."""
        corrected, was_corrected, confidence = cache_with_tools.validate_function_name(
            "tavily_tavily_sear"
        )
        assert corrected == "tavily_tavily_search"
        assert was_corrected is True
        assert 0.7 < confidence < 1.0

    def test_prefix_too_short(self, cache_with_tools):
        """Test that very short prefixes still match (no minimum threshold)."""
        # "gh" matches "gh_grep_searchGitHub" via prefix
        corrected, was_corrected, confidence = cache_with_tools.validate_function_name(
            "gh"
        )
        # It will match but with low confidence
        assert was_corrected is True
        assert confidence < 0.2  # Very low confidence for short prefix

    def test_no_match_invalid_name(self, cache_with_tools):
        """Test that completely invalid names are not matched."""
        corrected, was_corrected, confidence = cache_with_tools.validate_function_name(
            "completely_unknown_function"
        )
        assert was_corrected is False
        assert corrected == "completely_unknown_function"
        assert confidence == 0.0

    def test_empty_cache(self):
        """Test validation with empty cache returns original name."""
        cache = FunctionCallingCache(logger=MagicMock())
        # No cache set
        corrected, was_corrected, confidence = cache.validate_function_name(
            "any_function"
        )
        assert was_corrected is False
        assert corrected == "any_function"
        assert confidence == 0.0

    def test_empty_tool_names(self):
        """Test validation with empty tool_names set."""
        cache = FunctionCallingCache(logger=MagicMock())
        cache._cache = FunctionCallingCacheEntry(
            tools_digest="test",
            toggle_enabled=True,
            declarations_set=True,
            timestamp=time.time(),
            tool_names=set(),
        )
        corrected, was_corrected, confidence = cache.validate_function_name(
            "any_function"
        )
        assert was_corrected is False
        assert corrected == "any_function"
        assert confidence == 0.0

    def test_ambiguous_prefix(self, cache_with_tools):
        """Test that ambiguous prefixes (matching multiple tools) return first match."""
        # Add another tool with similar prefix
        cache_with_tools._cache.tool_names.add("chrome_devtools_screenshot")
        # "chrome_devtools_c" matches "chrome_devtools_click"
        corrected, was_corrected, confidence = cache_with_tools.validate_function_name(
            "chrome_devtools_c"
        )
        assert was_corrected is True
        assert corrected == "chrome_devtools_click"


class TestGetRegisteredToolNames:
    """Tests for get_registered_tool_names method."""

    def test_no_cache(self):
        """Test returns empty set when no cache exists."""
        cache = FunctionCallingCache(logger=MagicMock())
        names = cache.get_registered_tool_names()
        assert names == set()

    def test_with_cache(self):
        """Test returns tool names from cache."""
        cache = FunctionCallingCache(logger=MagicMock())
        cache._cache = FunctionCallingCacheEntry(
            tools_digest="test",
            toggle_enabled=True,
            declarations_set=True,
            timestamp=time.time(),
            tool_names={"func1", "func2", "func3"},
        )
        names = cache.get_registered_tool_names()
        assert names == {"func1", "func2", "func3"}


class TestUpdateCacheWithTools:
    """Tests for update_cache with tools parameter."""

    @pytest.fixture
    def cache(self):
        """Create a cache instance."""
        cache = FunctionCallingCache(logger=MagicMock())
        cache._enabled = True
        return cache

    def test_update_cache_with_openai_tools(self, cache):
        """Test that update_cache extracts and stores tool names."""
        tools = [
            {"type": "function", "function": {"name": "get_weather"}},
            {"type": "function", "function": {"name": "search_web"}},
        ]
        cache.update_cache(
            tools_digest="digest123",
            toggle_enabled=True,
            declarations_set=True,
            tools=tools,
        )
        assert cache._cache is not None
        assert cache._cache.tool_names == {"get_weather", "search_web"}

    def test_update_cache_without_tools(self, cache):
        """Test that update_cache works without tools (empty set)."""
        cache.update_cache(
            tools_digest="digest123",
            toggle_enabled=True,
            declarations_set=True,
        )
        assert cache._cache is not None
        assert cache._cache.tool_names == set()

    def test_update_cache_replaces_previous_tools(self, cache):
        """Test that update_cache replaces previous tool names."""
        # First update
        cache.update_cache(
            tools_digest="digest1",
            toggle_enabled=True,
            declarations_set=True,
            tools=[{"name": "old_tool"}],
        )
        assert cache._cache.tool_names == {"old_tool"}

        # Second update replaces
        cache.update_cache(
            tools_digest="digest2",
            toggle_enabled=True,
            declarations_set=True,
            tools=[{"name": "new_tool"}],
        )
        assert cache._cache.tool_names == {"new_tool"}
        assert "old_tool" not in cache._cache.tool_names