veris-ai 1.13.0__tar.gz → 1.14.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of veris-ai might be problematic. Click here for more details.

Files changed (44) hide show
  1. {veris_ai-1.13.0 → veris_ai-1.14.1}/PKG-INFO +1 -1
  2. {veris_ai-1.13.0 → veris_ai-1.14.1}/pyproject.toml +1 -1
  3. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/api_client.py +20 -8
  4. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/tool_mock.py +14 -2
  5. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/utils.py +72 -0
  6. veris_ai-1.14.1/tests/test_api_client.py +140 -0
  7. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_side_effects.py +354 -0
  8. {veris_ai-1.13.0 → veris_ai-1.14.1}/uv.lock +1 -1
  9. {veris_ai-1.13.0 → veris_ai-1.14.1}/.cursor/rules/documentation-management.mdc +0 -0
  10. {veris_ai-1.13.0 → veris_ai-1.14.1}/.github/workflows/release.yml +0 -0
  11. {veris_ai-1.13.0 → veris_ai-1.14.1}/.github/workflows/test.yml +0 -0
  12. {veris_ai-1.13.0 → veris_ai-1.14.1}/.gitignore +0 -0
  13. {veris_ai-1.13.0 → veris_ai-1.14.1}/.pre-commit-config.yaml +0 -0
  14. {veris_ai-1.13.0 → veris_ai-1.14.1}/CHANGELOG.md +0 -0
  15. {veris_ai-1.13.0 → veris_ai-1.14.1}/CLAUDE.md +0 -0
  16. {veris_ai-1.13.0 → veris_ai-1.14.1}/LICENSE +0 -0
  17. {veris_ai-1.13.0 → veris_ai-1.14.1}/README.md +0 -0
  18. {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/README.md +0 -0
  19. {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/__init__.py +0 -0
  20. {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/import_options.py +0 -0
  21. {veris_ai-1.13.0 → veris_ai-1.14.1}/examples/openai_agents_example.py +0 -0
  22. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/README.md +0 -0
  23. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/__init__.py +0 -0
  24. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/agents_wrapper.py +0 -0
  25. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/README.md +0 -0
  26. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/__init__.py +0 -0
  27. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/client.py +0 -0
  28. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/jaeger_interface/models.py +0 -0
  29. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/models.py +0 -0
  30. {veris_ai-1.13.0 → veris_ai-1.14.1}/src/veris_ai/observability.py +0 -0
  31. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/README.md +0 -0
  32. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/__init__.py +0 -0
  33. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/conftest.py +0 -0
  34. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/fixtures/__init__.py +0 -0
  35. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/fixtures/http_server.py +0 -0
  36. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/fixtures/simple_app.py +0 -0
  37. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_agents_wrapper_extract.py +0 -0
  38. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_agents_wrapper_simple.py +0 -0
  39. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_helpers.py +0 -0
  40. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_mcp_protocol_server_mocked.py +0 -0
  41. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_token_decoding.py +0 -0
  42. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_tool_mock.py +0 -0
  43. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_utils.py +0 -0
  44. {veris_ai-1.13.0 → veris_ai-1.14.1}/tests/test_veris_runner_tool_options.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: veris-ai
3
- Version: 1.13.0
3
+ Version: 1.14.1
4
4
  Summary: A Python package for Veris AI tools
5
5
  Project-URL: Homepage, https://github.com/veris-ai/veris-python-sdk
6
6
  Project-URL: Bug Tracker, https://github.com/veris-ai/veris-python-sdk/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "veris-ai"
7
- version = "1.13.0"
7
+ version = "1.14.1"
8
8
  description = "A Python package for Veris AI tools"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -20,9 +20,15 @@ class SimulatorAPIClient:
20
20
  changes reflected without recreating the singleton.
21
21
  """
22
22
 
23
- def __init__(self) -> None:
23
+ def __init__(self, timeout: float | None = None, base_url: str | None = None) -> None:
24
24
  """Initialize the API client with static timeout configuration."""
25
- self.timeout = float(os.getenv("VERIS_MOCK_TIMEOUT", "90.0"))
25
+ self._timeout = timeout or float(os.getenv("VERIS_MOCK_TIMEOUT", "300.0"))
26
+ self._base_url = base_url
27
+
28
+ @property
29
+ def base_url(self) -> str:
30
+ """Get the resolved base URL."""
31
+ return self._get_base_url()
26
32
 
27
33
  def _get_base_url(self) -> str:
28
34
  """Resolve the base URL from environment.
@@ -33,7 +39,7 @@ class SimulatorAPIClient:
33
39
  (do not fall back). This supports tests expecting connection
34
40
  failures when an invalid endpoint is provided.
35
41
  """
36
- return os.getenv("VERIS_API_URL") or "https://simulator.api.veris.ai"
42
+ return self._base_url or os.getenv("VERIS_API_URL") or "https://simulator.api.veris.ai"
37
43
 
38
44
  def _build_headers(self) -> dict[str, str] | None:
39
45
  """Build headers including OpenTelemetry tracing and API key."""
@@ -55,7 +61,7 @@ class SimulatorAPIClient:
55
61
  if not endpoint.startswith(("http://", "https://")):
56
62
  raise httpx.ConnectError("Invalid endpoint URL (not absolute): {endpoint}")
57
63
 
58
- with httpx.Client(timeout=self.timeout) as client:
64
+ with httpx.Client(timeout=self._timeout) as client:
59
65
  response = client.post(endpoint, json=payload, headers=headers)
60
66
  response.raise_for_status()
61
67
  return response.json() if response.content else None
@@ -73,7 +79,7 @@ class SimulatorAPIClient:
73
79
  error_msg = f"Invalid endpoint URL (not absolute): {endpoint}"
74
80
  raise httpx.ConnectError(error_msg)
75
81
 
76
- async with httpx.AsyncClient(timeout=self.timeout) as client:
82
+ async with httpx.AsyncClient(timeout=self._timeout) as client:
77
83
  response = await client.post(endpoint, json=payload, headers=headers)
78
84
  response.raise_for_status()
79
85
  return response.json() if response.content else None
@@ -81,15 +87,15 @@ class SimulatorAPIClient:
81
87
  @property
82
88
  def tool_mock_endpoint(self) -> str:
83
89
  """Get the tool mock endpoint URL."""
84
- return urljoin(self._get_base_url(), "v3/tool_mock")
90
+ return urljoin(self.base_url, "v3/tool_mock")
85
91
 
86
92
  def get_log_tool_call_endpoint(self, session_id: str) -> str:
87
93
  """Get the log tool call endpoint URL."""
88
- return urljoin(self._get_base_url(), f"v3/log_tool_call?session_id={session_id}")
94
+ return urljoin(self.base_url, f"v3/log_tool_call?session_id={session_id}")
89
95
 
90
96
  def get_log_tool_response_endpoint(self, session_id: str) -> str:
91
97
  """Get the log tool response endpoint URL."""
92
- return urljoin(self._get_base_url(), f"v3/log_tool_response?session_id={session_id}")
98
+ return urljoin(self.base_url, f"v3/log_tool_response?session_id={session_id}")
93
99
 
94
100
 
95
101
  # Global singleton instance
@@ -99,3 +105,9 @@ _api_client = SimulatorAPIClient()
99
105
  def get_api_client() -> SimulatorAPIClient:
100
106
  """Get the global API client instance."""
101
107
  return _api_client
108
+
109
+
110
+ def set_api_client_params(base_url: str | None = None, timeout: float | None = None) -> None:
111
+ """Set the global API client instance for testing purposes."""
112
+ global _api_client # noqa: PLW0603
113
+ _api_client = SimulatorAPIClient(base_url=base_url, timeout=timeout)
@@ -16,14 +16,16 @@ from typing import (
16
16
 
17
17
 
18
18
  from veris_ai.models import ResponseExpectation, ToolCallOptions
19
- from veris_ai.api_client import get_api_client
19
+ from veris_ai.api_client import get_api_client, set_api_client_params
20
20
  from veris_ai.utils import (
21
21
  convert_to_type,
22
22
  execute_callback,
23
+ execute_combined_callback,
23
24
  extract_json_schema,
24
25
  get_function_parameters,
25
26
  get_input_parameters,
26
27
  launch_callback_task,
28
+ launch_combined_callback_task,
27
29
  )
28
30
 
29
31
  logger = logging.getLogger(__name__)
@@ -90,6 +92,8 @@ class VerisSDK:
90
92
 
91
93
  self._set_session_id(token_data["session_id"])
92
94
  self._set_thread_id(token_data["thread_id"])
95
+ if token_data.get("api_url"):
96
+ set_api_client_params(base_url=token_data["api_url"])
93
97
  logger.info(
94
98
  f"Session ID set to {token_data['session_id']}, "
95
99
  f"Thread ID set to {token_data['thread_id']} - mocking enabled"
@@ -245,13 +249,14 @@ class VerisSDK:
245
249
 
246
250
  return decorator
247
251
 
248
- def mock( # noqa: C901, PLR0915
252
+ def mock( # noqa: C901, PLR0915, PLR0913
249
253
  self,
250
254
  mode: Literal["tool", "function"] = "tool",
251
255
  expects_response: bool | None = None,
252
256
  cache_response: bool | None = None,
253
257
  input_callback: Callable[..., Any] | None = None,
254
258
  output_callback: Callable[[Any], Any] | None = None,
259
+ combined_callback: Callable[..., Any] | None = None,
255
260
  ) -> Callable:
256
261
  """Decorator for mocking tool calls.
257
262
 
@@ -261,6 +266,7 @@ class VerisSDK:
261
266
  cache_response: Whether to cache the response
262
267
  input_callback: Callable that receives input parameters as individual arguments
263
268
  output_callback: Callable that receives the output value
269
+ combined_callback: Callable that receives both input parameters and mock_output
264
270
  """
265
271
  response_expectation = (
266
272
  ResponseExpectation.NONE
@@ -306,6 +312,7 @@ class VerisSDK:
306
312
  input_params = get_input_parameters(func, args, kwargs)
307
313
  launch_callback_task(input_callback, input_params, unpack=True)
308
314
  launch_callback_task(output_callback, result, unpack=False)
315
+ launch_combined_callback_task(combined_callback, input_params, result)
309
316
 
310
317
  return result
311
318
 
@@ -337,6 +344,7 @@ class VerisSDK:
337
344
  input_params = get_input_parameters(func, args, kwargs)
338
345
  execute_callback(input_callback, input_params, unpack=True)
339
346
  execute_callback(output_callback, result, unpack=False)
347
+ execute_combined_callback(combined_callback, input_params, result)
340
348
 
341
349
  return result
342
350
 
@@ -350,6 +358,7 @@ class VerisSDK:
350
358
  return_value: Any, # noqa: ANN401
351
359
  input_callback: Callable[..., Any] | None = None,
352
360
  output_callback: Callable[[Any], Any] | None = None,
361
+ combined_callback: Callable[..., Any] | None = None,
353
362
  ) -> Callable:
354
363
  """Decorator for stubbing tool calls.
355
364
 
@@ -357,6 +366,7 @@ class VerisSDK:
357
366
  return_value: The value to return when the function is stubbed
358
367
  input_callback: Callable that receives input parameters as individual arguments
359
368
  output_callback: Callable that receives the output value
369
+ combined_callback: Callable that receives both input parameters and mock_output
360
370
  """
361
371
 
362
372
  def decorator(func: Callable) -> Callable:
@@ -380,6 +390,7 @@ class VerisSDK:
380
390
  input_params = get_input_parameters(func, args, kwargs)
381
391
  launch_callback_task(input_callback, input_params, unpack=True)
382
392
  launch_callback_task(output_callback, return_value, unpack=False)
393
+ launch_combined_callback_task(combined_callback, input_params, return_value)
383
394
 
384
395
  return return_value
385
396
 
@@ -397,6 +408,7 @@ class VerisSDK:
397
408
  input_params = get_input_parameters(func, args, kwargs)
398
409
  execute_callback(input_callback, input_params, unpack=True)
399
410
  execute_callback(output_callback, return_value, unpack=False)
411
+ execute_combined_callback(combined_callback, input_params, return_value)
400
412
 
401
413
  return return_value
402
414
 
@@ -489,3 +489,75 @@ def launch_callback_task(
489
489
  except RuntimeError:
490
490
  # If no event loop is running, log a warning
491
491
  logger.warning("Cannot launch callback task: no event loop running")
492
+
493
+
494
+ def execute_combined_callback(
495
+ callback: Callable | None,
496
+ input_params: dict[str, Any],
497
+ mock_output: Any, # noqa: ANN401
498
+ ) -> None:
499
+ """Execute a combined callback synchronously with input parameters and mock output.
500
+
501
+ Args:
502
+ callback: The callback callable to execute
503
+ input_params: Dictionary of input parameters
504
+ mock_output: The output from the mock/stub call
505
+
506
+ Note:
507
+ Exceptions in callbacks are caught and logged to prevent breaking the main flow.
508
+ """
509
+ if callback is None:
510
+ return
511
+
512
+ try:
513
+ # Combine input params with mock_output
514
+ combined_data = {**input_params, "mock_output": mock_output}
515
+ # Filter parameters to match callback signature
516
+ filtered_data = filter_callback_parameters(callback, combined_data)
517
+ callback(**filtered_data)
518
+ except Exception as e:
519
+ logger.warning(f"Combined callback execution failed: {e}", exc_info=True)
520
+
521
+
522
+ def launch_combined_callback_task(
523
+ callback: Callable | None,
524
+ input_params: dict[str, Any],
525
+ mock_output: Any, # noqa: ANN401
526
+ ) -> None:
527
+ """Launch a combined callback as a background task (fire-and-forget).
528
+
529
+ Args:
530
+ callback: The callback callable to execute (can be sync or async)
531
+ input_params: Dictionary of input parameters
532
+ mock_output: The output from the mock/stub call
533
+
534
+ Note:
535
+ This launches the callback without blocking. Errors are logged but won't
536
+ affect the main execution flow.
537
+ """
538
+ if callback is None:
539
+ return
540
+
541
+ async def _run_callback() -> None:
542
+ """Wrapper to run combined callback with error handling."""
543
+ try:
544
+ # Combine input params with mock_output
545
+ combined_data = {**input_params, "mock_output": mock_output}
546
+ # Filter parameters to match callback signature
547
+ filtered_data = filter_callback_parameters(callback, combined_data)
548
+
549
+ if inspect.iscoroutinefunction(callback):
550
+ await callback(**filtered_data)
551
+ else:
552
+ result = callback(**filtered_data)
553
+ if inspect.iscoroutine(result):
554
+ await result
555
+ except Exception as e:
556
+ logger.warning(f"Combined callback execution failed: {e}", exc_info=True)
557
+
558
+ # Create task without awaiting (fire-and-forget)
559
+ try:
560
+ asyncio.create_task(_run_callback())
561
+ except RuntimeError:
562
+ # If no event loop is running, log a warning
563
+ logger.warning("Cannot launch combined callback task: no event loop running")
@@ -0,0 +1,140 @@
1
+ """Tests for SimulatorAPIClient endpoint URL generation and configuration."""
2
+
3
+ import os
4
+ from unittest.mock import patch
5
+
6
+ import pytest
7
+
8
+ from veris_ai.api_client import SimulatorAPIClient, get_api_client, set_api_client_params
9
+
10
+
11
+ def test_tool_mock_endpoint_default():
12
+ """Test that tool_mock_endpoint property returns correct URL with default base_url."""
13
+ client = SimulatorAPIClient()
14
+ endpoint = client.tool_mock_endpoint
15
+
16
+ # Should use default base URL
17
+ assert endpoint == "https://simulator.api.veris.ai/v3/tool_mock"
18
+
19
+
20
+ def test_tool_mock_endpoint_custom_base_url():
21
+ """Test that tool_mock_endpoint uses cached base_url from constructor."""
22
+ client = SimulatorAPIClient(base_url="https://custom.api.com")
23
+ endpoint = client.tool_mock_endpoint
24
+
25
+ assert endpoint == "https://custom.api.com/v3/tool_mock"
26
+
27
+
28
+ def test_tool_mock_endpoint_with_env_var():
29
+ """Test that tool_mock_endpoint respects VERIS_API_URL environment variable."""
30
+ with patch.dict(os.environ, {"VERIS_API_URL": "https://test.api.veris.ai"}):
31
+ client = SimulatorAPIClient()
32
+ endpoint = client.tool_mock_endpoint
33
+
34
+ assert endpoint == "https://test.api.veris.ai/v3/tool_mock"
35
+
36
+
37
+ def test_log_tool_call_endpoint():
38
+ """Test get_log_tool_call_endpoint generates correct URL."""
39
+ client = SimulatorAPIClient(base_url="https://test.api.com")
40
+ endpoint = client.get_log_tool_call_endpoint("session-123")
41
+
42
+ assert endpoint == "https://test.api.com/v3/log_tool_call?session_id=session-123"
43
+
44
+
45
+ def test_log_tool_response_endpoint():
46
+ """Test get_log_tool_response_endpoint generates correct URL."""
47
+ client = SimulatorAPIClient(base_url="https://test.api.com")
48
+ endpoint = client.get_log_tool_response_endpoint("session-456")
49
+
50
+ assert endpoint == "https://test.api.com/v3/log_tool_response?session_id=session-456"
51
+
52
+
53
+ def test_base_url_without_protocol_behavior():
54
+ """Test that base_url without protocol is treated as relative path by urljoin."""
55
+ # Note: urljoin treats URLs without protocol as relative paths
56
+ client = SimulatorAPIClient(base_url="api.example.com")
57
+ endpoint = client.tool_mock_endpoint
58
+
59
+ # urljoin treats "api.example.com" as a relative path, so result is just the path component
60
+ assert endpoint == "v3/tool_mock"
61
+
62
+
63
+ def test_base_url_with_trailing_slash():
64
+ """Test that base_url with trailing slash is handled correctly."""
65
+ client = SimulatorAPIClient(base_url="https://test.api.com/")
66
+ endpoint = client.tool_mock_endpoint
67
+
68
+ # urljoin should handle trailing slash correctly
69
+ assert endpoint == "https://test.api.com/v3/tool_mock"
70
+
71
+
72
+ def test_custom_timeout():
73
+ """Test that custom timeout is set correctly."""
74
+ client = SimulatorAPIClient(timeout=30.0)
75
+ assert client._timeout == 30.0
76
+
77
+
78
+ def test_default_timeout():
79
+ """Test that default timeout is used when not specified."""
80
+ with patch.dict(os.environ, {"VERIS_MOCK_TIMEOUT": "120.0"}):
81
+ client = SimulatorAPIClient()
82
+ assert client._timeout == 120.0
83
+
84
+
85
+ def test_set_api_client_params():
86
+ """Test that set_api_client_params reconfigures the global client."""
87
+ # Get original client
88
+ original_client = get_api_client()
89
+ original_endpoint = original_client.tool_mock_endpoint
90
+
91
+ # Reconfigure with custom parameters
92
+ set_api_client_params(base_url="https://custom.test.api", timeout=60.0)
93
+
94
+ # Get new client and verify it's been updated
95
+ new_client = get_api_client()
96
+ assert new_client.tool_mock_endpoint == "https://custom.test.api/v3/tool_mock"
97
+ assert new_client._timeout == 60.0
98
+
99
+ # Restore original client
100
+ set_api_client_params()
101
+
102
+
103
+ def test_set_api_client_params_partial():
104
+ """Test that set_api_client_params can set only base_url or timeout."""
105
+ # Set only base_url
106
+ set_api_client_params(base_url="https://partial.test")
107
+ client = get_api_client()
108
+ assert client.tool_mock_endpoint == "https://partial.test/v3/tool_mock"
109
+ assert client._timeout == 300 # Should use default
110
+
111
+ # Set only timeout
112
+ set_api_client_params(timeout=45.0)
113
+ client = get_api_client()
114
+ assert client._timeout == 45.0
115
+
116
+ # Restore defaults
117
+ set_api_client_params()
118
+
119
+
120
+ def test_multiple_endpoint_calls_use_cached_base_url():
121
+ """Test that multiple endpoint property accesses use the same cached base_url."""
122
+ client = SimulatorAPIClient(base_url="https://cached.api")
123
+
124
+ # Call multiple endpoint methods
125
+ endpoint1 = client.tool_mock_endpoint
126
+ endpoint2 = client.get_log_tool_call_endpoint("session-1")
127
+ endpoint3 = client.get_log_tool_response_endpoint("session-2")
128
+
129
+ # All should use the same base URL
130
+ assert endpoint1.startswith("https://cached.api/")
131
+ assert endpoint2.startswith("https://cached.api/")
132
+ assert endpoint3.startswith("https://cached.api/")
133
+
134
+
135
+ def test_empty_env_var_uses_default():
136
+ """Test that empty VERIS_API_URL falls back to default."""
137
+ with patch.dict(os.environ, {"VERIS_API_URL": ""}, clear=True):
138
+ client = SimulatorAPIClient()
139
+ # Empty string should NOT be used, should fall back to default
140
+ assert client._get_base_url() == "https://simulator.api.veris.ai"
@@ -948,3 +948,357 @@ def test_callback_with_no_matching_parameters(simulation_env):
948
948
  result = some_func(1, "test")
949
949
  assert result == "result"
950
950
  assert len(called) == 1
951
+
952
+
953
+ # Test combined_callback functionality
954
+
955
+
956
+ @pytest.mark.asyncio
957
+ async def test_mock_with_async_combined_callback(simulation_env):
958
+ """Test mock decorator with async combined_callback."""
959
+ captured_results = []
960
+
961
+ async def combined_handler(a: int, b: int, d: int = None, mock_output: int = None):
962
+ """Combined callback that uses both inputs and output."""
963
+ # Compute sum of inputs that are not None, then multiply by mock_output
964
+ total_input = a + b + (d if d is not None else 0)
965
+ result = total_input * (mock_output if mock_output is not None else 1)
966
+ captured_results.append(result)
967
+
968
+ @veris.mock(mode="function", combined_callback=combined_handler)
969
+ async def add(a: int, b: int, c: int = None, d: int = None) -> int:
970
+ """Add numbers together."""
971
+ return a + b + (c if c is not None else 0) + (d if d is not None else 0)
972
+
973
+ mock_response = 6
974
+
975
+ with patch.object(get_api_client(), "post_async", return_value=mock_response):
976
+ result = await add(1, 2, d=4)
977
+ assert result == 6
978
+ # Wait for background tasks to complete
979
+ await asyncio.sleep(0.01)
980
+ assert len(captured_results) == 1
981
+ # (1 + 2 + 4) * 6 = 42
982
+ assert captured_results[0] == 42
983
+
984
+
985
+ @pytest.mark.asyncio
986
+ async def test_mock_with_sync_combined_callback_async_func(simulation_env):
987
+ """Test mock decorator with sync combined_callback on async function."""
988
+ captured_results = []
989
+
990
+ def multiply_by_result(a: int, b: int, mock_output: int = None):
991
+ """Multiply inputs by the mock output."""
992
+ result = (a + b) * (mock_output if mock_output is not None else 1)
993
+ captured_results.append(result)
994
+
995
+ @veris.mock(mode="function", combined_callback=multiply_by_result)
996
+ async def add(a: int, b: int) -> int:
997
+ return a + b
998
+
999
+ mock_response = 10
1000
+
1001
+ with patch.object(get_api_client(), "post_async", return_value=mock_response):
1002
+ result = await add(3, 7)
1003
+ assert result == 10
1004
+ # Wait for background tasks to complete
1005
+ await asyncio.sleep(0.01)
1006
+ assert len(captured_results) == 1
1007
+ # (3 + 7) * 10 = 100
1008
+ assert captured_results[0] == 100
1009
+
1010
+
1011
+ def test_mock_with_sync_combined_callback_sync_func(simulation_env):
1012
+ """Test mock decorator with sync combined_callback on sync function."""
1013
+ captured_results = []
1014
+
1015
+ def process_combined(x: int, y: str, mock_output: dict = None):
1016
+ """Process both input and output."""
1017
+ result = {
1018
+ "input_x": x,
1019
+ "input_y": y,
1020
+ "output": mock_output,
1021
+ "combined": f"{y}:{x}:{mock_output.get('value') if mock_output else 'none'}",
1022
+ }
1023
+ captured_results.append(result)
1024
+
1025
+ @veris.mock(mode="function", combined_callback=process_combined)
1026
+ def test_func(x: int, y: str) -> dict:
1027
+ return {"value": x}
1028
+
1029
+ mock_response = {"value": 999}
1030
+
1031
+ with patch.object(get_api_client(), "post", return_value=mock_response):
1032
+ result = test_func(42, "test")
1033
+ assert result == {"value": 999}
1034
+ assert len(captured_results) == 1
1035
+ assert captured_results[0]["input_x"] == 42
1036
+ assert captured_results[0]["input_y"] == "test"
1037
+ assert captured_results[0]["output"] == {"value": 999}
1038
+ assert captured_results[0]["combined"] == "test:42:999"
1039
+
1040
+
1041
+ @pytest.mark.asyncio
1042
+ async def test_stub_with_async_combined_callback(simulation_env):
1043
+ """Test stub decorator with async combined_callback."""
1044
+ captured_results = []
1045
+
1046
+ async def combined_handler(name: str, count: int, mock_output: str = None):
1047
+ """Combine input and output."""
1048
+ result = f"{name}:{count}:{mock_output}"
1049
+ captured_results.append(result)
1050
+
1051
+ @veris.stub(return_value="stubbed_value", combined_callback=combined_handler)
1052
+ async def process(name: str, count: int) -> str:
1053
+ return f"{name}_{count}"
1054
+
1055
+ result = await process("test", 5)
1056
+ assert result == "stubbed_value"
1057
+ # Wait for background tasks to complete
1058
+ await asyncio.sleep(0.01)
1059
+ assert len(captured_results) == 1
1060
+ assert captured_results[0] == "test:5:stubbed_value"
1061
+
1062
+
1063
+ def test_stub_with_sync_combined_callback_sync_func(simulation_env):
1064
+ """Test stub decorator with sync combined_callback on sync function."""
1065
+ captured_results = []
1066
+
1067
+ def multiply_inputs_by_output(a: int, b: int, mock_output: int = None):
1068
+ """Multiply sum of inputs by output."""
1069
+ result = (a + b) * (mock_output if mock_output is not None else 1)
1070
+ captured_results.append(result)
1071
+
1072
+ @veris.stub(return_value=5, combined_callback=multiply_inputs_by_output)
1073
+ def add(a: int, b: int) -> int:
1074
+ return a + b
1075
+
1076
+ result = add(2, 3)
1077
+ assert result == 5
1078
+ assert len(captured_results) == 1
1079
+ # (2 + 3) * 5 = 25
1080
+ assert captured_results[0] == 25
1081
+
1082
+
1083
+ @pytest.mark.asyncio
1084
+ async def test_combined_callback_with_parameter_filtering(simulation_env):
1085
+ """Test that combined callback only receives parameters it accepts."""
1086
+ captured_results = []
1087
+
1088
+ async def selective_callback(a: int, mock_output: int = None):
1089
+ """Callback that only accepts 'a' and 'mock_output', not 'b' or 'c'."""
1090
+ result = a * (mock_output if mock_output is not None else 1)
1091
+ captured_results.append(result)
1092
+
1093
+ @veris.mock(mode="function", combined_callback=selective_callback)
1094
+ async def complex_func(a: int, b: int, c: str = "default") -> int:
1095
+ return a + b
1096
+
1097
+ mock_response = 7
1098
+
1099
+ with patch.object(get_api_client(), "post_async", return_value=mock_response):
1100
+ result = await complex_func(3, 5, c="ignored")
1101
+ assert result == 7
1102
+ # Wait for background tasks to complete
1103
+ await asyncio.sleep(0.01)
1104
+ assert len(captured_results) == 1
1105
+ # 3 * 7 = 21 (b and c are filtered out)
1106
+ assert captured_results[0] == 21
1107
+
1108
+
1109
+ @pytest.mark.asyncio
1110
+ async def test_combined_callback_in_production_mode(production_env):
1111
+ """Test that combined_callback is not called in production mode."""
1112
+ callback_mock = Mock()
1113
+
1114
+ @veris.mock(mode="function", combined_callback=callback_mock)
1115
+ async def test_func(x: int) -> int:
1116
+ return x * 2
1117
+
1118
+ result = await test_func(21)
1119
+ assert result == 42
1120
+
1121
+ # Combined callback should NOT have been called
1122
+ callback_mock.assert_not_called()
1123
+
1124
+
1125
+ @pytest.mark.asyncio
1126
+ async def test_combined_callback_exception_is_logged(simulation_env):
1127
+ """Test that exceptions in combined callback are caught and logged."""
1128
+
1129
+ async def failing_combined_callback(x: int, mock_output: int = None):
1130
+ raise ValueError("Combined callback error")
1131
+
1132
+ @veris.mock(mode="function", combined_callback=failing_combined_callback)
1133
+ async def test_func(x: int) -> int:
1134
+ return x * 2
1135
+
1136
+ mock_response = 100
1137
+
1138
+ with (
1139
+ patch.object(get_api_client(), "post_async", return_value=mock_response),
1140
+ patch("veris_ai.utils.logger.warning") as mock_logger,
1141
+ ):
1142
+ # Function should still work despite combined callback failure
1143
+ result = await test_func(42)
1144
+ assert result == 100
1145
+
1146
+ # Wait for background tasks to complete
1147
+ await asyncio.sleep(0.01)
1148
+
1149
+ # Error should have been logged
1150
+ mock_logger.assert_called()
1151
+ assert "Combined callback execution failed" in str(mock_logger.call_args)
1152
+
1153
+
1154
+ @pytest.mark.asyncio
1155
+ async def test_mock_with_all_three_callbacks(simulation_env):
1156
+ """Test mock decorator with input, output, and combined callbacks."""
1157
+ captured_inputs = []
1158
+ captured_outputs = []
1159
+ captured_combined = []
1160
+
1161
+ async def capture_input(**kwargs):
1162
+ captured_inputs.append(kwargs)
1163
+
1164
+ async def capture_output(result: int):
1165
+ captured_outputs.append(result)
1166
+
1167
+ async def capture_combined(a: int, b: int, mock_output: int = None):
1168
+ combined = {"sum": a + b, "product": a * b, "mock": mock_output}
1169
+ captured_combined.append(combined)
1170
+
1171
+ @veris.mock(
1172
+ mode="function",
1173
+ input_callback=capture_input,
1174
+ output_callback=capture_output,
1175
+ combined_callback=capture_combined,
1176
+ )
1177
+ async def add(a: int, b: int) -> int:
1178
+ return a + b
1179
+
1180
+ mock_response = 50
1181
+
1182
+ with patch.object(get_api_client(), "post_async", return_value=mock_response):
1183
+ result = await add(10, 20)
1184
+ assert result == 50
1185
+ # Wait for background tasks to complete
1186
+ await asyncio.sleep(0.01)
1187
+
1188
+ # All callbacks should have been called
1189
+ assert len(captured_inputs) == 1
1190
+ assert captured_inputs[0] == {"a": 10, "b": 20}
1191
+
1192
+ assert len(captured_outputs) == 1
1193
+ assert captured_outputs[0] == 50
1194
+
1195
+ assert len(captured_combined) == 1
1196
+ assert captured_combined[0] == {"sum": 30, "product": 200, "mock": 50}
1197
+
1198
+
1199
+ def test_stub_with_all_three_callbacks(simulation_env):
1200
+ """Test stub decorator with input, output, and combined callbacks."""
1201
+ captured_inputs = []
1202
+ captured_outputs = []
1203
+ captured_combined = []
1204
+
1205
+ def capture_input(**kwargs):
1206
+ captured_inputs.append(kwargs)
1207
+
1208
+ def capture_output(result: str):
1209
+ captured_outputs.append(result)
1210
+
1211
+ def capture_combined(name: str, value: int, mock_output: str = None):
1212
+ combined = f"{name}_{value}_{mock_output}"
1213
+ captured_combined.append(combined)
1214
+
1215
+ @veris.stub(
1216
+ return_value="stubbed",
1217
+ input_callback=capture_input,
1218
+ output_callback=capture_output,
1219
+ combined_callback=capture_combined,
1220
+ )
1221
+ def process(name: str, value: int) -> str:
1222
+ return f"{name}:{value}"
1223
+
1224
+ result = process("test", 42)
1225
+ assert result == "stubbed"
1226
+
1227
+ # All callbacks should have been called
1228
+ assert len(captured_inputs) == 1
1229
+ assert captured_inputs[0] == {"name": "test", "value": 42}
1230
+
1231
+ assert len(captured_outputs) == 1
1232
+ assert captured_outputs[0] == "stubbed"
1233
+
1234
+ assert len(captured_combined) == 1
1235
+ assert captured_combined[0] == "test_42_stubbed"
1236
+
1237
+
1238
+ @pytest.mark.asyncio
1239
+ async def test_combined_callback_with_kwargs_accepts_all(simulation_env):
1240
+ """Test combined callback with **kwargs accepts all parameters including mock_output."""
1241
+ captured_data = []
1242
+
1243
+ async def flexible_callback(**kwargs):
1244
+ """Callback that accepts any parameters via **kwargs."""
1245
+ captured_data.append(kwargs)
1246
+
1247
+ @veris.mock(mode="function", combined_callback=flexible_callback)
1248
+ async def multi_param_func(a: int, b: str, c: float = 3.14) -> dict:
1249
+ return {"result": a}
1250
+
1251
+ mock_response = {"result": 999}
1252
+
1253
+ with patch.object(get_api_client(), "post_async", return_value=mock_response):
1254
+ result = await multi_param_func(10, "test", c=2.71)
1255
+ assert result == {"result": 999}
1256
+ # Wait for background tasks to complete
1257
+ await asyncio.sleep(0.01)
1258
+
1259
+ assert len(captured_data) == 1
1260
+ # Should have all input params plus mock_output
1261
+ assert captured_data[0]["a"] == 10
1262
+ assert captured_data[0]["b"] == "test"
1263
+ assert captured_data[0]["c"] == 2.71
1264
+ assert captured_data[0]["mock_output"] == {"result": 999}
1265
+
1266
+
1267
+ def test_combined_callback_user_example(simulation_env):
1268
+ """Test the exact example from the user: add function with multiply_by_result."""
1269
+ multiplication_results = []
1270
+
1271
+ def multiply_by_result(a: int, b: int, d: int = None, mock_output: int = None):
1272
+ """Multiply sum of provided inputs by the mock output."""
1273
+ # Sum the inputs that are not None
1274
+ input_sum = a + b + (d if d is not None else 0)
1275
+ # Multiply by mock output
1276
+ result = input_sum * (mock_output if mock_output is not None else 1)
1277
+ multiplication_results.append(result)
1278
+
1279
+ @veris.mock(mode="function", combined_callback=multiply_by_result)
1280
+ def add(a: int, b: int, c: int = None, d: int = None) -> int:
1281
+ """Add numbers together."""
1282
+ total = a + b
1283
+ if c is not None:
1284
+ total += c
1285
+ if d is not None:
1286
+ total += d
1287
+ return total
1288
+
1289
+ # Mock the API to return 6 as the result
1290
+ mock_response = 6
1291
+
1292
+ with patch.object(get_api_client(), "post", return_value=mock_response):
1293
+ # Call add(1, 2, d=4)
1294
+ # Expected: inputs are 1, 2, 4 (c is None)
1295
+ # Mock returns 6
1296
+ # Combined callback should compute: (1 + 2 + 4) * 6 = 42
1297
+ result = add(1, 2, d=4)
1298
+
1299
+ # The function returns the mocked value
1300
+ assert result == 6
1301
+
1302
+ # The combined callback computed (1+2+4)*6
1303
+ assert len(multiplication_results) == 1
1304
+ assert multiplication_results[0] == 42
@@ -1571,7 +1571,7 @@ wheels = [
1571
1571
 
1572
1572
  [[package]]
1573
1573
  name = "veris-ai"
1574
- version = "1.12.3"
1574
+ version = "1.14.0"
1575
1575
  source = { editable = "." }
1576
1576
  dependencies = [
1577
1577
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes