vectorwave 0.1.5__tar.gz → 0.1.6__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.
Files changed (69) hide show
  1. {vectorwave-0.1.5/src/vectorwave.egg-info → vectorwave-0.1.6}/PKG-INFO +1 -1
  2. {vectorwave-0.1.5 → vectorwave-0.1.6}/pyproject.toml +1 -1
  3. vectorwave-0.1.6/src/tests/conftest.py +27 -0
  4. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/database/test_db.py +3 -0
  5. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/models/test_db_config.py +36 -1
  6. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/test_tracer.py +144 -68
  7. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/search/test_execution_search.py +12 -7
  8. vectorwave-0.1.6/src/tests/utils/test_function_cahe.py +126 -0
  9. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/__init__.py +2 -1
  10. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/core/decorator.py +39 -22
  11. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/database/db.py +15 -6
  12. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/database/db_search.py +100 -6
  13. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/models/db_config.py +17 -2
  14. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/tracer.py +140 -18
  15. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/search/execution_search.py +19 -38
  16. vectorwave-0.1.6/src/vectorwave/utils/__init__.py +0 -0
  17. vectorwave-0.1.6/src/vectorwave/utils/function_cache.py +73 -0
  18. vectorwave-0.1.6/src/vectorwave/vectorizer/__init__.py +0 -0
  19. {vectorwave-0.1.5 → vectorwave-0.1.6/src/vectorwave.egg-info}/PKG-INFO +1 -1
  20. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/SOURCES.txt +5 -0
  21. {vectorwave-0.1.5 → vectorwave-0.1.6}/LICENSE +0 -0
  22. {vectorwave-0.1.5 → vectorwave-0.1.6}/MANIFEST.in +0 -0
  23. {vectorwave-0.1.5 → vectorwave-0.1.6}/NOTICE +0 -0
  24. {vectorwave-0.1.5 → vectorwave-0.1.6}/Readme.md +0 -0
  25. {vectorwave-0.1.5 → vectorwave-0.1.6}/setup.cfg +0 -0
  26. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/__init__.py +0 -0
  27. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/batch/__init__.py +0 -0
  28. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/batch/test_batch.py +0 -0
  29. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/core/__init__.py +0 -0
  30. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/core/test_decorator.py +0 -0
  31. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/database/__init__.py +0 -0
  32. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/database/test_db_search.py +0 -0
  33. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/exception/__init__.py +0 -0
  34. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/models/__init__.py +0 -0
  35. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/__init__.py +0 -0
  36. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/alert/__init__.py +0 -0
  37. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/alert/test_alerter.py +0 -0
  38. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/monitoring/test_async_trace.py +0 -0
  39. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/prediction/__init__.py +0 -0
  40. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/tests/search/__init__.py +0 -0
  41. {vectorwave-0.1.5/src/tests/vectorizer → vectorwave-0.1.6/src/tests/utils}/__init__.py +0 -0
  42. {vectorwave-0.1.5/src/vectorwave/batch → vectorwave-0.1.6/src/tests/vectorizer}/__init__.py +0 -0
  43. {vectorwave-0.1.5/src/vectorwave/core → vectorwave-0.1.6/src/vectorwave/batch}/__init__.py +0 -0
  44. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/batch/batch.py +0 -0
  45. {vectorwave-0.1.5/src/vectorwave/database → vectorwave-0.1.6/src/vectorwave/core}/__init__.py +0 -0
  46. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/core/core.py +0 -0
  47. {vectorwave-0.1.5/src/vectorwave/exception → vectorwave-0.1.6/src/vectorwave/database}/__init__.py +0 -0
  48. {vectorwave-0.1.5/src/vectorwave/models → vectorwave-0.1.6/src/vectorwave/exception}/__init__.py +0 -0
  49. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/exception/exceptions.py +0 -0
  50. {vectorwave-0.1.5/src/vectorwave/monitoring → vectorwave-0.1.6/src/vectorwave/models}/__init__.py +0 -0
  51. {vectorwave-0.1.5/src/vectorwave/monitoring/alert → vectorwave-0.1.6/src/vectorwave/monitoring}/__init__.py +0 -0
  52. {vectorwave-0.1.5/src/vectorwave/prediction → vectorwave-0.1.6/src/vectorwave/monitoring/alert}/__init__.py +0 -0
  53. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/base.py +0 -0
  54. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/factory.py +0 -0
  55. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/null_alerter.py +0 -0
  56. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/alert/webhook_alerter.py +0 -0
  57. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/monitoring/monitoring.py +0 -0
  58. {vectorwave-0.1.5/src/vectorwave/search → vectorwave-0.1.6/src/vectorwave/prediction}/__init__.py +0 -0
  59. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/prediction/predictor.py +0 -0
  60. {vectorwave-0.1.5/src/vectorwave/vectorizer → vectorwave-0.1.6/src/vectorwave/search}/__init__.py +0 -0
  61. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/search/extended_search.py +0 -0
  62. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/search/rag_search.py +0 -0
  63. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/base.py +0 -0
  64. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/factory.py +0 -0
  65. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/huggingface_vectorizer.py +0 -0
  66. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave/vectorizer/openai_vectorizer.py +0 -0
  67. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/dependency_links.txt +0 -0
  68. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/requires.txt +0 -0
  69. {vectorwave-0.1.5 → vectorwave-0.1.6}/src/vectorwave.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vectorwave
3
- Version: 0.1.5
3
+ Version: 0.1.6
4
4
  Summary: VectorWave: Seamless Auto-Vectorization Framework
5
5
  Author-email: junyeonggim <junyeonggim5@gmail.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vectorwave"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  authors = [
9
9
  { name = "junyeonggim", email = "junyeonggim5@gmail.com" },
10
10
  ]
@@ -0,0 +1,27 @@
1
+ import pytest
2
+ import os
3
+ import logging
4
+
5
+ CACHE_FILE_PATH = ".vectorwave_functions_cache.json"
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def _delete_cache():
10
+ """Helper function to remove the cache file if it exists."""
11
+ if os.path.exists(CACHE_FILE_PATH):
12
+ try:
13
+ os.remove(CACHE_FILE_PATH)
14
+ except OSError as e:
15
+ print(f"\n[CacheFixture Error] Failed to remove {CACHE_FILE_PATH}: {e}")
16
+
17
+
18
+ @pytest.fixture(autouse=True, scope="function")
19
+ def atomic_function_cache():
20
+ # --- SETUP (Before Test) ---
21
+ _delete_cache()
22
+
23
+ # --- Run the test ---
24
+ yield
25
+
26
+ # --- TEARDOWN (After Test) ---
27
+ _delete_cache()
@@ -355,6 +355,9 @@ def test_create_execution_schema_new(mock_vectorizer_none, test_settings):
355
355
  assert "status" in passed_props_map
356
356
  assert "duration_ms" in passed_props_map
357
357
 
358
+ assert "return_value" in passed_props_map
359
+ assert passed_props_map["return_value"].dataType == wvc.DataType.TEXT
360
+
358
361
  assert collection == mock_new_collection
359
362
 
360
363
 
@@ -148,4 +148,39 @@ def test_get_settings_loads_global_custom_values(mock_env_get, mock_open_file, m
148
148
  assert settings.global_custom_values["run_id"] == "test-run-123"
149
149
 
150
150
  # "EXPERIMENT_ID" should not be included as os.environ.get returned None.
151
- assert "experiment_id" not in settings.global_custom_values
151
+ assert "experiment_id" not in settings.global_custom_values
152
+
153
+ @patch('os.path.exists', return_value=False)
154
+ def test_get_settings_parses_sensitive_keys(mock_exists, monkeypatch):
155
+ """
156
+ Tests whether the SENSITIVE_FIELD_NAMES environment variable is correctly parsed
157
+ and converted into a lowercase set (sensitive_keys).
158
+ """
159
+ monkeypatch.setenv("SENSITIVE_FIELD_NAMES", "password, Token, api_key,, secret ")
160
+
161
+ get_weaviate_settings.cache_clear()
162
+
163
+ # 2. Act
164
+ settings = get_weaviate_settings()
165
+
166
+ # 3. Assert
167
+ assert isinstance(settings.sensitive_keys, set)
168
+ assert len(settings.sensitive_keys) == 4 # 이제 4개가 맞습니다.
169
+ assert settings.sensitive_keys == {"password", "token", "api_key", "secret"}
170
+
171
+
172
+ # [MODIFIED] @patch('os.environ.get', return_value=None) 제거, monkeypatch 인자 추가
173
+ @patch('os.path.exists', return_value=False)
174
+ def test_get_settings_sensitive_keys_default(mock_exists, monkeypatch): # <-- FIX: monkeypatch 사용
175
+ """
176
+ Test when there is no SENSITIVE_FIELD_NAMES environment variable set default
177
+ """
178
+ get_weaviate_settings.cache_clear()
179
+ monkeypatch.delenv("SENSITIVE_FIELD_NAMES", raising=False)
180
+
181
+ # 2. Act
182
+ settings = get_weaviate_settings()
183
+
184
+ # 3. Assert: check default for 5
185
+ assert settings.sensitive_keys == {"password", "api_key", "token", "secret", "auth_token"}
186
+ assert len(settings.sensitive_keys) == 5
@@ -1,15 +1,18 @@
1
1
  import pytest
2
2
  from unittest.mock import MagicMock
3
+ import json
4
+ from datetime import datetime
3
5
  import time
4
6
 
5
- from vectorwave.monitoring.tracer import trace_root, trace_span
7
+ from vectorwave.monitoring.tracer import trace_root, trace_span, _mask_and_serialize
8
+
6
9
  from vectorwave.models.db_config import WeaviateSettings
7
10
 
8
11
  # --- Import real functions for cache clearing ---
9
12
  from vectorwave.batch.batch import get_batch_manager as real_get_batch_manager
10
13
  from vectorwave.database.db import get_cached_client as real_get_cached_client
11
14
  from vectorwave.models.db_config import get_weaviate_settings as real_get_settings
12
- from vectorwave.monitoring.tracer import TraceCollector, current_tracer_var
15
+ from vectorwave.monitoring.tracer import TraceCollector, current_tracer_var, current_span_id_var
13
16
  from vectorwave.monitoring.alert.base import BaseAlerter
14
17
 
15
18
  # Module paths to mock (adjust to your project structure if needed)
@@ -39,7 +42,8 @@ def mock_tracer_deps(monkeypatch):
39
42
  EXECUTION_COLLECTION_NAME="TestExecutions",
40
43
  custom_properties=None, # Not important for this test
41
44
  global_custom_values={"run_id": "global-run-abc", "env": "test"},
42
- failure_mapping={"ValueError": "INVALID_INPUT"}
45
+ failure_mapping={"ValueError": "INVALID_INPUT"},
46
+ sensitive_keys={"password", "token", "api_key"}
43
47
  )
44
48
  mock_get_settings = MagicMock(return_value=mock_settings)
45
49
 
@@ -73,16 +77,18 @@ def mock_tracer_deps(monkeypatch):
73
77
 
74
78
  def test_trace_root_and_span_success(mock_tracer_deps):
75
79
  """
76
- Case 1: Success (Root + Span) - The span should be recorded successfully.
80
+ Case 1: Success (Root + Span)
81
+ [MODIFIED] Now also checks for parent_span_id hierarchy.
77
82
  """
78
- mock_batch = mock_tracer_deps["batch"]
83
+ mock_batch = mock_tracer_deps["batch"].add_object # Get the add_object mock
79
84
  mock_alerter = mock_tracer_deps["alerter"]
80
85
 
81
- @trace_span
86
+ @trace_span # Child
82
87
  def my_inner_span(x):
83
88
  return f"result: {x}"
84
89
 
85
- @trace_root()
90
+ @trace_root() # Root (sets up context)
91
+ @trace_span # Root Span (logs itself)
86
92
  def my_workflow_root():
87
93
  return my_inner_span(x=10)
88
94
 
@@ -91,34 +97,48 @@ def test_trace_root_and_span_success(mock_tracer_deps):
91
97
 
92
98
  # --- Assert ---
93
99
  assert result == "result: 10"
94
- mock_batch.add_object.assert_called_once()
100
+ # 2 calls: 1 (inner_span) + 1 (workflow_root)
101
+ assert mock_batch.call_count == 2
95
102
  mock_alerter.notify.assert_not_called()
96
103
 
97
- args, kwargs = mock_batch.add_object.call_args
98
- props = kwargs["properties"]
104
+ call_args_list = mock_batch.call_args_list
105
+ props_map = {
106
+ call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
107
+ for call in call_args_list
108
+ }
109
+
110
+ assert "my_inner_span" in props_map
111
+ assert "my_workflow_root" in props_map
112
+
113
+ root_props = props_map["my_workflow_root"]
114
+ inner_props = props_map["my_inner_span"]
99
115
 
100
- assert kwargs["collection"] == "TestExecutions"
101
- assert props["status"] == "SUCCESS"
102
- assert props["function_name"] == "my_inner_span"
103
- assert props["error_message"] is None
104
- assert kwargs["properties"]["error_code"] is None
105
- assert "trace_id" in props
106
- assert props["run_id"] == "global-run-abc"
107
- assert props["env"] == "test"
116
+ # --- [NEW] Hierarchy Validation ---
117
+ assert root_props["parent_span_id"] is None # Root span has no parent
118
+ assert inner_props["parent_span_id"] is not None
119
+ assert inner_props["parent_span_id"] == root_props["span_id"] # Child's parent is Root
120
+
121
+ assert root_props["trace_id"] == inner_props["trace_id"]
122
+ assert root_props["status"] == "SUCCESS"
123
+ assert inner_props["status"] == "SUCCESS"
124
+ assert root_props["run_id"] == "global-run-abc"
125
+ assert inner_props["run_id"] == "global-run-abc"
108
126
 
109
127
 
110
128
  def test_trace_span_failure(mock_tracer_deps):
111
129
  """
112
- Case 2: Failure (Root + Failing Span) - The span should be recorded with an ERROR status.
130
+ Case 2: Failure (Root + Failing Span)
131
+ Now checks hierarchy and logs both root (success) and child (error).
113
132
  """
114
- mock_batch = mock_tracer_deps["batch"]
133
+ mock_batch = mock_tracer_deps["batch"].add_object # Get the add_object mock
115
134
  mock_alerter = mock_tracer_deps["alerter"]
116
135
 
117
- @trace_span
136
+ @trace_span # Child (Failing)
118
137
  def my_failing_span():
119
138
  raise ValueError("This is a test error")
120
139
 
121
- @trace_root()
140
+ @trace_root() # Root (sets up context)
141
+ @trace_span # Root Span (Success)
122
142
  def my_workflow_root_fail():
123
143
  my_failing_span()
124
144
 
@@ -127,34 +147,46 @@ def test_trace_span_failure(mock_tracer_deps):
127
147
  my_workflow_root_fail()
128
148
 
129
149
  # --- Assert (Log) ---
130
- mock_batch.add_object.assert_called_once()
131
- db_props = mock_batch.add_object.call_args.kwargs["properties"]
132
- assert db_props["status"] == "ERROR"
133
- assert "ValueError: This is a test error" in db_props["error_message"]
134
- assert db_props["error_code"] == "INVALID_INPUT"
150
+ # 2 calls: 1 (root_fail - SUCCESS) + 1 (failing_span - ERROR)
151
+ assert mock_batch.call_count == 2
135
152
 
136
- mock_alerter.notify.assert_called_once()
137
- alert_props = mock_alerter.notify.call_args.args[0]
153
+ call_args_list = mock_batch.call_args_list
154
+ props_map = {
155
+ call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
156
+ for call in call_args_list
157
+ }
138
158
 
139
- args, kwargs = mock_batch.add_object.call_args
140
- props = kwargs["properties"]
159
+ assert "my_workflow_root_fail" in props_map
160
+ assert "my_failing_span" in props_map
141
161
 
162
+ root_props = props_map["my_workflow_root_fail"]
163
+ failing_props = props_map["my_failing_span"]
142
164
 
143
- assert alert_props == db_props
165
+ # Root Span Asserts (SUCCESS)
166
+ assert root_props["status"] == "ERROR"
167
+ assert root_props["parent_span_id"] is None
168
+
169
+ # Failing Span Asserts (ERROR)
170
+ assert failing_props["status"] == "ERROR"
171
+ assert "ValueError: This is a test error" in failing_props["error_message"]
172
+ assert failing_props["error_code"] == "INVALID_INPUT"
173
+ assert failing_props["parent_span_id"] == root_props["span_id"]
174
+ assert failing_props["run_id"] == "global-run-abc"
175
+
176
+ # Alerter Asserts
177
+ mock_alerter.notify.assert_called_once()
178
+ alert_props = mock_alerter.notify.call_args.args[0]
179
+
180
+ assert alert_props == failing_props # Alerter was called with the failing span's properties
144
181
  assert alert_props["status"] == "ERROR"
145
182
  assert alert_props["error_code"] == "INVALID_INPUT"
146
- assert props["status"] == "ERROR"
147
- assert "ValueError: This is a test error" in props["error_message"]
148
- assert kwargs["properties"]["error_code"] == "INVALID_INPUT"
149
- assert props["function_name"] == "my_failing_span"
150
- assert props["run_id"] == "global-run-abc"
151
183
 
152
184
 
153
185
  def test_span_without_root_does_nothing(mock_tracer_deps):
154
186
  """
155
187
  Case 3: Tracing disabled (Span only) - If there's no Root, nothing should be recorded.
156
188
  """
157
- mock_batch = mock_tracer_deps["batch"]
189
+ mock_batch = mock_tracer_deps["batch"].add_object
158
190
 
159
191
  @trace_span
160
192
  def my_lonely_span():
@@ -165,60 +197,78 @@ def test_span_without_root_does_nothing(mock_tracer_deps):
165
197
 
166
198
  # --- Assert ---
167
199
  assert result == "lonely_result"
168
- mock_batch.add_object.assert_not_called()
200
+ mock_batch.assert_not_called()
169
201
 
170
202
 
171
203
  def test_span_captures_attributes_and_overrides_globals(mock_tracer_deps):
172
204
  """
173
205
  Case 4/5: Attribute Capturing and Overriding
206
+ Checks hierarchy.
174
207
  """
175
- mock_batch = mock_tracer_deps["batch"]
208
+ mock_batch = mock_tracer_deps["batch"].add_object
176
209
 
177
210
  class MyObject:
178
211
  def __str__(self): return "MyObjectInstance"
179
212
 
180
- @trace_span(attributes_to_capture=["team", "priority", "run_id", "user_obj"])
181
- def my_span_with_attrs(team, priority, run_id, user_obj, other_arg="default"):
213
+ @trace_span(attributes_to_capture=["team", "priority", "run_id", "user_obj", "password"])
214
+ def my_span_with_attrs(team, priority, run_id, user_obj, password, other_arg="default"):
182
215
  return "captured"
183
216
 
184
217
  @trace_root()
218
+ @trace_span # Root span
185
219
  def my_workflow_root_attrs():
186
220
  return my_span_with_attrs(
187
221
  team="backend",
188
222
  priority=1,
189
223
  run_id="override-run-xyz", # <-- This should override "global-run-abc"
190
224
  user_obj=MyObject(),
191
- other_arg="should-be-ignored"
225
+ other_arg="should-be-ignored",
226
+ password="my_secret_password_123"
192
227
  )
193
228
 
194
229
  # --- Act ---
195
230
  my_workflow_root_attrs()
196
231
 
197
232
  # --- Assert ---
198
- mock_batch.add_object.assert_called_once()
199
- props = mock_batch.add_object.call_args.kwargs["properties"]
233
+ assert mock_batch.call_count == 2
234
+
235
+ props_map = {
236
+ call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
237
+ for call in mock_batch.call_args_list
238
+ }
239
+ root_props = props_map["my_workflow_root_attrs"]
240
+ child_props = props_map["my_span_with_attrs"]
241
+
242
+ # Hierarchy
243
+ assert root_props["parent_span_id"] is None
244
+ assert child_props["parent_span_id"] == root_props["span_id"]
200
245
 
201
- assert props["team"] == "backend"
202
- assert props["priority"] == 1
203
- assert props["user_obj"] == "MyObjectInstance"
204
- assert props["run_id"] == "override-run-xyz" # Overridden
205
- assert props["env"] == "test" # Non-overridden global remains
206
- assert "other_arg" not in props
246
+ # Child properties (existing test)
247
+ assert child_props["team"] == "backend"
248
+ assert child_props["priority"] == 1
249
+ assert child_props["user_obj"] == "MyObjectInstance"
250
+ assert child_props["run_id"] == "override-run-xyz" # Overridden
251
+ assert child_props["env"] == "test" # Non-overridden global remains
252
+ assert "other_arg" not in child_props
253
+
254
+ assert "password" in child_props
255
+ assert child_props["password"] == "[MASKED]"
207
256
 
208
257
 
209
258
  def test_root_accepts_custom_trace_id(mock_tracer_deps):
210
259
  """
211
260
  Bonus: Test case for manually providing a 'trace_id'.
212
- (This is the test that was fixed)
261
+ Checks hierarchy and trace_id propagation.
213
262
  """
214
- mock_batch = mock_tracer_deps["batch"]
263
+ mock_batch = mock_tracer_deps["batch"].add_object
215
264
 
216
265
  @trace_span
217
266
  def my_inner_span():
218
267
  pass
219
268
 
220
- @trace_root()
221
- def my_workflow_root_custom_id(): # <-- ✅ FIXED: Removed 'trace_id' arg
269
+ @trace_root() # Sets up context
270
+ @trace_span # Root span
271
+ def my_workflow_root_custom_id():
222
272
  my_inner_span()
223
273
 
224
274
  # --- Act ---
@@ -226,16 +276,30 @@ def test_root_accepts_custom_trace_id(mock_tracer_deps):
226
276
  my_workflow_root_custom_id(trace_id="my-custom-trace-id-123")
227
277
 
228
278
  # --- Assert ---
229
- mock_batch.add_object.assert_called_once()
230
- props = mock_batch.add_object.call_args.kwargs["properties"]
279
+ assert mock_batch.call_count == 2
280
+
281
+ props_map = {
282
+ call.kwargs["properties"]["function_name"]: call.kwargs["properties"]
283
+ for call in mock_batch.call_args_list
284
+ }
285
+ root_props = props_map["my_workflow_root_custom_id"]
286
+ child_props = props_map["my_inner_span"]
231
287
 
232
288
  # Check if the trace_id was popped and injected correctly
233
- assert props["trace_id"] == "my-custom-trace-id-123"
289
+ assert root_props["trace_id"] == "my-custom-trace-id-123"
290
+ assert child_props["trace_id"] == "my-custom-trace-id-123"
291
+
292
+ # Check hierarchy
293
+ assert root_props["parent_span_id"] is None
294
+ assert child_props["parent_span_id"] == root_props["span_id"]
234
295
 
235
296
 
236
297
  def test_trace_span_error_code_priority_1_custom_attr(mock_tracer_deps):
237
- """Test (Priority 1) custom e.error_code attribute."""
238
- mock_batch = mock_tracer_deps["batch"]
298
+ """
299
+ Test (Priority 1) custom e.error_code attribute.
300
+ Also sets and checks parent_span_id.
301
+ """
302
+ mock_batch = mock_tracer_deps["batch"].add_object
239
303
  tracer = TraceCollector(trace_id="test_trace_p1")
240
304
  tracer.settings = mock_tracer_deps["settings"]
241
305
 
@@ -243,21 +307,29 @@ def test_trace_span_error_code_priority_1_custom_attr(mock_tracer_deps):
243
307
  def my_custom_fail_function():
244
308
  raise CustomErrorWithCode("Test", "PAYMENT_FAILED_001")
245
309
 
246
- token = current_tracer_var.set(tracer)
310
+ # Set up mock parent context
311
+ tracer_token = current_tracer_var.set(tracer)
312
+ parent_span_token = current_span_id_var.set("mock-parent-id-123")
313
+
247
314
  try:
248
315
  with pytest.raises(CustomErrorWithCode):
249
316
  my_custom_fail_function()
250
317
  finally:
251
- current_tracer_var.reset(token)
318
+ current_span_id_var.reset(parent_span_token)
319
+ current_tracer_var.reset(tracer_token)
252
320
 
253
- args, kwargs = mock_batch.add_object.call_args
321
+ args, kwargs = mock_batch.call_args
254
322
  assert kwargs["properties"]["status"] == "ERROR"
255
323
  assert kwargs["properties"]["error_code"] == "PAYMENT_FAILED_001"
324
+ assert kwargs["properties"]["parent_span_id"] == "mock-parent-id-123"
256
325
 
257
326
 
258
327
  def test_trace_span_error_code_priority_3_default_class_name(mock_tracer_deps):
259
- """Test (Priority 3) default class name (KeyError is not in mapping)."""
260
- mock_batch = mock_tracer_deps["batch"]
328
+ """
329
+ Test (Priority 3) default class name (KeyError is not in mapping).
330
+ Also sets and checks parent_span_id.
331
+ """
332
+ mock_batch = mock_tracer_deps["batch"].add_object
261
333
  tracer = TraceCollector(trace_id="test_trace_p3")
262
334
  tracer.settings = mock_tracer_deps["settings"]
263
335
 
@@ -265,13 +337,17 @@ def test_trace_span_error_code_priority_3_default_class_name(mock_tracer_deps):
265
337
  def my_key_error_function():
266
338
  _ = {}["missing_key"] # Raises KeyError
267
339
 
268
- token = current_tracer_var.set(tracer)
340
+ tracer_token = current_tracer_var.set(tracer)
341
+ parent_span_token = current_span_id_var.set("mock-parent-id-456")
342
+
269
343
  try:
270
344
  with pytest.raises(KeyError):
271
345
  my_key_error_function()
272
346
  finally:
273
- current_tracer_var.reset(token)
347
+ current_span_id_var.reset(parent_span_token)
348
+ current_tracer_var.reset(tracer_token)
274
349
 
275
- args, kwargs = mock_batch.add_object.call_args
350
+ args, kwargs = mock_batch.call_args
276
351
  assert kwargs["properties"]["status"] == "ERROR"
277
352
  assert kwargs["properties"]["error_code"] == "KeyError"
353
+ assert kwargs["properties"]["parent_span_id"] == "mock-parent-id-456"
@@ -66,17 +66,22 @@ MockDateTime.fromisoformat = MagicMock(side_effect=datetime.fromisoformat)
66
66
 
67
67
 
68
68
  @patch('vectorwave.search.execution_search.datetime', MockDateTime)
69
- @patch('vectorwave.search.execution_search.find_executions', MagicMock(return_value=mock_db_errors))
70
- def test_find_recent_errors():
69
+ @patch('vectorwave.search.execution_search.find_executions')
70
+ def test_find_recent_errors(mock_find_executions):
71
71
  """
72
- Tests if find_recent_errors (including manual filtering) works correctly.
72
+ Tests if find_recent_errors calls find_executions with the correct filters.
73
+ (The test no longer checks manual filtering, as filtering is delegated to the DB).
73
74
  """
74
75
  # 1. Run test
75
- result = find_recent_errors(minutes_ago=10, error_codes=["INVALID_INPUT"])
76
+ find_recent_errors(minutes_ago=10, error_codes=["INVALID_INPUT"])
77
+
78
+ # 2. Verify find_executions was called correctly
79
+ call_args = mock_find_executions.call_args
80
+ filters_arg = call_args.kwargs['filters']
76
81
 
77
- # 2. Verify result (if manual filtering worked correctly)
78
- assert len(result) == 1 # Logs from 15 minutes ago should be filtered out
79
- assert result[0]["error_code"] == "INVALID_INPUT"
82
+ assert filters_arg['status'] == 'ERROR'
83
+ assert filters_arg['error_code'] == 'INVALID_INPUT'
84
+ assert filters_arg['timestamp_utc__gte'] == expected_time_limit_iso
80
85
 
81
86
 
82
87
  @patch('vectorwave.search.execution_search.find_executions')
@@ -0,0 +1,126 @@
1
+ import pytest
2
+ import json
3
+ from unittest.mock import patch, mock_open, ANY, MagicMock
4
+ from vectorwave.utils.function_cache import FunctionCacheManager
5
+
6
+ # --- 1. Tests for calculate_content_hash static method ---
7
+
8
+ def test_calculate_content_hash_consistency():
9
+ """Tests that identical inputs always return the same hash."""
10
+ props1 = {"a": 1, "b": "test", "c": [1, 2, 3]}
11
+ props2 = {"a": 1, "b": "test", "c": [1, 2, 3]}
12
+
13
+ hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
14
+ hash2 = FunctionCacheManager.calculate_content_hash("id1", props2)
15
+
16
+ assert hash1 == hash2
17
+ assert len(hash1) == 64 # SHA256
18
+
19
+ def test_calculate_content_hash_sensitivity_props():
20
+ """Tests that different properties return different hashes."""
21
+ props1 = {"a": 1, "b": "test"}
22
+ props2 = {"a": 1, "b": "changed"}
23
+
24
+ hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
25
+ hash2 = FunctionCacheManager.calculate_content_hash("id1", props2)
26
+
27
+ assert hash1 != hash2
28
+
29
+ def test_calculate_content_hash_sensitivity_id():
30
+ """Tests that different identifiers return different hashes."""
31
+ props1 = {"a": 1}
32
+
33
+ hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
34
+ hash2 = FunctionCacheManager.calculate_content_hash("id2", props1)
35
+
36
+ assert hash1 != hash2
37
+
38
+ def test_calculate_content_hash_order_invariance():
39
+ """
40
+ Tests that different dictionary key orders return the same hash,
41
+ due to json.dumps(sort_keys=True).
42
+ """
43
+ props1 = {"a": 1, "b": 2}
44
+ props2 = {"b": 2, "a": 1} # Changed order
45
+
46
+ hash1 = FunctionCacheManager.calculate_content_hash("id1", props1)
47
+ hash2 = FunctionCacheManager.calculate_content_hash("id1", props2)
48
+
49
+ assert hash1 == hash2
50
+
51
+
52
+ # --- 2. Tests for __init__ and _load_cache ---
53
+
54
+ @patch("os.path.exists", return_value=False)
55
+ def test_init_cache_file_not_found(mock_exists):
56
+ """Tests that self.cache is initialized as an empty dict ({}) when the cache file is not found."""
57
+ manager = FunctionCacheManager()
58
+
59
+ mock_exists.assert_called_once_with("./.vectorwave_functions_cache.json")
60
+ assert manager.cache == {}
61
+
62
+ @patch("os.path.exists", return_value=True)
63
+ @patch("builtins.open", new_callable=mock_open, read_data='{"uuid_abc": "hash_123"}')
64
+ def test_init_cache_file_loaded(mock_file, mock_exists):
65
+ """Tests that the cache file is loaded successfully."""
66
+ manager = FunctionCacheManager()
67
+
68
+ mock_exists.assert_called_once_with("./.vectorwave_functions_cache.json")
69
+ mock_file.assert_called_once_with("./.vectorwave_functions_cache.json", 'r', encoding='utf-8')
70
+ assert manager.cache == {"uuid_abc": "hash_123"}
71
+
72
+ @patch("os.path.exists", return_value=True)
73
+ @patch("builtins.open", new_callable=mock_open, read_data='{invalid_json')
74
+ @patch("logging.Logger.warning") # Mock the logger
75
+ def test_init_cache_invalid_json(mock_logger, mock_file, mock_exists):
76
+ """Tests that a warning is logged and cache is an empty dict ({}) if JSON parsing fails."""
77
+ manager = FunctionCacheManager()
78
+
79
+ assert manager.cache == {}
80
+ mock_logger.assert_called_once()
81
+ assert "Failed to load static function cache" in mock_logger.call_args[0][0]
82
+
83
+
84
+ # --- 3. Tests for logic and saving ---
85
+
86
+ @patch("os.path.exists", return_value=False)
87
+ def test_is_cached_and_unchanged_logic(mock_exists):
88
+ """Tests the True/False logic of is_cached_and_unchanged."""
89
+ manager = FunctionCacheManager()
90
+ manager.cache = {
91
+ "uuid_A": "hash_A_123",
92
+ "uuid_B": "hash_B_123"
93
+ }
94
+
95
+ # 1. Cache Hit (Both UUID and hash match)
96
+ assert manager.is_cached_and_unchanged("uuid_A", "hash_A_123") == True
97
+
98
+ # 2. Cache Miss (UUID matches, but hash is different)
99
+ assert manager.is_cached_and_unchanged("uuid_B", "hash_B_456_NEW") == False
100
+
101
+ # 3. Cache Miss (UUID does not exist)
102
+ assert manager.is_cached_and_unchanged("uuid_C_NEW", "hash_C_123") == False
103
+
104
+ @patch("os.path.exists", return_value=False)
105
+ @patch("json.dump") # Mock json.dump
106
+ @patch("builtins.open", new_callable=mock_open)
107
+ def test_update_cache_calls_save(mock_file, mock_json_dump, mock_exists):
108
+ """Tests if update_cache updates self.cache and calls _save_cache."""
109
+ manager = FunctionCacheManager()
110
+ assert manager.cache == {}
111
+
112
+ manager.update_cache("uuid_new", "hash_new_123")
113
+
114
+ # 1. Check if the internal cache was updated
115
+ assert manager.cache == {"uuid_new": "hash_new_123"}
116
+
117
+ # 2. Check if _save_cache was called and attempted to write to the file
118
+ mock_file.assert_called_once_with("./.vectorwave_functions_cache.json", 'w', encoding='utf-8')
119
+
120
+ # 3. Check if json.dump was called with the correct arguments (indent, sort_keys)
121
+ mock_json_dump.assert_called_once_with(
122
+ {"uuid_new": "hash_new_123"}, # 1. The updated cache
123
+ ANY, # 2. The mock_file handle
124
+ indent=4,
125
+ sort_keys=True
126
+ )
@@ -1,7 +1,7 @@
1
1
  from .core.decorator import vectorize
2
2
 
3
3
  from .database.db import initialize_database
4
- from .database.db_search import search_functions, search_executions
4
+ from .database.db_search import search_functions, search_executions, search_errors_by_message
5
5
  from .monitoring.tracer import trace_span
6
6
 
7
7
  __all__ = [
@@ -9,5 +9,6 @@ __all__ = [
9
9
  'initialize_database',
10
10
  'search_functions',
11
11
  'search_executions',
12
+ 'search_errors_by_message',
12
13
  'trace_span'
13
14
  ]