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
examples/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Examples package - sample projects demonstrating framework features."""
@@ -0,0 +1,5 @@
1
+ """Codegen Example - Implementation Agent.
2
+
3
+ A standalone example demonstrating YAMLGraph for code analysis
4
+ and implementation planning.
5
+ """
@@ -0,0 +1,13 @@
1
+ """Codegen models for structured LLM outputs."""
2
+
3
+ from examples.codegen.models.schemas import (
4
+ DiscoveryFindings,
5
+ DiscoveryPlan,
6
+ DiscoveryTask,
7
+ )
8
+
9
+ __all__ = [
10
+ "DiscoveryTask",
11
+ "DiscoveryPlan",
12
+ "DiscoveryFindings",
13
+ ]
@@ -0,0 +1,76 @@
1
+ """Schemas for impl-agent discovery and planning.
2
+
3
+ These models define the structure for todo-driven discovery workflow.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Literal
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class DiscoveryTask(BaseModel):
14
+ """A single discovery task for impl-agent.
15
+
16
+ Represents one investigation step in the discovery plan.
17
+ """
18
+
19
+ id: int = Field(description="Unique task identifier")
20
+ task: str = Field(description="Human-readable description of what to discover")
21
+ tool: str = Field(description="Name of the tool to call")
22
+ args: dict[str, Any] = Field(description="Arguments to pass to the tool")
23
+ rationale: str = Field(description="Why this discovery task matters")
24
+ status: Literal["pending", "done", "failed"] = Field(
25
+ default="pending",
26
+ description="Current status of the task",
27
+ )
28
+ priority: int = Field(
29
+ default=1,
30
+ description="Priority (1=high, 2=medium, 3=low)",
31
+ ge=1,
32
+ le=3,
33
+ )
34
+
35
+
36
+ class DiscoveryResult(BaseModel):
37
+ """Result of executing a discovery task.
38
+
39
+ Captures both successful results and error information.
40
+ """
41
+
42
+ task_id: int = Field(description="ID of the task that was executed")
43
+ tool: str = Field(description="Name of the tool that was called")
44
+ success: bool = Field(description="Whether the tool call succeeded")
45
+ result: Any = Field(
46
+ default=None,
47
+ description="Result from the tool (if successful)",
48
+ )
49
+ error: str | None = Field(
50
+ default=None,
51
+ description="Error message (if failed)",
52
+ )
53
+
54
+
55
+ class DiscoveryPlan(BaseModel):
56
+ """Complete discovery plan with ordered tasks.
57
+
58
+ Generated by the plan_discovery LLM node.
59
+ """
60
+
61
+ tasks: list[DiscoveryTask] = Field(
62
+ default_factory=list,
63
+ description="Ordered list of discovery tasks to execute",
64
+ )
65
+
66
+
67
+ class DiscoveryFindings(BaseModel):
68
+ """Accumulated discovery results.
69
+
70
+ Collected by the execute_discovery map node.
71
+ """
72
+
73
+ results: list[DiscoveryResult] = Field(
74
+ default_factory=list,
75
+ description="Results from all executed discovery tasks",
76
+ )
@@ -0,0 +1 @@
1
+ """Tests for codegen example."""
@@ -0,0 +1,235 @@
1
+ """Tests for AI helper tools (context compression, diff preview, similarity)."""
2
+
3
+ from examples.codegen.tools.ai_helpers import (
4
+ diff_preview,
5
+ find_similar_code,
6
+ summarize_module,
7
+ )
8
+
9
+
10
+ class TestSummarizeModule:
11
+ """Tests for summarize_module function."""
12
+
13
+ def test_summarizes_real_module(self):
14
+ """Summarizes a real module with classes and functions."""
15
+ result = summarize_module("yamlgraph/executor.py")
16
+
17
+ assert "error" not in result
18
+ assert "summary" in result
19
+ assert isinstance(result["summary"], str)
20
+ # Should be significantly shorter than original file
21
+ assert len(result["summary"]) < 2000
22
+
23
+ def test_includes_class_names(self):
24
+ """Summary includes class names."""
25
+ result = summarize_module("yamlgraph/models/schemas.py")
26
+
27
+ assert "error" not in result
28
+ # Should mention key classes
29
+ assert "class" in result["summary"].lower() or "Class" in result["summary"]
30
+
31
+ def test_includes_function_signatures(self):
32
+ """Summary includes function signatures (not bodies)."""
33
+ result = summarize_module("yamlgraph/executor.py")
34
+
35
+ assert "error" not in result
36
+ # Should have function defs
37
+ assert "def " in result["summary"]
38
+ # Bodies are stripped - should be much shorter than reading whole file
39
+
40
+ def test_includes_docstring(self):
41
+ """Summary includes module docstring."""
42
+ result = summarize_module("yamlgraph/executor.py")
43
+
44
+ assert "error" not in result
45
+ # Module docstring should be preserved
46
+ assert len(result["summary"]) > 50 # Not empty
47
+
48
+ def test_returns_error_for_invalid_file(self):
49
+ """Returns error for non-existent file."""
50
+ result = summarize_module("nonexistent/file.py")
51
+
52
+ assert "error" in result
53
+
54
+ def test_max_length_parameter(self):
55
+ """Respects max_length parameter."""
56
+ result = summarize_module("yamlgraph/executor.py", max_length=500)
57
+
58
+ assert "error" not in result
59
+ assert len(result["summary"]) <= 550 # Some tolerance
60
+
61
+ def test_returns_line_count(self):
62
+ """Returns original line count for context."""
63
+ result = summarize_module("yamlgraph/executor.py")
64
+
65
+ assert "error" not in result
66
+ assert "original_lines" in result
67
+ assert result["original_lines"] > 50
68
+
69
+
70
+ class TestDiffPreview:
71
+ """Tests for diff_preview function."""
72
+
73
+ def test_shows_add_diff(self):
74
+ """Shows diff for adding a line."""
75
+ result = diff_preview(
76
+ file_path="yamlgraph/__init__.py",
77
+ line=1,
78
+ action="ADD",
79
+ new_code="# New comment",
80
+ )
81
+
82
+ assert "error" not in result
83
+ assert "diff" in result
84
+ assert "+" in result["diff"] # Added line marker
85
+
86
+ def test_shows_modify_diff(self):
87
+ """Shows diff for modifying a line."""
88
+ # Read first line to modify it
89
+ result = diff_preview(
90
+ file_path="yamlgraph/__init__.py",
91
+ line=1,
92
+ action="MODIFY",
93
+ new_code='"""Modified docstring."""',
94
+ )
95
+
96
+ assert "error" not in result
97
+ assert "diff" in result
98
+ # Should show both old (-) and new (+)
99
+
100
+ def test_shows_delete_diff(self):
101
+ """Shows diff for deleting a line."""
102
+ result = diff_preview(
103
+ file_path="yamlgraph/__init__.py",
104
+ line=1,
105
+ action="DELETE",
106
+ new_code="",
107
+ )
108
+
109
+ assert "error" not in result
110
+ assert "diff" in result
111
+ assert "-" in result["diff"] # Deleted line marker
112
+
113
+ def test_returns_error_for_invalid_file(self):
114
+ """Returns error for non-existent file."""
115
+ result = diff_preview(
116
+ file_path="nonexistent/file.py",
117
+ line=1,
118
+ action="ADD",
119
+ new_code="test",
120
+ )
121
+
122
+ assert "error" in result
123
+
124
+ def test_returns_error_for_invalid_line(self):
125
+ """Returns error for line beyond file length."""
126
+ result = diff_preview(
127
+ file_path="yamlgraph/__init__.py",
128
+ line=99999,
129
+ action="MODIFY",
130
+ new_code="test",
131
+ )
132
+
133
+ assert "error" in result
134
+
135
+ def test_validates_syntax_of_result(self):
136
+ """Optionally validates syntax of resulting code."""
137
+ result = diff_preview(
138
+ file_path="yamlgraph/__init__.py",
139
+ line=1,
140
+ action="MODIFY",
141
+ new_code="def broken(:", # Invalid syntax - colon after open paren
142
+ validate_syntax=True,
143
+ )
144
+
145
+ # Should still return diff but flag syntax issue
146
+ assert "syntax_valid" in result
147
+ assert result["syntax_valid"] is False
148
+
149
+
150
+ class TestFindSimilarCode:
151
+ """Tests for find_similar_code function."""
152
+
153
+ def test_finds_similar_functions(self):
154
+ """Finds functions with similar structure."""
155
+ # Look for functions similar to git_blame (simple tool pattern)
156
+ result = find_similar_code(
157
+ file_path="examples/codegen/tools/git_tools.py",
158
+ symbol_name="git_blame",
159
+ project_path="examples/codegen/tools",
160
+ )
161
+
162
+ assert "error" not in result
163
+ assert "similar" in result
164
+ assert isinstance(result["similar"], list)
165
+
166
+ def test_returns_file_and_line(self):
167
+ """Each result includes file and line."""
168
+ result = find_similar_code(
169
+ file_path="examples/codegen/tools/git_tools.py",
170
+ symbol_name="git_blame",
171
+ project_path="examples/codegen/tools",
172
+ )
173
+
174
+ assert "error" not in result
175
+ for item in result["similar"]:
176
+ assert "file" in item
177
+ assert "name" in item
178
+ assert "line" in item
179
+
180
+ def test_includes_similarity_reason(self):
181
+ """Results explain why they're similar."""
182
+ result = find_similar_code(
183
+ file_path="examples/codegen/tools/git_tools.py",
184
+ symbol_name="git_blame",
185
+ project_path="examples/codegen/tools",
186
+ )
187
+
188
+ assert "error" not in result
189
+ for item in result["similar"]:
190
+ assert "reason" in item
191
+
192
+ def test_returns_error_for_invalid_file(self):
193
+ """Returns error for non-existent file."""
194
+ result = find_similar_code(
195
+ file_path="nonexistent/file.py",
196
+ symbol_name="foo",
197
+ project_path="yamlgraph",
198
+ )
199
+
200
+ assert "error" in result
201
+
202
+ def test_returns_error_for_invalid_symbol(self):
203
+ """Returns error for non-existent symbol."""
204
+ result = find_similar_code(
205
+ file_path="yamlgraph/executor.py",
206
+ symbol_name="nonexistent_function_xyz",
207
+ project_path="yamlgraph",
208
+ )
209
+
210
+ assert "error" in result
211
+
212
+ def test_max_results_parameter(self):
213
+ """Respects max_results parameter."""
214
+ result = find_similar_code(
215
+ file_path="examples/codegen/tools/git_tools.py",
216
+ symbol_name="git_blame",
217
+ project_path="examples/codegen/tools",
218
+ max_results=2,
219
+ )
220
+
221
+ assert "error" not in result
222
+ assert len(result["similar"]) <= 2
223
+
224
+ def test_includes_code_snippet(self):
225
+ """Results include code snippet."""
226
+ result = find_similar_code(
227
+ file_path="examples/codegen/tools/git_tools.py",
228
+ symbol_name="git_blame",
229
+ project_path="examples/codegen/tools",
230
+ )
231
+
232
+ assert "error" not in result
233
+ for item in result["similar"]:
234
+ assert "snippet" in item
235
+ assert "def " in item["snippet"]
@@ -0,0 +1,174 @@
1
+ """Tests for AST-based code analysis tools."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ from examples.codegen.tools.ast_analysis import get_module_structure
7
+
8
+
9
+ class TestGetModuleStructure:
10
+ """Tests for get_module_structure function."""
11
+
12
+ def test_extracts_classes_with_line_numbers(self):
13
+ """Classes include name, bases, methods, line numbers."""
14
+ # Use a known file in the project
15
+ result = get_module_structure("yamlgraph/models/schemas.py")
16
+
17
+ assert "error" not in result
18
+ assert "classes" in result
19
+ assert len(result["classes"]) > 0
20
+
21
+ # Each class should have required fields
22
+ for cls in result["classes"]:
23
+ assert "name" in cls
24
+ assert "line" in cls
25
+ assert "end_line" in cls
26
+ assert isinstance(cls["line"], int)
27
+ assert isinstance(cls["end_line"], int)
28
+ assert cls["end_line"] >= cls["line"]
29
+
30
+ def test_extracts_functions_with_signature(self):
31
+ """Functions include name, args, returns, decorators."""
32
+ result = get_module_structure("yamlgraph/executor.py")
33
+
34
+ assert "error" not in result
35
+ assert "functions" in result
36
+ assert len(result["functions"]) > 0
37
+
38
+ # Each function should have required fields
39
+ for func in result["functions"]:
40
+ assert "name" in func
41
+ assert "args" in func
42
+ assert "line" in func
43
+ assert "end_line" in func
44
+ assert isinstance(func["args"], list)
45
+
46
+ def test_extracts_imports(self):
47
+ """Import statements are extracted."""
48
+ result = get_module_structure("yamlgraph/executor.py")
49
+
50
+ assert "error" not in result
51
+ assert "imports" in result
52
+ assert len(result["imports"]) > 0
53
+
54
+ def test_extracts_module_docstring(self):
55
+ """Module-level docstring is extracted."""
56
+ result = get_module_structure("yamlgraph/executor.py")
57
+
58
+ assert "error" not in result
59
+ assert "docstring" in result
60
+ # executor.py should have a docstring
61
+ assert result["docstring"] is not None
62
+
63
+ def test_handles_missing_file(self):
64
+ """Returns error for non-existent file."""
65
+ result = get_module_structure("nonexistent_file_12345.py")
66
+
67
+ assert "error" in result
68
+ assert "not found" in result["error"].lower()
69
+
70
+ def test_handles_syntax_error(self):
71
+ """Returns error for file with syntax errors."""
72
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
73
+ f.write("def broken(\n") # Invalid syntax
74
+ temp_path = f.name
75
+
76
+ try:
77
+ result = get_module_structure(temp_path)
78
+ assert "error" in result
79
+ assert "syntax" in result["error"].lower()
80
+ finally:
81
+ Path(temp_path).unlink()
82
+
83
+ def test_includes_file_path_in_result(self):
84
+ """Result includes the file path."""
85
+ result = get_module_structure("yamlgraph/executor.py")
86
+
87
+ assert "error" not in result
88
+ assert "file" in result
89
+ assert "executor.py" in result["file"]
90
+
91
+ def test_extracts_class_methods(self):
92
+ """Class methods are listed."""
93
+ result = get_module_structure("yamlgraph/models/schemas.py")
94
+
95
+ assert "error" not in result
96
+ # At least some classes should have methods
97
+ # Note: Pydantic models may not have explicit methods
98
+ assert "methods" in result["classes"][0]
99
+
100
+ def test_extracts_class_bases(self):
101
+ """Class base classes are extracted."""
102
+ result = get_module_structure("yamlgraph/models/schemas.py")
103
+
104
+ assert "error" not in result
105
+ # Pydantic models inherit from BaseModel
106
+ pydantic_classes = [
107
+ c for c in result["classes"] if "BaseModel" in c.get("bases", [])
108
+ ]
109
+ assert len(pydantic_classes) > 0
110
+
111
+ def test_extracts_function_decorators(self):
112
+ """Function decorators are extracted."""
113
+ # Create a temp file with decorators
114
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
115
+ f.write("""
116
+ def simple_decorator(func):
117
+ return func
118
+
119
+ @simple_decorator
120
+ def decorated_function():
121
+ pass
122
+ """)
123
+ temp_path = f.name
124
+
125
+ try:
126
+ result = get_module_structure(temp_path)
127
+ assert "error" not in result
128
+ decorated = [
129
+ f for f in result["functions"] if f["name"] == "decorated_function"
130
+ ]
131
+ assert len(decorated) == 1
132
+ assert len(decorated[0]["decorators"]) > 0
133
+ finally:
134
+ Path(temp_path).unlink()
135
+
136
+ def test_extracts_function_return_type(self):
137
+ """Function return type annotations are extracted."""
138
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
139
+ f.write("""
140
+ def typed_function(x: int, y: str) -> bool:
141
+ return True
142
+ """)
143
+ temp_path = f.name
144
+
145
+ try:
146
+ result = get_module_structure(temp_path)
147
+ assert "error" not in result
148
+ typed = [f for f in result["functions"] if f["name"] == "typed_function"]
149
+ assert len(typed) == 1
150
+ assert typed[0]["returns"] == "bool"
151
+ assert typed[0]["args"] == ["x", "y"]
152
+ finally:
153
+ Path(temp_path).unlink()
154
+
155
+ def test_extracts_function_docstring(self):
156
+ """Function docstrings are extracted."""
157
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
158
+ f.write('''
159
+ def documented_function():
160
+ """This is the docstring."""
161
+ pass
162
+ ''')
163
+ temp_path = f.name
164
+
165
+ try:
166
+ result = get_module_structure(temp_path)
167
+ assert "error" not in result
168
+ documented = [
169
+ f for f in result["functions"] if f["name"] == "documented_function"
170
+ ]
171
+ assert len(documented) == 1
172
+ assert documented[0]["docstring"] == "This is the docstring."
173
+ finally:
174
+ Path(temp_path).unlink()
@@ -0,0 +1,134 @@
1
+ """Tests for code-analysis graph."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ class TestCodeAnalysisGraphStructure:
7
+ """Tests for code-analysis.yaml graph structure."""
8
+
9
+ def test_graph_file_exists(self):
10
+ """Graph file should exist."""
11
+ graph_path = Path("graphs/code-analysis.yaml")
12
+ assert graph_path.exists(), "graphs/code-analysis.yaml not found"
13
+
14
+ def test_graph_loads_successfully(self):
15
+ """Graph should load without errors."""
16
+ from yamlgraph.graph_loader import load_and_compile
17
+
18
+ graph = load_and_compile("graphs/code-analysis.yaml")
19
+ assert graph is not None
20
+
21
+ def test_graph_has_required_nodes(self):
22
+ """Graph should have run_analysis and generate_recommendations nodes."""
23
+ from yamlgraph.graph_loader import load_graph_config
24
+
25
+ config = load_graph_config("graphs/code-analysis.yaml")
26
+ assert "run_analysis" in config.nodes
27
+ assert "generate_recommendations" in config.nodes
28
+
29
+ def test_graph_has_analysis_tools(self):
30
+ """Graph should define analysis tools."""
31
+ from yamlgraph.graph_loader import load_graph_config
32
+
33
+ config = load_graph_config("graphs/code-analysis.yaml")
34
+ tools = config.tools
35
+
36
+ # Should have at least these tools
37
+ expected_tools = ["run_ruff", "run_tests", "run_bandit"]
38
+ for tool in expected_tools:
39
+ assert tool in tools, f"Missing tool: {tool}"
40
+
41
+
42
+ class TestCodeAnalysisPrompts:
43
+ """Tests for code-analysis prompts."""
44
+
45
+ def test_analyzer_prompt_exists(self):
46
+ """Analyzer prompt should exist."""
47
+ prompt_path = Path("prompts/code-analysis/analyzer.yaml")
48
+ assert prompt_path.exists(), "prompts/code-analysis/analyzer.yaml not found"
49
+
50
+ def test_recommend_prompt_exists(self):
51
+ """Recommend prompt should exist."""
52
+ prompt_path = Path("prompts/code-analysis/recommend.yaml")
53
+ assert prompt_path.exists(), "prompts/code-analysis/recommend.yaml not found"
54
+
55
+ def test_analyzer_prompt_has_system_and_user(self):
56
+ """Analyzer prompt should have system and user sections."""
57
+ import yaml
58
+
59
+ with open("prompts/code-analysis/analyzer.yaml") as f:
60
+ prompt = yaml.safe_load(f)
61
+
62
+ assert "system" in prompt, "Missing system prompt"
63
+ assert "user" in prompt, "Missing user prompt"
64
+
65
+ def test_recommend_prompt_has_system_and_user(self):
66
+ """Recommend prompt should have system and user sections."""
67
+ import yaml
68
+
69
+ with open("prompts/code-analysis/recommend.yaml") as f:
70
+ prompt = yaml.safe_load(f)
71
+
72
+ assert "system" in prompt, "Missing system prompt"
73
+ assert "user" in prompt, "Missing user prompt"
74
+
75
+
76
+ class TestCodeAnalysisTools:
77
+ """Tests for shell tool commands."""
78
+
79
+ def test_ruff_tool_command_valid(self):
80
+ """Ruff tool should have valid command structure."""
81
+ from yamlgraph.graph_loader import load_graph_config
82
+
83
+ config = load_graph_config("graphs/code-analysis.yaml")
84
+ ruff_tool = config.tools.get("run_ruff", {})
85
+
86
+ assert "command" in ruff_tool
87
+ assert "ruff" in ruff_tool["command"]
88
+
89
+ def test_tests_tool_command_valid(self):
90
+ """Tests tool should have valid command structure."""
91
+ from yamlgraph.graph_loader import load_graph_config
92
+
93
+ config = load_graph_config("graphs/code-analysis.yaml")
94
+ tests_tool = config.tools.get("run_tests", {})
95
+
96
+ assert "command" in tests_tool
97
+ assert "pytest" in tests_tool["command"]
98
+
99
+ def test_bandit_tool_command_valid(self):
100
+ """Bandit tool should have valid command structure."""
101
+ from yamlgraph.graph_loader import load_graph_config
102
+
103
+ config = load_graph_config("graphs/code-analysis.yaml")
104
+ bandit_tool = config.tools.get("run_bandit", {})
105
+
106
+ assert "command" in bandit_tool
107
+ assert "bandit" in bandit_tool["command"]
108
+
109
+
110
+ class TestCodeAnalysisCompilation:
111
+ """Tests for graph compilation."""
112
+
113
+ def test_graph_compiles_with_checkpointer(self):
114
+ """Graph should compile with SQLite checkpointer."""
115
+ from langgraph.checkpoint.sqlite import SqliteSaver
116
+
117
+ from yamlgraph.graph_loader import load_and_compile
118
+
119
+ graph = load_and_compile("graphs/code-analysis.yaml")
120
+
121
+ with SqliteSaver.from_conn_string(":memory:") as checkpointer:
122
+ compiled = graph.compile(checkpointer=checkpointer)
123
+ assert compiled is not None
124
+
125
+ def test_graph_has_entry_point(self):
126
+ """Graph should have START -> run_analysis edge."""
127
+ from yamlgraph.graph_loader import load_graph_config
128
+
129
+ config = load_graph_config("graphs/code-analysis.yaml")
130
+
131
+ # Find edge from START
132
+ start_edges = [e for e in config.edges if e.get("from") == "START"]
133
+ assert len(start_edges) > 0, "No edge from START"
134
+ assert start_edges[0]["to"] == "run_analysis"