|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -import json |
4 | 3 | import uuid |
5 | 4 | from typing import Any |
6 | 5 | from datetime import UTC, datetime |
@@ -44,6 +43,23 @@ def _synthetic_function(module_name: str, filename: str, body: str, **values: An |
44 | 43 | return namespace["run"] |
45 | 44 |
|
46 | 45 |
|
| 46 | +def _record_synthetic(function: Any, *, innermost_only: bool = False) -> dict[str, Any]: |
| 47 | + span = _make_span() |
| 48 | + try: |
| 49 | + function() |
| 50 | + except Exception as exc: |
| 51 | + if innermost_only: |
| 52 | + traceback = exc.__traceback__ |
| 53 | + assert traceback is not None |
| 54 | + while traceback.tb_next is not None: |
| 55 | + traceback = traceback.tb_next |
| 56 | + exc = exc.with_traceback(traceback) |
| 57 | + set_span_error(span, exc) |
| 58 | + error = get_span_error(span) |
| 59 | + assert error is not None |
| 60 | + return error |
| 61 | + |
| 62 | + |
47 | 63 | # --------------------------------------------------------------------------- |
48 | 64 | # Helpers: set_span_error / get_span_error |
49 | 65 | # --------------------------------------------------------------------------- |
@@ -102,53 +118,102 @@ class ImplicitlyCategorizedError(RuntimeError): |
102 | 118 | set_span_error(span, ImplicitlyCategorizedError("boom")) |
103 | 119 | assert get_span_error(span)["category"] == "unknown" # type: ignore[index] |
104 | 120 |
|
105 | | - def test_agentex_internal_origin_is_platform(self): |
106 | | - platform = _synthetic_function( |
107 | | - "agentex.lib.synthetic_runtime", |
108 | | - "/synthetic/agentex/runtime.py", |
| 121 | + def test_generic_agentex_wrapper_around_user_failure_is_application(self): |
| 122 | + application = _synthetic_function( |
| 123 | + "customer_agent.tool", |
| 124 | + "/synthetic/application/tool.py", |
109 | 125 | "raise RuntimeError('boom')", |
110 | 126 | ) |
111 | | - span = _make_span() |
112 | | - try: |
113 | | - platform() |
114 | | - except Exception as exc: |
115 | | - set_span_error(span, exc) |
| 127 | + wrapper = _synthetic_function( |
| 128 | + "agentex.lib.core.temporal.workflows.workflow", |
| 129 | + "/synthetic/agentex/workflow.py", |
| 130 | + "target()", |
| 131 | + target=application, |
| 132 | + ) |
116 | 133 |
|
117 | | - error = get_span_error(span) |
118 | | - assert error is not None |
119 | | - assert error["category"] == "platform" |
| 134 | + error = _record_synthetic(wrapper) |
| 135 | + |
| 136 | + assert error["category"] == "application" |
120 | 137 | assert error["category_source"] == "stack_trace" |
121 | | - assert error["category_reason"] == "stack_rule:platform_module" |
| 138 | + assert error["category_reason"] == "stack_rule:unowned_absolute_source" |
122 | 139 |
|
123 | | - def test_stdlib_dependency_under_application_is_application(self): |
| 140 | + def test_application_database_failure_is_application(self): |
| 141 | + driver = _synthetic_function( |
| 142 | + "sqlalchemy.engine", |
| 143 | + "/venv/site-packages/sqlalchemy/engine.py", |
| 144 | + "raise RuntimeError('db failed')", |
| 145 | + ) |
124 | 146 | application = _synthetic_function( |
125 | 147 | "customer_agent.main", |
126 | 148 | "/synthetic/application/main.py", |
127 | | - "parse('{')", |
128 | | - parse=json.loads, |
| 149 | + "query()", |
| 150 | + query=driver, |
129 | 151 | ) |
130 | | - span = _make_span() |
131 | | - try: |
132 | | - application() |
133 | | - except Exception as exc: |
134 | | - set_span_error(span, exc) |
135 | 152 |
|
136 | | - assert get_span_error(span)["category"] == "application" # type: ignore[index] |
| 153 | + assert _record_synthetic(application)["category"] == "application" |
| 154 | + |
| 155 | + def test_managed_stream_database_failure_is_platform(self): |
| 156 | + driver = _synthetic_function( |
| 157 | + "redis.asyncio.client", |
| 158 | + "/venv/site-packages/redis/client.py", |
| 159 | + "raise RuntimeError('db failed')", |
| 160 | + ) |
| 161 | + managed_store = _synthetic_function( |
| 162 | + "agentex.lib.core.adapters.streams.adapter_redis", |
| 163 | + "/synthetic/agentex/adapter_redis.py", |
| 164 | + "query()", |
| 165 | + query=driver, |
| 166 | + ) |
137 | 167 |
|
138 | | - def test_stdlib_dependency_under_agentex_is_platform(self): |
139 | | - platform = _synthetic_function( |
140 | | - "agentex.lib.synthetic_runtime", |
141 | | - "/synthetic/agentex/runtime.py", |
142 | | - "parse('{')", |
143 | | - parse=json.loads, |
| 168 | + assert _record_synthetic(managed_store)["category"] == "platform" |
| 169 | + |
| 170 | + def test_application_validation_inside_platform_operation_is_application(self): |
| 171 | + validation = _synthetic_function( |
| 172 | + "customer_agent.query", |
| 173 | + "/synthetic/application/query.py", |
| 174 | + "raise ValueError('invalid configuration')", |
| 175 | + ) |
| 176 | + managed_store = _synthetic_function( |
| 177 | + "agentex.lib.core.adapters.streams.adapter_redis", |
| 178 | + "/synthetic/agentex/adapter_redis.py", |
| 179 | + "validate()", |
| 180 | + validate=validation, |
| 181 | + ) |
| 182 | + |
| 183 | + assert _record_synthetic(managed_store)["category"] == "application" |
| 184 | + |
| 185 | + def test_driver_only_database_failure_is_unknown(self): |
| 186 | + driver = _synthetic_function( |
| 187 | + "pyodbc", |
| 188 | + "/venv/site-packages/pyodbc.py", |
| 189 | + "raise RuntimeError('db failed')", |
| 190 | + ) |
| 191 | + |
| 192 | + error = _record_synthetic(driver, innermost_only=True) |
| 193 | + |
| 194 | + assert error["category"] == "unknown" |
| 195 | + assert error["category_reason"] == "stack_no_owned_frame" |
| 196 | + |
| 197 | + def test_nested_wrapper_dependency_propagation_uses_application(self): |
| 198 | + driver = _synthetic_function( |
| 199 | + "sqlalchemy.engine", |
| 200 | + "/venv/site-packages/sqlalchemy/engine.py", |
| 201 | + "raise RuntimeError('db failed')", |
| 202 | + ) |
| 203 | + application = _synthetic_function( |
| 204 | + "customer_agent.repository", |
| 205 | + "/synthetic/application/repository.py", |
| 206 | + "query()", |
| 207 | + query=driver, |
| 208 | + ) |
| 209 | + wrapper = _synthetic_function( |
| 210 | + "agentex.lib.core.temporal.activities.activity_helpers", |
| 211 | + "/synthetic/agentex/activity_helpers.py", |
| 212 | + "target()", |
| 213 | + target=application, |
144 | 214 | ) |
145 | | - span = _make_span() |
146 | | - try: |
147 | | - platform() |
148 | | - except Exception as exc: |
149 | | - set_span_error(span, exc) |
150 | 215 |
|
151 | | - assert get_span_error(span)["category"] == "platform" # type: ignore[index] |
| 216 | + assert _record_synthetic(wrapper)["category"] == "application" |
152 | 217 |
|
153 | 218 | def test_set_preserves_existing_dict_keys(self): |
154 | 219 | span = _make_span(data={"__span_type__": "LLM"}) |
|
0 commit comments