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,295 @@
1
+ """Integration tests for subgraph functionality.
2
+
3
+ Tests end-to-end subgraph execution with mocked LLM calls.
4
+ """
5
+
6
+ from pathlib import Path
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+
12
+ @pytest.fixture
13
+ def subgraph_graphs(tmp_path: Path) -> tuple[Path, Path]:
14
+ """Create parent and child graph files for testing."""
15
+ # Create prompts directory
16
+ prompts_dir = tmp_path / "prompts"
17
+ prompts_dir.mkdir()
18
+
19
+ # Create child prompt
20
+ child_prompt = prompts_dir / "child" / "process.yaml"
21
+ child_prompt.parent.mkdir()
22
+ child_prompt.write_text(
23
+ """
24
+ system: You are a processor.
25
+ user: Process this: {input_text}
26
+ """
27
+ )
28
+
29
+ # Create parent prompts
30
+ parent_prompt_dir = prompts_dir / "parent"
31
+ parent_prompt_dir.mkdir()
32
+ (parent_prompt_dir / "prepare.yaml").write_text(
33
+ """
34
+ system: You are a preparer.
35
+ user: Prepare this: {raw_text}
36
+ """
37
+ )
38
+ (parent_prompt_dir / "finalize.yaml").write_text(
39
+ """
40
+ system: You are a finalizer.
41
+ user: Finalize this: {processed}
42
+ """
43
+ )
44
+
45
+ # Create child subgraph
46
+ subgraphs_dir = tmp_path / "graphs" / "subgraphs"
47
+ subgraphs_dir.mkdir(parents=True)
48
+ child_graph = subgraphs_dir / "processor.yaml"
49
+ child_graph.write_text(
50
+ """
51
+ version: "1.0"
52
+ name: processor
53
+ state:
54
+ input_text: str
55
+ output_text: str
56
+ nodes:
57
+ process:
58
+ type: llm
59
+ prompt: child/process
60
+ state_key: output_text
61
+ edges:
62
+ - {from: START, to: process}
63
+ - {from: process, to: END}
64
+ """
65
+ )
66
+
67
+ # Create parent graph
68
+ parent_graph = tmp_path / "graphs" / "parent.yaml"
69
+ parent_graph.write_text(
70
+ """
71
+ version: "1.0"
72
+ name: parent
73
+ state:
74
+ raw_text: str
75
+ prepared: str
76
+ processed: str
77
+ final: str
78
+ nodes:
79
+ prepare:
80
+ type: llm
81
+ prompt: parent/prepare
82
+ state_key: prepared
83
+ process:
84
+ type: subgraph
85
+ mode: invoke
86
+ graph: subgraphs/processor.yaml
87
+ input_mapping:
88
+ prepared: input_text
89
+ output_mapping:
90
+ processed: output_text
91
+ finalize:
92
+ type: llm
93
+ prompt: parent/finalize
94
+ state_key: final
95
+ edges:
96
+ - {from: START, to: prepare}
97
+ - {from: prepare, to: process}
98
+ - {from: process, to: finalize}
99
+ - {from: finalize, to: END}
100
+ """
101
+ )
102
+
103
+ return parent_graph, child_graph
104
+
105
+
106
+ class TestSubgraphIntegration:
107
+ """End-to-end subgraph tests with mocked LLM."""
108
+
109
+ def test_runs_parent_to_subgraph_to_parent(self, subgraph_graphs, monkeypatch):
110
+ """Runs parent → subgraph → parent flow successfully."""
111
+
112
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
113
+
114
+ parent_graph, _ = subgraph_graphs
115
+ prompts_dir = parent_graph.parent.parent / "prompts"
116
+
117
+ # Set prompts directory
118
+ monkeypatch.setenv("YAMLGRAPH_PROMPTS_DIR", str(prompts_dir))
119
+
120
+ # Mock execute_prompt to return predictable results
121
+ call_count = {"count": 0}
122
+
123
+ def mock_execute(prompt_name, **kwargs):
124
+ call_count["count"] += 1
125
+ if "prepare" in prompt_name:
126
+ return "prepared text"
127
+ elif "process" in prompt_name:
128
+ return "processed text"
129
+ elif "finalize" in prompt_name:
130
+ return "final text"
131
+ return f"mocked response for {prompt_name}"
132
+
133
+ with patch("yamlgraph.node_factory.execute_prompt", side_effect=mock_execute):
134
+ config = load_graph_config(parent_graph)
135
+ graph = compile_graph(config)
136
+ compiled = graph.compile()
137
+
138
+ result = compiled.invoke({"raw_text": "test input"})
139
+
140
+ # Verify all nodes ran
141
+ assert result["prepared"] == "prepared text"
142
+ assert result["processed"] == "processed text"
143
+ assert result["final"] == "final text"
144
+ assert call_count["count"] == 3 # prepare + process (subgraph) + finalize
145
+
146
+ def test_subgraph_state_mapping_works(self, subgraph_graphs, monkeypatch):
147
+ """Input/output mapping correctly transforms state."""
148
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
149
+
150
+ parent_graph, _ = subgraph_graphs
151
+ prompts_dir = parent_graph.parent.parent / "prompts"
152
+ monkeypatch.setenv("YAMLGRAPH_PROMPTS_DIR", str(prompts_dir))
153
+
154
+ captured_inputs = {}
155
+
156
+ def mock_execute(prompt_name, **kwargs):
157
+ captured_inputs[prompt_name] = kwargs.get("variables", {})
158
+ if "prepare" in prompt_name:
159
+ return "PREPARED"
160
+ elif "process" in prompt_name:
161
+ # This should receive input_text (mapped from prepared)
162
+ return "PROCESSED"
163
+ elif "finalize" in prompt_name:
164
+ return "FINAL"
165
+ return "mock"
166
+
167
+ with patch("yamlgraph.node_factory.execute_prompt", side_effect=mock_execute):
168
+ config = load_graph_config(parent_graph)
169
+ graph = compile_graph(config)
170
+ compiled = graph.compile()
171
+
172
+ result = compiled.invoke({"raw_text": "original"})
173
+
174
+ # Check that subgraph received mapped input
175
+ assert "child/process" in captured_inputs
176
+ # The subgraph should have input_text (mapped from parent's prepared)
177
+ assert captured_inputs["child/process"].get("input_text") == "PREPARED"
178
+
179
+ # Check output was mapped back
180
+ assert result["processed"] == "PROCESSED"
181
+
182
+ def test_nested_subgraphs(self, tmp_path, monkeypatch):
183
+ """Supports subgraph within subgraph (2 levels deep)."""
184
+ prompts_dir = tmp_path / "prompts"
185
+ prompts_dir.mkdir()
186
+
187
+ # Level 2 (deepest)
188
+ (prompts_dir / "level2").mkdir()
189
+ (prompts_dir / "level2" / "process.yaml").write_text(
190
+ "system: L2\nuser: {data}" # Use 'data' to avoid skip
191
+ )
192
+
193
+ # Level 1
194
+ (prompts_dir / "level1").mkdir()
195
+ (prompts_dir / "level1" / "pre.yaml").write_text(
196
+ "system: L1\nuser: {input}" # Use 'input' to avoid skip
197
+ )
198
+
199
+ graphs_dir = tmp_path / "graphs"
200
+ graphs_dir.mkdir()
201
+ (graphs_dir / "subgraphs").mkdir()
202
+
203
+ # Level 2 graph
204
+ (graphs_dir / "subgraphs" / "level2.yaml").write_text(
205
+ """
206
+ version: "1.0"
207
+ name: level2
208
+ state:
209
+ data: str
210
+ output: str
211
+ nodes:
212
+ work:
213
+ type: llm
214
+ prompt: level2/process
215
+ state_key: output
216
+ edges:
217
+ - {from: START, to: work}
218
+ - {from: work, to: END}
219
+ """
220
+ )
221
+
222
+ # Level 1 graph (calls level 2)
223
+ (graphs_dir / "subgraphs" / "level1.yaml").write_text(
224
+ """
225
+ version: "1.0"
226
+ name: level1
227
+ state:
228
+ input: str
229
+ prepared: str
230
+ output: str
231
+ nodes:
232
+ pre:
233
+ type: llm
234
+ prompt: level1/pre
235
+ state_key: prepared
236
+ nested:
237
+ type: subgraph
238
+ mode: invoke
239
+ graph: level2.yaml
240
+ input_mapping:
241
+ prepared: data
242
+ output_mapping:
243
+ output: output
244
+ edges:
245
+ - {from: START, to: pre}
246
+ - {from: pre, to: nested}
247
+ - {from: nested, to: END}
248
+ """
249
+ )
250
+
251
+ # Root graph (calls level 1)
252
+ root = graphs_dir / "root.yaml"
253
+ root.write_text(
254
+ """
255
+ version: "1.0"
256
+ name: root
257
+ state:
258
+ start: str
259
+ result: str
260
+ nodes:
261
+ delegate:
262
+ type: subgraph
263
+ mode: invoke
264
+ graph: subgraphs/level1.yaml
265
+ input_mapping:
266
+ start: input
267
+ output_mapping:
268
+ result: output
269
+ edges:
270
+ - {from: START, to: delegate}
271
+ - {from: delegate, to: END}
272
+ """
273
+ )
274
+
275
+ monkeypatch.setenv("YAMLGRAPH_PROMPTS_DIR", str(prompts_dir))
276
+
277
+ call_sequence = []
278
+
279
+ def mock_execute(prompt_name, **kwargs):
280
+ call_sequence.append(prompt_name)
281
+ return f"result from {prompt_name}"
282
+
283
+ with patch("yamlgraph.node_factory.execute_prompt", side_effect=mock_execute):
284
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
285
+
286
+ config = load_graph_config(root)
287
+ graph = compile_graph(config)
288
+ compiled = graph.compile()
289
+
290
+ result = compiled.invoke({"start": "hello"})
291
+
292
+ # Both levels should have executed
293
+ assert "level1/pre" in call_sequence
294
+ assert "level2/process" in call_sequence
295
+ assert result["result"] == "result from level2/process"
@@ -0,0 +1,106 @@
1
+ """Integration test for FR-006: interrupt_output_mapping limitation.
2
+
3
+ This test demonstrates the known limitation where interrupt_output_mapping
4
+ cannot expose child state when the subgraph hits an interrupt, because
5
+ LangGraph's interrupt mechanism uses exceptions that bypass the mapping code.
6
+
7
+ See: docs/subgraph-interrupt-bug.md
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+
15
+ class TestSubgraphInterruptMapping:
16
+ """Tests for FR-006 interrupt_output_mapping with real graphs."""
17
+
18
+ @pytest.fixture
19
+ def parent_graph_path(self) -> Path:
20
+ """Path to the parent graph with interrupt_output_mapping."""
21
+ return Path(__file__).parent.parent.parent / "graphs" / "interrupt-parent.yaml"
22
+
23
+ @pytest.fixture
24
+ def compiled_graph(self, parent_graph_path: Path):
25
+ """Compile the parent graph with checkpointer."""
26
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
27
+
28
+ config = load_graph_config(parent_graph_path)
29
+ state_graph = compile_graph(config)
30
+
31
+ # Use memory checkpointer for testing
32
+ from langgraph.checkpoint.memory import MemorySaver
33
+ checkpointer = MemorySaver()
34
+
35
+ return state_graph.compile(checkpointer=checkpointer)
36
+
37
+ def test_interrupt_output_mapping_surfaces_child_state(self, compiled_graph):
38
+ """FR-006: Parent should see child state when subgraph is interrupted.
39
+
40
+ EXPECTED TO FAIL: This test documents the current limitation.
41
+ When it passes, FR-006 is truly fixed.
42
+ """
43
+ # Run parent graph - child will hit interrupt
44
+ config = {"configurable": {"thread_id": "test-fr006"}}
45
+ result = compiled_graph.invoke({"user_input": "hello"}, config)
46
+
47
+ # Verify we hit the interrupt
48
+ assert "__interrupt__" in result, "Expected graph to be interrupted"
49
+
50
+ # FR-006 EXPECTATION: child state should be mapped to parent
51
+ # This currently FAILS because interrupt exception bypasses mapping
52
+ assert "child_phase" in result, (
53
+ "FR-006 LIMITATION: child_phase not in result. "
54
+ "interrupt_output_mapping is bypassed by LangGraph's exception mechanism. "
55
+ "See docs/subgraph-interrupt-bug.md"
56
+ )
57
+ assert result.get("child_phase") == "processing", (
58
+ "Expected child_phase='processing' from child graph"
59
+ )
60
+ assert result.get("child_data") == "partial result from child", (
61
+ "Expected child_data from interrupt_output_mapping"
62
+ )
63
+
64
+ def test_output_mapping_works_on_completion(self, compiled_graph):
65
+ """Verify output_mapping works when subgraph completes normally.
66
+
67
+ This test should PASS - it resumes the interrupt and completes.
68
+ """
69
+ from langgraph.types import Command
70
+
71
+ config = {"configurable": {"thread_id": "test-completion"}}
72
+
73
+ # First run - hits interrupt
74
+ result = compiled_graph.invoke({"user_input": "hello"}, config)
75
+ assert "__interrupt__" in result
76
+
77
+ # Resume with user answer
78
+ result = compiled_graph.invoke(Command(resume="my answer"), config)
79
+
80
+ # After completion, output_mapping should work
81
+ # Note: The 'done' node sets final_result to 'all done'
82
+ assert result.get("final_result") == "all done", (
83
+ "Expected final_result from 'done' passthrough node"
84
+ )
85
+
86
+ def test_get_state_can_access_child_state(self, compiled_graph):
87
+ """Workaround: Use get_state() to access child state after interrupt.
88
+
89
+ This test documents the workaround for the FR-006 limitation.
90
+ """
91
+ config = {"configurable": {"thread_id": "test-workaround"}}
92
+
93
+ # Run until interrupt
94
+ result = compiled_graph.invoke({"user_input": "hello"}, config)
95
+ assert "__interrupt__" in result
96
+
97
+ # Workaround: access state via checkpointer
98
+ state_snapshot = compiled_graph.get_state(config)
99
+
100
+ # The parent state should be accessible
101
+ assert state_snapshot is not None
102
+ assert "values" in dir(state_snapshot) or hasattr(state_snapshot, "values")
103
+
104
+ # Note: Child state may be in nested subgraph thread
105
+ # This depends on checkpointer implementation
106
+ print(f"State snapshot values: {state_snapshot.values}")
tests/unit/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Unit tests for yamlgraph."""