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,403 @@
1
+ """Tests for Section 3: Self-Correction Loops (Reflexion).
2
+
3
+ TDD tests for expression conditions, loop tracking, and cyclic graphs.
4
+ """
5
+
6
+ from unittest.mock import MagicMock, patch
7
+
8
+ import pytest
9
+
10
+ # =============================================================================
11
+ # Test: Expression Condition Parsing
12
+ # =============================================================================
13
+
14
+
15
+ class TestExpressionConditions:
16
+ """Tests for condition expression evaluation."""
17
+
18
+ def test_evaluate_condition_exists(self):
19
+ """evaluate_condition function should exist."""
20
+ from yamlgraph.utils.conditions import evaluate_condition
21
+
22
+ assert callable(evaluate_condition)
23
+
24
+ def test_less_than_comparison(self):
25
+ """Evaluates 'score < 0.8' correctly."""
26
+ from yamlgraph.utils.conditions import evaluate_condition
27
+
28
+ state = {"score": 0.5}
29
+ assert evaluate_condition("score < 0.8", state) is True
30
+
31
+ state = {"score": 0.9}
32
+ assert evaluate_condition("score < 0.8", state) is False
33
+
34
+ def test_greater_than_comparison(self):
35
+ """Evaluates 'score > 0.5' correctly."""
36
+ from yamlgraph.utils.conditions import evaluate_condition
37
+
38
+ state = {"score": 0.7}
39
+ assert evaluate_condition("score > 0.5", state) is True
40
+
41
+ state = {"score": 0.3}
42
+ assert evaluate_condition("score > 0.5", state) is False
43
+
44
+ def test_less_than_or_equal(self):
45
+ """Evaluates 'score <= 0.8' correctly."""
46
+ from yamlgraph.utils.conditions import evaluate_condition
47
+
48
+ state = {"score": 0.8}
49
+ assert evaluate_condition("score <= 0.8", state) is True
50
+
51
+ state = {"score": 0.9}
52
+ assert evaluate_condition("score <= 0.8", state) is False
53
+
54
+ def test_greater_than_or_equal(self):
55
+ """Evaluates 'score >= 0.8' correctly."""
56
+ from yamlgraph.utils.conditions import evaluate_condition
57
+
58
+ state = {"score": 0.8}
59
+ assert evaluate_condition("score >= 0.8", state) is True
60
+
61
+ state = {"score": 0.7}
62
+ assert evaluate_condition("score >= 0.8", state) is False
63
+
64
+ def test_equality_comparison(self):
65
+ """Evaluates 'status == \"approved\"' correctly."""
66
+ from yamlgraph.utils.conditions import evaluate_condition
67
+
68
+ state = {"status": "approved"}
69
+ assert evaluate_condition('status == "approved"', state) is True
70
+
71
+ state = {"status": "pending"}
72
+ assert evaluate_condition('status == "approved"', state) is False
73
+
74
+ def test_inequality_comparison(self):
75
+ """Evaluates 'error != null' correctly."""
76
+ from yamlgraph.utils.conditions import evaluate_condition
77
+
78
+ state = {"error": "something"}
79
+ assert evaluate_condition("error != null", state) is True
80
+
81
+ state = {"error": None}
82
+ assert evaluate_condition("error != null", state) is False
83
+
84
+ def test_nested_attribute_access(self):
85
+ """Evaluates 'critique.score >= 0.8' from state."""
86
+ from yamlgraph.utils.conditions import evaluate_condition
87
+
88
+ # Using object with attribute
89
+ critique = MagicMock()
90
+ critique.score = 0.85
91
+ state = {"critique": critique}
92
+ assert evaluate_condition("critique.score >= 0.8", state) is True
93
+
94
+ critique.score = 0.7
95
+ assert evaluate_condition("critique.score >= 0.8", state) is False
96
+
97
+ def test_compound_and_condition(self):
98
+ """Evaluates 'score < 0.8 and iteration < 3'."""
99
+ from yamlgraph.utils.conditions import evaluate_condition
100
+
101
+ state = {"score": 0.5, "iteration": 2}
102
+ assert evaluate_condition("score < 0.8 and iteration < 3", state) is True
103
+
104
+ state = {"score": 0.9, "iteration": 2}
105
+ assert evaluate_condition("score < 0.8 and iteration < 3", state) is False
106
+
107
+ state = {"score": 0.5, "iteration": 5}
108
+ assert evaluate_condition("score < 0.8 and iteration < 3", state) is False
109
+
110
+ def test_compound_or_condition(self):
111
+ """Evaluates 'approved == true or override == true'."""
112
+ from yamlgraph.utils.conditions import evaluate_condition
113
+
114
+ state = {"approved": True, "override": False}
115
+ assert evaluate_condition("approved == true or override == true", state) is True
116
+
117
+ state = {"approved": False, "override": True}
118
+ assert evaluate_condition("approved == true or override == true", state) is True
119
+
120
+ state = {"approved": False, "override": False}
121
+ assert (
122
+ evaluate_condition("approved == true or override == true", state) is False
123
+ )
124
+
125
+ def test_invalid_expression_raises(self):
126
+ """Malformed expression raises ValueError."""
127
+ from yamlgraph.utils.conditions import evaluate_condition
128
+
129
+ with pytest.raises(ValueError):
130
+ evaluate_condition("score <<< 0.8", {})
131
+
132
+ def test_missing_attribute_returns_false(self):
133
+ """Missing attribute in state returns False gracefully."""
134
+ from yamlgraph.utils.conditions import evaluate_condition
135
+
136
+ state = {}
137
+ # Should not raise, should return False for missing attribute
138
+ assert evaluate_condition("score < 0.8", state) is False
139
+
140
+
141
+ # =============================================================================
142
+ # Test: Loop Tracking
143
+ # =============================================================================
144
+
145
+
146
+ class TestLoopTracking:
147
+ """Tests for loop iteration tracking."""
148
+
149
+ def test_state_has_loop_counts_field(self):
150
+ """Dynamic state should have _loop_counts field."""
151
+ from yamlgraph.models.state_builder import build_state_class
152
+
153
+ State = build_state_class({"nodes": {}})
154
+ # Should have _loop_counts in annotations
155
+ assert "_loop_counts" in State.__annotations__
156
+
157
+ # And work at runtime
158
+ state = {"_loop_counts": {"critique": 2}}
159
+ assert state["_loop_counts"]["critique"] == 2
160
+
161
+ def test_node_increments_loop_counter(self):
162
+ """Each node execution increments its counter in _loop_counts."""
163
+ from yamlgraph.node_factory import create_node_function
164
+
165
+ node_config = {
166
+ "prompt": "test_prompt",
167
+ "state_key": "result",
168
+ }
169
+
170
+ with patch("yamlgraph.node_factory.execute_prompt") as mock_execute:
171
+ mock_execute.return_value = "test result"
172
+
173
+ node_fn = create_node_function("critique", node_config, {})
174
+
175
+ # First call - should initialize counter
176
+ state = {"message": "test"}
177
+ result = node_fn(state)
178
+ assert result.get("_loop_counts", {}).get("critique") == 1
179
+
180
+ # Second call - should increment
181
+ state = {"message": "test", "_loop_counts": {"critique": 1}}
182
+ result = node_fn(state)
183
+ assert result.get("_loop_counts", {}).get("critique") == 2
184
+
185
+
186
+ # =============================================================================
187
+ # Test: Loop Limits Configuration
188
+ # =============================================================================
189
+
190
+
191
+ class TestLoopLimits:
192
+ """Tests for loop_limits configuration."""
193
+
194
+ def test_parses_loop_limits_from_yaml(self):
195
+ """GraphConfig parses loop_limits section."""
196
+ from yamlgraph.graph_loader import GraphConfig
197
+
198
+ config_dict = {
199
+ "version": "1.0",
200
+ "name": "test",
201
+ "nodes": {
202
+ "draft": {"prompt": "draft"},
203
+ "critique": {"prompt": "critique"},
204
+ },
205
+ "edges": [
206
+ {"from": "START", "to": "draft"},
207
+ {"from": "draft", "to": "critique"},
208
+ {"from": "critique", "to": "END"},
209
+ ],
210
+ "loop_limits": {
211
+ "critique": 3,
212
+ },
213
+ }
214
+ config = GraphConfig(config_dict)
215
+ assert config.loop_limits == {"critique": 3}
216
+
217
+ def test_loop_limits_defaults_to_empty(self):
218
+ """Missing loop_limits defaults to empty dict."""
219
+ from yamlgraph.graph_loader import GraphConfig
220
+
221
+ config_dict = {
222
+ "version": "1.0",
223
+ "name": "test",
224
+ "nodes": {"node1": {"prompt": "p1"}},
225
+ "edges": [{"from": "START", "to": "node1"}, {"from": "node1", "to": "END"}],
226
+ }
227
+ config = GraphConfig(config_dict)
228
+ assert config.loop_limits == {}
229
+
230
+ def test_node_checks_loop_limit(self):
231
+ """Node execution checks loop limit before running."""
232
+ from yamlgraph.node_factory import create_node_function
233
+
234
+ node_config = {
235
+ "prompt": "test_prompt",
236
+ "state_key": "result",
237
+ "loop_limit": 3, # Node-level limit
238
+ }
239
+
240
+ with patch("yamlgraph.node_factory.execute_prompt") as mock_execute:
241
+ mock_execute.return_value = "test result"
242
+
243
+ node_fn = create_node_function("critique", node_config, {})
244
+
245
+ # Under limit - should execute
246
+ state = {"_loop_counts": {"critique": 2}}
247
+ result = node_fn(state)
248
+ assert "result" in result
249
+
250
+ # At limit - should skip/terminate
251
+ state = {"_loop_counts": {"critique": 3}}
252
+ result = node_fn(state)
253
+ assert result.get("_loop_limit_reached") is True
254
+
255
+
256
+ # =============================================================================
257
+ # Test: Cyclic Edges
258
+ # =============================================================================
259
+
260
+
261
+ class TestCyclicEdges:
262
+ """Tests for cyclic graph support."""
263
+
264
+ def test_allows_backward_edges(self):
265
+ """Graph config allows edges pointing to earlier nodes."""
266
+ from yamlgraph.graph_loader import GraphConfig
267
+
268
+ config_dict = {
269
+ "version": "1.0",
270
+ "name": "test",
271
+ "nodes": {
272
+ "draft": {"prompt": "draft"},
273
+ "critique": {"prompt": "critique"},
274
+ "refine": {"prompt": "refine"},
275
+ },
276
+ "edges": [
277
+ {"from": "START", "to": "draft"},
278
+ {"from": "draft", "to": "critique"},
279
+ {
280
+ "from": "critique",
281
+ "to": "refine",
282
+ "condition": "critique.score < 0.8",
283
+ },
284
+ {"from": "critique", "to": "END", "condition": "critique.score >= 0.8"},
285
+ {"from": "refine", "to": "critique"}, # Backward edge (cycle)
286
+ ],
287
+ "loop_limits": {"critique": 3},
288
+ }
289
+ # Should not raise
290
+ config = GraphConfig(config_dict)
291
+ assert config is not None
292
+
293
+ def test_compiles_cyclic_graph(self):
294
+ """Cyclic graph compiles to StateGraph."""
295
+ from yamlgraph.graph_loader import GraphConfig, compile_graph
296
+
297
+ config_dict = {
298
+ "version": "1.0",
299
+ "name": "test",
300
+ "nodes": {
301
+ "draft": {"prompt": "draft", "state_key": "current_draft"},
302
+ "critique": {"prompt": "critique", "state_key": "critique"},
303
+ "refine": {"prompt": "refine", "state_key": "current_draft"},
304
+ },
305
+ "edges": [
306
+ {"from": "START", "to": "draft"},
307
+ {"from": "draft", "to": "critique"},
308
+ {
309
+ "from": "critique",
310
+ "to": "refine",
311
+ "condition": "critique.score < 0.8",
312
+ },
313
+ {"from": "critique", "to": "END", "condition": "critique.score >= 0.8"},
314
+ {"from": "refine", "to": "critique"}, # Cycle
315
+ ],
316
+ "loop_limits": {"critique": 3},
317
+ }
318
+ config = GraphConfig(config_dict)
319
+ graph = compile_graph(config)
320
+ assert graph is not None
321
+
322
+
323
+ # =============================================================================
324
+ # Test: Pydantic Models
325
+ # =============================================================================
326
+
327
+
328
+ class TestReflexionModels:
329
+ """Tests for DraftContent and Critique-like fixture models.
330
+
331
+ Note: Demo models were removed from yamlgraph.models in Section 10.
332
+ These tests use fixture models to prove the pattern still works.
333
+ """
334
+
335
+ def test_draft_content_model_exists(self):
336
+ """DraftContent-like fixture model can be created."""
337
+ from tests.conftest import FixtureDraftContent
338
+
339
+ assert FixtureDraftContent is not None
340
+
341
+ def test_draft_content_fields(self):
342
+ """DraftContent-like model has content and version fields."""
343
+ from tests.conftest import FixtureDraftContent
344
+
345
+ draft = FixtureDraftContent(content="Test essay", version=1)
346
+ assert draft.content == "Test essay"
347
+ assert draft.version == 1
348
+
349
+ def test_critique_model_exists(self):
350
+ """Critique-like fixture model can be created."""
351
+ from tests.conftest import FixtureCritique
352
+
353
+ assert FixtureCritique is not None
354
+
355
+ def test_critique_fields(self):
356
+ """Critique-like model has score, feedback, issues, should_refine fields."""
357
+ from tests.conftest import FixtureCritique
358
+
359
+ critique = FixtureCritique(
360
+ score=0.75,
361
+ feedback="Improve transitions",
362
+ issues=["Weak intro", "No conclusion"],
363
+ should_refine=True,
364
+ )
365
+ assert critique.score == 0.75
366
+ assert critique.feedback == "Improve transitions"
367
+ assert len(critique.issues) == 2
368
+ assert critique.should_refine is True
369
+
370
+
371
+ # =============================================================================
372
+ # Test: Reflexion Demo Graph
373
+ # =============================================================================
374
+
375
+
376
+ class TestReflexionDemoGraph:
377
+ """Tests for the reflexion-demo.yaml graph."""
378
+
379
+ def test_demo_graph_loads(self):
380
+ """reflexion-demo.yaml loads without error."""
381
+ from yamlgraph.graph_loader import load_graph_config
382
+
383
+ config = load_graph_config("graphs/reflexion-demo.yaml")
384
+ assert config.name == "reflexion-demo"
385
+ assert "draft" in config.nodes
386
+ assert "critique" in config.nodes
387
+ assert "refine" in config.nodes
388
+
389
+ def test_demo_graph_has_loop_limits(self):
390
+ """reflexion-demo.yaml has loop_limits configured."""
391
+ from yamlgraph.graph_loader import load_graph_config
392
+
393
+ config = load_graph_config("graphs/reflexion-demo.yaml")
394
+ assert "critique" in config.loop_limits
395
+ assert config.loop_limits["critique"] >= 3
396
+
397
+ def test_demo_graph_compiles(self):
398
+ """reflexion-demo.yaml compiles to StateGraph."""
399
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
400
+
401
+ config = load_graph_config("graphs/reflexion-demo.yaml")
402
+ graph = compile_graph(config)
403
+ assert graph is not None
@@ -0,0 +1,144 @@
1
+ """Tests for type: map node functionality."""
2
+
3
+ from unittest.mock import MagicMock
4
+
5
+ import pytest
6
+
7
+ from yamlgraph.map_compiler import compile_map_node, wrap_for_reducer
8
+
9
+
10
+ class TestWrapForReducer:
11
+ """Tests for wrap_for_reducer helper."""
12
+
13
+ def test_wraps_result_in_list(self):
14
+ """Wrap node output for reducer aggregation."""
15
+
16
+ def simple_node(state: dict) -> dict:
17
+ return {"result": state["item"] * 2}
18
+
19
+ wrapped = wrap_for_reducer(simple_node, "collected", "result")
20
+ result = wrapped({"item": 5})
21
+
22
+ assert result == {"collected": [10]}
23
+
24
+ def test_preserves_map_index(self):
25
+ """Preserve _map_index in wrapped output."""
26
+
27
+ def node_fn(state: dict) -> dict:
28
+ return {"data": state["value"]}
29
+
30
+ wrapped = wrap_for_reducer(node_fn, "results", "data")
31
+ result = wrapped({"value": "test", "_map_index": 2})
32
+
33
+ assert result == {"results": [{"_map_index": 2, "value": "test"}]}
34
+
35
+ def test_extracts_state_key(self):
36
+ """Extract specific state_key from node result."""
37
+
38
+ def node_fn(state: dict) -> dict:
39
+ return {"frame_data": {"before": "a", "after": "b"}, "other": "ignore"}
40
+
41
+ wrapped = wrap_for_reducer(node_fn, "frames", "frame_data")
42
+ result = wrapped({})
43
+
44
+ assert result == {"frames": [{"before": "a", "after": "b"}]}
45
+
46
+
47
+ class TestCompileMapNode:
48
+ """Tests for compile_map_node function."""
49
+
50
+ def test_creates_map_edge_function(self):
51
+ """compile_map_node returns a map edge function."""
52
+ config = {
53
+ "over": "{items}",
54
+ "as": "item",
55
+ "collect": "results",
56
+ "node": {"type": "llm", "prompt": "test", "state_key": "result"},
57
+ }
58
+ builder = MagicMock()
59
+ defaults = {}
60
+
61
+ map_edge, sub_node_name = compile_map_node("expand", config, builder, defaults)
62
+
63
+ # Should return callable and sub-node name
64
+ assert callable(map_edge)
65
+ assert sub_node_name == "_map_expand_sub"
66
+
67
+ def test_map_edge_returns_send_list(self):
68
+ """Map edge function returns list of Send objects."""
69
+ from langgraph.types import Send
70
+
71
+ config = {
72
+ "over": "{items}",
73
+ "as": "item",
74
+ "collect": "results",
75
+ "node": {"type": "llm", "prompt": "test", "state_key": "result"},
76
+ }
77
+ builder = MagicMock()
78
+ defaults = {}
79
+
80
+ map_edge, sub_node_name = compile_map_node("expand", config, builder, defaults)
81
+
82
+ state = {"items": ["a", "b", "c"]}
83
+ sends = map_edge(state)
84
+
85
+ assert len(sends) == 3
86
+ assert all(isinstance(s, Send) for s in sends)
87
+ assert sends[0].node == sub_node_name
88
+ assert sends[0].arg["item"] == "a"
89
+ assert sends[0].arg["_map_index"] == 0
90
+ assert sends[1].arg["item"] == "b"
91
+ assert sends[1].arg["_map_index"] == 1
92
+
93
+ def test_map_edge_empty_list(self):
94
+ """Empty list returns empty Send list."""
95
+ config = {
96
+ "over": "{items}",
97
+ "as": "item",
98
+ "collect": "results",
99
+ "node": {"type": "llm", "prompt": "test", "state_key": "result"},
100
+ }
101
+ builder = MagicMock()
102
+ defaults = {}
103
+
104
+ map_edge, _ = compile_map_node("expand", config, builder, defaults)
105
+
106
+ state = {"items": []}
107
+ sends = map_edge(state)
108
+
109
+ assert sends == []
110
+
111
+ def test_adds_wrapped_sub_node_to_builder(self):
112
+ """compile_map_node adds wrapped sub-node to builder."""
113
+ config = {
114
+ "over": "{items}",
115
+ "as": "item",
116
+ "collect": "results",
117
+ "node": {"type": "llm", "prompt": "test", "state_key": "result"},
118
+ }
119
+ builder = MagicMock()
120
+ defaults = {}
121
+
122
+ compile_map_node("expand", config, builder, defaults)
123
+
124
+ # Should call builder.add_node
125
+ builder.add_node.assert_called_once()
126
+ call_args = builder.add_node.call_args
127
+ assert call_args[0][0] == "_map_expand_sub"
128
+
129
+ def test_validates_over_is_list(self):
130
+ """Map edge validates that 'over' resolves to a list."""
131
+ config = {
132
+ "over": "{not_a_list}",
133
+ "as": "item",
134
+ "collect": "results",
135
+ "node": {"type": "llm", "prompt": "test", "state_key": "result"},
136
+ }
137
+ builder = MagicMock()
138
+ defaults = {}
139
+
140
+ map_edge, _ = compile_map_node("expand", config, builder, defaults)
141
+
142
+ state = {"not_a_list": "string"}
143
+ with pytest.raises(TypeError, match="must resolve to list"):
144
+ map_edge(state)
@@ -0,0 +1,56 @@
1
+ """Test that 'backward compatibility' markers are cleaned up in source code.
2
+
3
+ Per project guidelines (.github/copilot-instructions.md):
4
+ "Term 'backward compatibility' is a key indicator for a refactoring need."
5
+
6
+ This test fails if any Python source files contain backward compatibility markers,
7
+ ensuring deprecated code gets cleaned up rather than accumulating.
8
+ """
9
+
10
+ import subprocess
11
+ from pathlib import Path
12
+
13
+
14
+ class TestNoBackwardCompatibilityMarkers:
15
+ """Ensure no backward compatibility markers exist in source code."""
16
+
17
+ def test_no_backward_compat_in_yamlgraph_source(self):
18
+ """Source files should not contain 'backward compatibility' markers.
19
+
20
+ Allowed exceptions:
21
+ - deprecation.py: Documents the DeprecationError pattern
22
+ - Tests in this file
23
+ """
24
+ project_root = Path(__file__).parent.parent.parent
25
+ yamlgraph_dir = project_root / "yamlgraph"
26
+
27
+ result = subprocess.run(
28
+ [
29
+ "grep",
30
+ "-rn",
31
+ "-i",
32
+ "backward compatib",
33
+ str(yamlgraph_dir),
34
+ "--include=*.py",
35
+ ],
36
+ capture_output=True,
37
+ text=True,
38
+ )
39
+
40
+ if result.returncode == 0: # Found matches
41
+ lines = result.stdout.strip().split("\n")
42
+ # Filter out allowed files
43
+ violations = [
44
+ line
45
+ for line in lines
46
+ if "deprecation.py" not in line # Pattern documentation
47
+ ]
48
+
49
+ if violations:
50
+ msg = (
51
+ "Found 'backward compatibility' markers in source code.\n"
52
+ "Per guidelines, this signals refactoring need.\n"
53
+ "Clean up deprecated code or move to deprecation.py.\n\n"
54
+ "Violations:\n" + "\n".join(f" {v}" for v in violations)
55
+ )
56
+ raise AssertionError(msg)