yamlgraph 0.3.9__py3-none-any.whl

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 (185) hide show
  1. examples/__init__.py +1 -0
  2. examples/codegen/__init__.py +5 -0
  3. examples/codegen/models/__init__.py +13 -0
  4. examples/codegen/models/schemas.py +76 -0
  5. examples/codegen/tests/__init__.py +1 -0
  6. examples/codegen/tests/test_ai_helpers.py +235 -0
  7. examples/codegen/tests/test_ast_analysis.py +174 -0
  8. examples/codegen/tests/test_code_analysis.py +134 -0
  9. examples/codegen/tests/test_code_context.py +301 -0
  10. examples/codegen/tests/test_code_nav.py +89 -0
  11. examples/codegen/tests/test_dependency_tools.py +119 -0
  12. examples/codegen/tests/test_example_tools.py +185 -0
  13. examples/codegen/tests/test_git_tools.py +112 -0
  14. examples/codegen/tests/test_impl_agent_schemas.py +193 -0
  15. examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
  16. examples/codegen/tests/test_jedi_analysis.py +226 -0
  17. examples/codegen/tests/test_meta_tools.py +250 -0
  18. examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
  19. examples/codegen/tests/test_syntax_tools.py +85 -0
  20. examples/codegen/tests/test_synthesize_prompt.py +94 -0
  21. examples/codegen/tests/test_template_tools.py +244 -0
  22. examples/codegen/tools/__init__.py +80 -0
  23. examples/codegen/tools/ai_helpers.py +420 -0
  24. examples/codegen/tools/ast_analysis.py +92 -0
  25. examples/codegen/tools/code_context.py +180 -0
  26. examples/codegen/tools/code_nav.py +52 -0
  27. examples/codegen/tools/dependency_tools.py +120 -0
  28. examples/codegen/tools/example_tools.py +188 -0
  29. examples/codegen/tools/git_tools.py +151 -0
  30. examples/codegen/tools/impl_executor.py +614 -0
  31. examples/codegen/tools/jedi_analysis.py +311 -0
  32. examples/codegen/tools/meta_tools.py +202 -0
  33. examples/codegen/tools/syntax_tools.py +26 -0
  34. examples/codegen/tools/template_tools.py +356 -0
  35. examples/fastapi_interview.py +167 -0
  36. examples/npc/api/__init__.py +1 -0
  37. examples/npc/api/app.py +100 -0
  38. examples/npc/api/routes/__init__.py +5 -0
  39. examples/npc/api/routes/encounter.py +182 -0
  40. examples/npc/api/session.py +330 -0
  41. examples/npc/demo.py +387 -0
  42. examples/npc/nodes/__init__.py +5 -0
  43. examples/npc/nodes/image_node.py +92 -0
  44. examples/npc/run_encounter.py +230 -0
  45. examples/shared/__init__.py +0 -0
  46. examples/shared/replicate_tool.py +238 -0
  47. examples/storyboard/__init__.py +1 -0
  48. examples/storyboard/generate_videos.py +335 -0
  49. examples/storyboard/nodes/__init__.py +12 -0
  50. examples/storyboard/nodes/animated_character_node.py +248 -0
  51. examples/storyboard/nodes/animated_image_node.py +138 -0
  52. examples/storyboard/nodes/character_node.py +162 -0
  53. examples/storyboard/nodes/image_node.py +118 -0
  54. examples/storyboard/nodes/replicate_tool.py +49 -0
  55. examples/storyboard/retry_images.py +118 -0
  56. scripts/demo_async_executor.py +212 -0
  57. scripts/demo_interview_e2e.py +200 -0
  58. scripts/demo_streaming.py +140 -0
  59. scripts/run_interview_demo.py +94 -0
  60. scripts/test_interrupt_fix.py +26 -0
  61. tests/__init__.py +1 -0
  62. tests/conftest.py +178 -0
  63. tests/integration/__init__.py +1 -0
  64. tests/integration/test_animated_storyboard.py +63 -0
  65. tests/integration/test_cli_commands.py +242 -0
  66. tests/integration/test_colocated_prompts.py +139 -0
  67. tests/integration/test_map_demo.py +50 -0
  68. tests/integration/test_memory_demo.py +283 -0
  69. tests/integration/test_npc_api/__init__.py +1 -0
  70. tests/integration/test_npc_api/test_routes.py +357 -0
  71. tests/integration/test_npc_api/test_session.py +216 -0
  72. tests/integration/test_pipeline_flow.py +105 -0
  73. tests/integration/test_providers.py +163 -0
  74. tests/integration/test_resume.py +75 -0
  75. tests/integration/test_subgraph_integration.py +295 -0
  76. tests/integration/test_subgraph_interrupt.py +106 -0
  77. tests/unit/__init__.py +1 -0
  78. tests/unit/test_agent_nodes.py +355 -0
  79. tests/unit/test_async_executor.py +346 -0
  80. tests/unit/test_checkpointer.py +212 -0
  81. tests/unit/test_checkpointer_factory.py +212 -0
  82. tests/unit/test_cli.py +121 -0
  83. tests/unit/test_cli_package.py +81 -0
  84. tests/unit/test_compile_graph_map.py +132 -0
  85. tests/unit/test_conditions_routing.py +253 -0
  86. tests/unit/test_config.py +93 -0
  87. tests/unit/test_conversation_memory.py +276 -0
  88. tests/unit/test_database.py +145 -0
  89. tests/unit/test_deprecation.py +104 -0
  90. tests/unit/test_executor.py +172 -0
  91. tests/unit/test_executor_async.py +179 -0
  92. tests/unit/test_export.py +149 -0
  93. tests/unit/test_expressions.py +178 -0
  94. tests/unit/test_feature_brainstorm.py +194 -0
  95. tests/unit/test_format_prompt.py +145 -0
  96. tests/unit/test_generic_report.py +200 -0
  97. tests/unit/test_graph_commands.py +327 -0
  98. tests/unit/test_graph_linter.py +627 -0
  99. tests/unit/test_graph_loader.py +357 -0
  100. tests/unit/test_graph_schema.py +193 -0
  101. tests/unit/test_inline_schema.py +151 -0
  102. tests/unit/test_interrupt_node.py +182 -0
  103. tests/unit/test_issues.py +164 -0
  104. tests/unit/test_jinja2_prompts.py +85 -0
  105. tests/unit/test_json_extract.py +134 -0
  106. tests/unit/test_langsmith.py +600 -0
  107. tests/unit/test_langsmith_tools.py +204 -0
  108. tests/unit/test_llm_factory.py +109 -0
  109. tests/unit/test_llm_factory_async.py +118 -0
  110. tests/unit/test_loops.py +403 -0
  111. tests/unit/test_map_node.py +144 -0
  112. tests/unit/test_no_backward_compat.py +56 -0
  113. tests/unit/test_node_factory.py +348 -0
  114. tests/unit/test_passthrough_node.py +126 -0
  115. tests/unit/test_prompts.py +324 -0
  116. tests/unit/test_python_nodes.py +198 -0
  117. tests/unit/test_reliability.py +298 -0
  118. tests/unit/test_result_export.py +234 -0
  119. tests/unit/test_router.py +296 -0
  120. tests/unit/test_sanitize.py +99 -0
  121. tests/unit/test_schema_loader.py +295 -0
  122. tests/unit/test_shell_tools.py +229 -0
  123. tests/unit/test_state_builder.py +331 -0
  124. tests/unit/test_state_builder_map.py +104 -0
  125. tests/unit/test_state_config.py +197 -0
  126. tests/unit/test_streaming.py +307 -0
  127. tests/unit/test_subgraph.py +596 -0
  128. tests/unit/test_template.py +190 -0
  129. tests/unit/test_tool_call_integration.py +164 -0
  130. tests/unit/test_tool_call_node.py +178 -0
  131. tests/unit/test_tool_nodes.py +129 -0
  132. tests/unit/test_websearch.py +234 -0
  133. yamlgraph/__init__.py +35 -0
  134. yamlgraph/builder.py +110 -0
  135. yamlgraph/cli/__init__.py +159 -0
  136. yamlgraph/cli/__main__.py +6 -0
  137. yamlgraph/cli/commands.py +231 -0
  138. yamlgraph/cli/deprecation.py +92 -0
  139. yamlgraph/cli/graph_commands.py +541 -0
  140. yamlgraph/cli/validators.py +37 -0
  141. yamlgraph/config.py +67 -0
  142. yamlgraph/constants.py +70 -0
  143. yamlgraph/error_handlers.py +227 -0
  144. yamlgraph/executor.py +290 -0
  145. yamlgraph/executor_async.py +288 -0
  146. yamlgraph/graph_loader.py +451 -0
  147. yamlgraph/map_compiler.py +150 -0
  148. yamlgraph/models/__init__.py +36 -0
  149. yamlgraph/models/graph_schema.py +181 -0
  150. yamlgraph/models/schemas.py +124 -0
  151. yamlgraph/models/state_builder.py +236 -0
  152. yamlgraph/node_factory.py +768 -0
  153. yamlgraph/routing.py +87 -0
  154. yamlgraph/schema_loader.py +240 -0
  155. yamlgraph/storage/__init__.py +20 -0
  156. yamlgraph/storage/checkpointer.py +72 -0
  157. yamlgraph/storage/checkpointer_factory.py +123 -0
  158. yamlgraph/storage/database.py +320 -0
  159. yamlgraph/storage/export.py +269 -0
  160. yamlgraph/tools/__init__.py +1 -0
  161. yamlgraph/tools/agent.py +320 -0
  162. yamlgraph/tools/graph_linter.py +388 -0
  163. yamlgraph/tools/langsmith_tools.py +125 -0
  164. yamlgraph/tools/nodes.py +126 -0
  165. yamlgraph/tools/python_tool.py +179 -0
  166. yamlgraph/tools/shell.py +205 -0
  167. yamlgraph/tools/websearch.py +242 -0
  168. yamlgraph/utils/__init__.py +48 -0
  169. yamlgraph/utils/conditions.py +157 -0
  170. yamlgraph/utils/expressions.py +245 -0
  171. yamlgraph/utils/json_extract.py +104 -0
  172. yamlgraph/utils/langsmith.py +416 -0
  173. yamlgraph/utils/llm_factory.py +118 -0
  174. yamlgraph/utils/llm_factory_async.py +105 -0
  175. yamlgraph/utils/logging.py +104 -0
  176. yamlgraph/utils/prompts.py +171 -0
  177. yamlgraph/utils/sanitize.py +98 -0
  178. yamlgraph/utils/template.py +102 -0
  179. yamlgraph/utils/validators.py +181 -0
  180. yamlgraph-0.3.9.dist-info/METADATA +1105 -0
  181. yamlgraph-0.3.9.dist-info/RECORD +185 -0
  182. yamlgraph-0.3.9.dist-info/WHEEL +5 -0
  183. yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
  184. yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
  185. yamlgraph-0.3.9.dist-info/top_level.txt +4 -0
@@ -0,0 +1,600 @@
1
+ """Unit tests for LangSmith utilities.
2
+
3
+ Tests for:
4
+ - share_run() - Create public share links
5
+ - read_run_shared_link() - Get existing share links
6
+ - get_client() - Client creation with env var handling
7
+ - is_tracing_enabled() - Tracing detection
8
+ """
9
+
10
+ import os
11
+ from unittest.mock import MagicMock, patch
12
+
13
+ from yamlgraph.utils.langsmith import (
14
+ get_client,
15
+ get_latest_run_id,
16
+ get_project_name,
17
+ is_tracing_enabled,
18
+ read_run_shared_link,
19
+ share_run,
20
+ )
21
+
22
+ # =============================================================================
23
+ # is_tracing_enabled() tests
24
+ # =============================================================================
25
+
26
+
27
+ class TestIsTracingEnabled:
28
+ """Tests for is_tracing_enabled()."""
29
+
30
+ def test_enabled_with_langchain_tracing_v2_true(self):
31
+ """LANGCHAIN_TRACING_V2=true enables tracing."""
32
+ with patch.dict(os.environ, {"LANGCHAIN_TRACING_V2": "true"}, clear=False):
33
+ # Need to remove LANGSMITH_TRACING if set
34
+ env = dict(os.environ)
35
+ env.pop("LANGSMITH_TRACING", None)
36
+ with patch.dict(os.environ, env, clear=True):
37
+ os.environ["LANGCHAIN_TRACING_V2"] = "true"
38
+ assert is_tracing_enabled() is True
39
+
40
+ def test_enabled_with_langsmith_tracing_true(self):
41
+ """LANGSMITH_TRACING=true enables tracing."""
42
+ with patch.dict(os.environ, {"LANGSMITH_TRACING": "true"}, clear=True):
43
+ assert is_tracing_enabled() is True
44
+
45
+ def test_disabled_when_no_env_vars(self):
46
+ """No tracing vars means disabled."""
47
+ with patch.dict(os.environ, {}, clear=True):
48
+ assert is_tracing_enabled() is False
49
+
50
+ def test_disabled_with_false_value(self):
51
+ """Explicit false value disables tracing."""
52
+ with patch.dict(os.environ, {"LANGCHAIN_TRACING_V2": "false"}, clear=True):
53
+ assert is_tracing_enabled() is False
54
+
55
+ def test_case_insensitive(self):
56
+ """TRUE, True, true all work."""
57
+ with patch.dict(os.environ, {"LANGSMITH_TRACING": "TRUE"}, clear=True):
58
+ assert is_tracing_enabled() is True
59
+
60
+
61
+ # =============================================================================
62
+ # get_project_name() tests
63
+ # =============================================================================
64
+
65
+
66
+ class TestGetProjectName:
67
+ """Tests for get_project_name()."""
68
+
69
+ def test_langchain_project(self):
70
+ """Returns LANGCHAIN_PROJECT when set."""
71
+ with patch.dict(os.environ, {"LANGCHAIN_PROJECT": "my-project"}, clear=True):
72
+ assert get_project_name() == "my-project"
73
+
74
+ def test_langsmith_project(self):
75
+ """Returns LANGSMITH_PROJECT when set."""
76
+ with patch.dict(os.environ, {"LANGSMITH_PROJECT": "other-project"}, clear=True):
77
+ assert get_project_name() == "other-project"
78
+
79
+ def test_langchain_takes_precedence(self):
80
+ """LANGCHAIN_PROJECT takes precedence over LANGSMITH_PROJECT."""
81
+ with patch.dict(
82
+ os.environ,
83
+ {"LANGCHAIN_PROJECT": "first", "LANGSMITH_PROJECT": "second"},
84
+ clear=True,
85
+ ):
86
+ assert get_project_name() == "first"
87
+
88
+ def test_default_value(self):
89
+ """Returns default when no env vars."""
90
+ with patch.dict(os.environ, {}, clear=True):
91
+ assert get_project_name() == "yamlgraph"
92
+
93
+
94
+ # =============================================================================
95
+ # get_client() tests
96
+ # =============================================================================
97
+
98
+
99
+ class TestGetClient:
100
+ """Tests for get_client()."""
101
+
102
+ def test_returns_none_without_api_key(self):
103
+ """No API key means no client."""
104
+ with patch.dict(os.environ, {}, clear=True):
105
+ assert get_client() is None
106
+
107
+ def test_creates_client_with_langchain_key(self):
108
+ """Creates client with LANGCHAIN_API_KEY."""
109
+ with patch.dict(
110
+ os.environ,
111
+ {"LANGCHAIN_API_KEY": "lsv2_test_key"},
112
+ clear=True,
113
+ ), patch("langsmith.Client") as mock_client:
114
+ result = get_client()
115
+ mock_client.assert_called_once()
116
+ assert result is not None
117
+
118
+ def test_creates_client_with_langsmith_key(self):
119
+ """Creates client with LANGSMITH_API_KEY."""
120
+ with patch.dict(
121
+ os.environ,
122
+ {"LANGSMITH_API_KEY": "lsv2_test_key"},
123
+ clear=True,
124
+ ), patch("langsmith.Client") as mock_client:
125
+ result = get_client()
126
+ mock_client.assert_called_once()
127
+ assert result is not None
128
+
129
+ def test_uses_custom_endpoint(self):
130
+ """Uses LANGSMITH_ENDPOINT if set."""
131
+ with patch.dict(
132
+ os.environ,
133
+ {
134
+ "LANGSMITH_API_KEY": "key",
135
+ "LANGSMITH_ENDPOINT": "https://eu.smith.langchain.com",
136
+ },
137
+ clear=True,
138
+ ), patch("langsmith.Client") as mock_client:
139
+ get_client()
140
+ mock_client.assert_called_with(
141
+ api_url="https://eu.smith.langchain.com",
142
+ api_key="key",
143
+ )
144
+
145
+ def test_returns_none_on_import_error(self):
146
+ """Returns None if langsmith not installed."""
147
+ # Verify graceful handling when Client constructor fails
148
+ with (
149
+ patch.dict(os.environ, {"LANGSMITH_API_KEY": "key"}, clear=True),
150
+ patch("langsmith.Client", side_effect=ImportError("No module")),
151
+ ):
152
+ # Should catch ImportError and return None
153
+ result = get_client()
154
+ assert result is None
155
+
156
+
157
+ # =============================================================================
158
+ # share_run() tests
159
+ # =============================================================================
160
+
161
+
162
+ class TestShareRun:
163
+ """Tests for share_run()."""
164
+
165
+ def test_returns_none_when_no_client(self):
166
+ """Returns None when client unavailable."""
167
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=None):
168
+ result = share_run("test-run-id")
169
+ assert result is None
170
+
171
+ def test_shares_provided_run_id(self):
172
+ """Shares the provided run ID."""
173
+ mock_client = MagicMock()
174
+ mock_client.share_run.return_value = "https://smith.langchain.com/public/abc123"
175
+
176
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
177
+ result = share_run("my-run-id")
178
+
179
+ mock_client.share_run.assert_called_once_with("my-run-id")
180
+ assert result == "https://smith.langchain.com/public/abc123"
181
+
182
+ def test_uses_latest_run_when_no_id(self):
183
+ """Gets latest run ID when not provided."""
184
+ mock_client = MagicMock()
185
+ mock_client.share_run.return_value = "https://share.url"
186
+
187
+ with (
188
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
189
+ patch(
190
+ "yamlgraph.utils.langsmith.get_latest_run_id",
191
+ return_value="latest-id",
192
+ ),
193
+ ):
194
+ result = share_run()
195
+
196
+ mock_client.share_run.assert_called_once_with("latest-id")
197
+ assert result == "https://share.url"
198
+
199
+ def test_returns_none_when_no_latest_run(self):
200
+ """Returns None when no latest run found."""
201
+ mock_client = MagicMock()
202
+
203
+ with (
204
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
205
+ patch(
206
+ "yamlgraph.utils.langsmith.get_latest_run_id",
207
+ return_value=None,
208
+ ),
209
+ ):
210
+ result = share_run()
211
+ assert result is None
212
+
213
+ def test_handles_exception_gracefully(self):
214
+ """Returns None on error (logs warning to stderr)."""
215
+ mock_client = MagicMock()
216
+ mock_client.share_run.side_effect = Exception("API error")
217
+
218
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
219
+ result = share_run("test-id")
220
+ assert result is None
221
+
222
+
223
+ # =============================================================================
224
+ # read_run_shared_link() tests
225
+ # =============================================================================
226
+
227
+
228
+ class TestReadRunSharedLink:
229
+ """Tests for read_run_shared_link()."""
230
+
231
+ def test_returns_none_when_no_client(self):
232
+ """Returns None when client unavailable."""
233
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=None):
234
+ result = read_run_shared_link("test-run-id")
235
+ assert result is None
236
+
237
+ def test_returns_existing_link(self):
238
+ """Returns existing share link."""
239
+ mock_client = MagicMock()
240
+ mock_client.read_run_shared_link.return_value = "https://existing.url"
241
+
242
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
243
+ result = read_run_shared_link("my-run-id")
244
+
245
+ mock_client.read_run_shared_link.assert_called_once_with("my-run-id")
246
+ assert result == "https://existing.url"
247
+
248
+ def test_returns_none_when_not_shared(self):
249
+ """Returns None when run not shared (exception)."""
250
+ mock_client = MagicMock()
251
+ mock_client.read_run_shared_link.side_effect = Exception("Not found")
252
+
253
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
254
+ result = read_run_shared_link("test-id")
255
+ assert result is None
256
+
257
+
258
+ # =============================================================================
259
+ # get_latest_run_id() tests
260
+ # =============================================================================
261
+
262
+
263
+ class TestGetLatestRunId:
264
+ """Tests for get_latest_run_id()."""
265
+
266
+ def test_returns_none_when_no_client(self):
267
+ """Returns None when client unavailable."""
268
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=None):
269
+ result = get_latest_run_id()
270
+ assert result is None
271
+
272
+ def test_returns_latest_run_id(self):
273
+ """Returns ID of most recent run."""
274
+ mock_run = MagicMock()
275
+ mock_run.id = "abc-123"
276
+
277
+ mock_client = MagicMock()
278
+ mock_client.list_runs.return_value = [mock_run]
279
+
280
+ with (
281
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
282
+ patch(
283
+ "yamlgraph.utils.langsmith.get_project_name",
284
+ return_value="test-project",
285
+ ),
286
+ ):
287
+ result = get_latest_run_id()
288
+
289
+ mock_client.list_runs.assert_called_once_with(
290
+ project_name="test-project", limit=1
291
+ )
292
+ assert result == "abc-123"
293
+
294
+ def test_returns_none_when_no_runs(self):
295
+ """Returns None when no runs found."""
296
+ mock_client = MagicMock()
297
+ mock_client.list_runs.return_value = []
298
+
299
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
300
+ result = get_latest_run_id()
301
+ assert result is None
302
+
303
+ def test_uses_provided_project_name(self):
304
+ """Uses provided project name."""
305
+ mock_run = MagicMock()
306
+ mock_run.id = "run-id"
307
+ mock_client = MagicMock()
308
+ mock_client.list_runs.return_value = [mock_run]
309
+
310
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
311
+ get_latest_run_id(project_name="custom-project")
312
+
313
+ mock_client.list_runs.assert_called_once_with(
314
+ project_name="custom-project", limit=1
315
+ )
316
+
317
+ def test_handles_exception_gracefully(self):
318
+ """Returns None on error (logs warning to stderr)."""
319
+ mock_client = MagicMock()
320
+ mock_client.list_runs.side_effect = Exception("API error")
321
+
322
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
323
+ result = get_latest_run_id()
324
+ assert result is None
325
+
326
+
327
+ # =============================================================================
328
+ # get_run_details() tests
329
+ # =============================================================================
330
+
331
+
332
+ class TestGetRunDetails:
333
+ """Tests for get_run_details()."""
334
+
335
+ def test_returns_none_when_no_client(self):
336
+ """Returns None when client unavailable."""
337
+ from yamlgraph.utils.langsmith import get_run_details
338
+
339
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=None):
340
+ result = get_run_details("test-run-id")
341
+ assert result is None
342
+
343
+ def test_returns_none_when_no_run_id_and_no_latest(self):
344
+ """Returns None when no run ID provided and no latest run."""
345
+ from yamlgraph.utils.langsmith import get_run_details
346
+
347
+ mock_client = MagicMock()
348
+ with (
349
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
350
+ patch(
351
+ "yamlgraph.utils.langsmith.get_latest_run_id", return_value=None
352
+ ),
353
+ ):
354
+ result = get_run_details()
355
+ assert result is None
356
+
357
+ def test_returns_run_details(self):
358
+ """Returns detailed run information."""
359
+ from datetime import datetime
360
+
361
+ from yamlgraph.utils.langsmith import get_run_details
362
+
363
+ mock_run = MagicMock()
364
+ mock_run.id = "run-123"
365
+ mock_run.name = "test_pipeline"
366
+ mock_run.status = "success"
367
+ mock_run.error = None
368
+ mock_run.start_time = datetime(2026, 1, 18, 10, 0, 0)
369
+ mock_run.end_time = datetime(2026, 1, 18, 10, 1, 0)
370
+ mock_run.inputs = {"topic": "AI"}
371
+ mock_run.outputs = {"result": "done"}
372
+ mock_run.run_type = "chain"
373
+
374
+ mock_client = MagicMock()
375
+ mock_client.read_run.return_value = mock_run
376
+
377
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
378
+ result = get_run_details("run-123")
379
+
380
+ assert result["id"] == "run-123"
381
+ assert result["name"] == "test_pipeline"
382
+ assert result["status"] == "success"
383
+ assert result["error"] is None
384
+ assert result["inputs"] == {"topic": "AI"}
385
+ assert result["outputs"] == {"result": "done"}
386
+ assert result["run_type"] == "chain"
387
+
388
+ def test_uses_latest_run_when_no_id(self):
389
+ """Uses latest run ID when not provided."""
390
+ from yamlgraph.utils.langsmith import get_run_details
391
+
392
+ mock_run = MagicMock()
393
+ mock_run.id = "latest-run"
394
+ mock_run.name = "latest"
395
+ mock_run.status = "success"
396
+ mock_run.error = None
397
+ mock_run.start_time = None
398
+ mock_run.end_time = None
399
+ mock_run.inputs = {}
400
+ mock_run.outputs = {}
401
+ mock_run.run_type = "chain"
402
+
403
+ mock_client = MagicMock()
404
+ mock_client.read_run.return_value = mock_run
405
+
406
+ with (
407
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
408
+ patch(
409
+ "yamlgraph.utils.langsmith.get_latest_run_id",
410
+ return_value="latest-run",
411
+ ),
412
+ ):
413
+ result = get_run_details()
414
+
415
+ mock_client.read_run.assert_called_once_with("latest-run")
416
+ assert result["id"] == "latest-run"
417
+
418
+ def test_handles_exception_gracefully(self):
419
+ """Returns None on error."""
420
+ from yamlgraph.utils.langsmith import get_run_details
421
+
422
+ mock_client = MagicMock()
423
+ mock_client.read_run.side_effect = Exception("API error")
424
+
425
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
426
+ result = get_run_details("test-id")
427
+ assert result is None
428
+
429
+
430
+ # =============================================================================
431
+ # get_run_errors() tests
432
+ # =============================================================================
433
+
434
+
435
+ class TestGetRunErrors:
436
+ """Tests for get_run_errors()."""
437
+
438
+ def test_returns_empty_list_when_no_client(self):
439
+ """Returns empty list when client unavailable."""
440
+ from yamlgraph.utils.langsmith import get_run_errors
441
+
442
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=None):
443
+ result = get_run_errors("test-run-id")
444
+ assert result == []
445
+
446
+ def test_returns_empty_list_when_no_run_id(self):
447
+ """Returns empty list when no run ID and no latest."""
448
+ from yamlgraph.utils.langsmith import get_run_errors
449
+
450
+ mock_client = MagicMock()
451
+ with (
452
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
453
+ patch(
454
+ "yamlgraph.utils.langsmith.get_latest_run_id", return_value=None
455
+ ),
456
+ ):
457
+ result = get_run_errors()
458
+ assert result == []
459
+
460
+ def test_returns_parent_run_error(self):
461
+ """Returns error from parent run."""
462
+ from yamlgraph.utils.langsmith import get_run_errors
463
+
464
+ mock_run = MagicMock()
465
+ mock_run.name = "parent_node"
466
+ mock_run.error = "Parent failed"
467
+ mock_run.run_type = "chain"
468
+
469
+ mock_client = MagicMock()
470
+ mock_client.read_run.return_value = mock_run
471
+ mock_client.list_runs.return_value = []
472
+
473
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
474
+ result = get_run_errors("run-123")
475
+
476
+ assert len(result) == 1
477
+ assert result[0]["node"] == "parent_node"
478
+ assert result[0]["error"] == "Parent failed"
479
+
480
+ def test_returns_child_run_errors(self):
481
+ """Returns errors from child runs."""
482
+ from yamlgraph.utils.langsmith import get_run_errors
483
+
484
+ mock_parent = MagicMock()
485
+ mock_parent.error = None
486
+
487
+ mock_child1 = MagicMock()
488
+ mock_child1.name = "generate"
489
+ mock_child1.error = "Generate failed"
490
+ mock_child1.run_type = "llm"
491
+
492
+ mock_child2 = MagicMock()
493
+ mock_child2.name = "analyze"
494
+ mock_child2.error = "Analyze failed"
495
+ mock_child2.run_type = "llm"
496
+
497
+ mock_client = MagicMock()
498
+ mock_client.read_run.return_value = mock_parent
499
+ mock_client.list_runs.return_value = [mock_child1, mock_child2]
500
+
501
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
502
+ result = get_run_errors("run-123")
503
+
504
+ assert len(result) == 2
505
+ assert result[0]["node"] == "generate"
506
+ assert result[1]["node"] == "analyze"
507
+
508
+ def test_handles_exception_gracefully(self):
509
+ """Returns empty list on error."""
510
+ from yamlgraph.utils.langsmith import get_run_errors
511
+
512
+ mock_client = MagicMock()
513
+ mock_client.read_run.side_effect = Exception("API error")
514
+
515
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
516
+ result = get_run_errors("test-id")
517
+ assert result == []
518
+
519
+
520
+ # =============================================================================
521
+ # get_failed_runs() tests
522
+ # =============================================================================
523
+
524
+
525
+ class TestGetFailedRuns:
526
+ """Tests for get_failed_runs()."""
527
+
528
+ def test_returns_empty_list_when_no_client(self):
529
+ """Returns empty list when client unavailable."""
530
+ from yamlgraph.utils.langsmith import get_failed_runs
531
+
532
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=None):
533
+ result = get_failed_runs()
534
+ assert result == []
535
+
536
+ def test_returns_failed_runs(self):
537
+ """Returns list of failed runs."""
538
+ from datetime import datetime
539
+
540
+ from yamlgraph.utils.langsmith import get_failed_runs
541
+
542
+ mock_run1 = MagicMock()
543
+ mock_run1.id = "run-1"
544
+ mock_run1.name = "pipeline_1"
545
+ mock_run1.error = "Error 1"
546
+ mock_run1.start_time = datetime(2026, 1, 18, 10, 0, 0)
547
+
548
+ mock_run2 = MagicMock()
549
+ mock_run2.id = "run-2"
550
+ mock_run2.name = "pipeline_2"
551
+ mock_run2.error = "Error 2"
552
+ mock_run2.start_time = datetime(2026, 1, 18, 11, 0, 0)
553
+
554
+ mock_client = MagicMock()
555
+ mock_client.list_runs.return_value = [mock_run1, mock_run2]
556
+
557
+ with (
558
+ patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client),
559
+ patch(
560
+ "yamlgraph.utils.langsmith.get_project_name",
561
+ return_value="test-project",
562
+ ),
563
+ ):
564
+ result = get_failed_runs(limit=5)
565
+
566
+ mock_client.list_runs.assert_called_once_with(
567
+ project_name="test-project",
568
+ error=True,
569
+ limit=5,
570
+ )
571
+ assert len(result) == 2
572
+ assert result[0]["id"] == "run-1"
573
+ assert result[0]["error"] == "Error 1"
574
+
575
+ def test_uses_provided_project_name(self):
576
+ """Uses provided project name."""
577
+ from yamlgraph.utils.langsmith import get_failed_runs
578
+
579
+ mock_client = MagicMock()
580
+ mock_client.list_runs.return_value = []
581
+
582
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
583
+ get_failed_runs(project_name="custom-project", limit=3)
584
+
585
+ mock_client.list_runs.assert_called_once_with(
586
+ project_name="custom-project",
587
+ error=True,
588
+ limit=3,
589
+ )
590
+
591
+ def test_handles_exception_gracefully(self):
592
+ """Returns empty list on error."""
593
+ from yamlgraph.utils.langsmith import get_failed_runs
594
+
595
+ mock_client = MagicMock()
596
+ mock_client.list_runs.side_effect = Exception("API error")
597
+
598
+ with patch("yamlgraph.utils.langsmith.get_client", return_value=mock_client):
599
+ result = get_failed_runs()
600
+ assert result == []