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,112 @@
1
+ """Tests for git analysis tools."""
2
+
3
+ from examples.codegen.tools.git_tools import git_blame, git_log
4
+
5
+
6
+ class TestGitBlame:
7
+ """Tests for git_blame function."""
8
+
9
+ def test_returns_author_for_valid_line(self):
10
+ """Returns author info for a valid file/line."""
11
+ # Use a known file in the project
12
+ result = git_blame("yamlgraph/__init__.py", 1)
13
+
14
+ assert "error" not in result
15
+ assert "author" in result
16
+ assert "date" in result
17
+ assert "commit" in result
18
+ assert isinstance(result["author"], str)
19
+ assert len(result["author"]) > 0
20
+
21
+ def test_returns_commit_message(self):
22
+ """Returns commit message summary."""
23
+ result = git_blame("yamlgraph/__init__.py", 1)
24
+
25
+ assert "error" not in result
26
+ assert "summary" in result
27
+ assert isinstance(result["summary"], str)
28
+
29
+ def test_returns_error_for_invalid_file(self):
30
+ """Returns error for non-existent file."""
31
+ result = git_blame("nonexistent/file.py", 1)
32
+
33
+ assert "error" in result
34
+ assert (
35
+ "not found" in result["error"].lower() or "fatal" in result["error"].lower()
36
+ )
37
+
38
+ def test_returns_error_for_invalid_line(self):
39
+ """Returns error for line beyond file length."""
40
+ result = git_blame("yamlgraph/__init__.py", 99999)
41
+
42
+ assert "error" in result
43
+
44
+ def test_returns_line_content(self):
45
+ """Returns the actual line content."""
46
+ result = git_blame("yamlgraph/__init__.py", 1)
47
+
48
+ assert "error" not in result
49
+ assert "line_content" in result
50
+ assert isinstance(result["line_content"], str)
51
+
52
+
53
+ class TestGitLog:
54
+ """Tests for git_log function."""
55
+
56
+ def test_returns_recent_commits(self):
57
+ """Returns list of recent commits for file."""
58
+ result = git_log("yamlgraph/__init__.py")
59
+
60
+ assert "error" not in result
61
+ assert "commits" in result
62
+ assert isinstance(result["commits"], list)
63
+ assert len(result["commits"]) > 0
64
+
65
+ def test_each_commit_has_required_fields(self):
66
+ """Each commit has hash, author, date, message."""
67
+ result = git_log("yamlgraph/__init__.py")
68
+
69
+ assert "error" not in result
70
+ for commit in result["commits"]:
71
+ assert "hash" in commit
72
+ assert "author" in commit
73
+ assert "date" in commit
74
+ assert "message" in commit
75
+
76
+ def test_respects_n_limit(self):
77
+ """Returns at most n commits."""
78
+ result = git_log("yamlgraph/__init__.py", n=2)
79
+
80
+ assert "error" not in result
81
+ assert len(result["commits"]) <= 2
82
+
83
+ def test_returns_error_for_invalid_file(self):
84
+ """Returns error for non-existent file."""
85
+ result = git_log("nonexistent/file.py")
86
+
87
+ assert "error" in result
88
+
89
+ def test_returns_error_for_untracked_file(self):
90
+ """Returns error for file not in git."""
91
+ # Create a temp file that won't be tracked
92
+ import tempfile
93
+ from pathlib import Path
94
+
95
+ with tempfile.NamedTemporaryFile(suffix=".py", delete=False) as f:
96
+ f.write(b"# temp file")
97
+ temp_path = f.name
98
+
99
+ try:
100
+ result = git_log(temp_path)
101
+ # Should either error or return empty commits
102
+ assert "error" in result or len(result.get("commits", [])) == 0
103
+ finally:
104
+ Path(temp_path).unlink()
105
+
106
+ def test_default_n_is_5(self):
107
+ """Default returns up to 5 commits."""
108
+ result = git_log("yamlgraph/__init__.py")
109
+
110
+ assert "error" not in result
111
+ # Can be less than 5 if file has fewer commits
112
+ assert len(result["commits"]) <= 5
@@ -0,0 +1,193 @@
1
+ """Tests for impl-agent discovery schemas."""
2
+
3
+ import pytest
4
+ from pydantic import ValidationError
5
+
6
+ from examples.codegen.models.schemas import (
7
+ DiscoveryFindings,
8
+ DiscoveryPlan,
9
+ DiscoveryResult,
10
+ DiscoveryTask,
11
+ )
12
+
13
+
14
+ class TestDiscoveryTask:
15
+ """Tests for DiscoveryTask model."""
16
+
17
+ def test_valid_task(self):
18
+ """Create a valid discovery task."""
19
+ task = DiscoveryTask(
20
+ id=1,
21
+ task="Find websearch function",
22
+ tool="get_structure",
23
+ args={"file_path": "yamlgraph/tools/websearch.py"},
24
+ rationale="Need to locate the target function",
25
+ )
26
+ assert task.id == 1
27
+ assert task.tool == "get_structure"
28
+ assert task.status == "pending" # default
29
+ assert task.priority == 1 # default
30
+
31
+ def test_task_with_all_fields(self):
32
+ """Create task with explicit status and priority."""
33
+ task = DiscoveryTask(
34
+ id=2,
35
+ task="Find callers",
36
+ tool="get_callers",
37
+ args={"function_name": "websearch"},
38
+ rationale="Identify dependencies",
39
+ status="done",
40
+ priority=2,
41
+ )
42
+ assert task.status == "done"
43
+ assert task.priority == 2
44
+
45
+ def test_invalid_status(self):
46
+ """Reject invalid status values."""
47
+ with pytest.raises(ValidationError):
48
+ DiscoveryTask(
49
+ id=1,
50
+ task="Test",
51
+ tool="test_tool",
52
+ args={},
53
+ rationale="Test",
54
+ status="invalid_status",
55
+ )
56
+
57
+ def test_missing_required_fields(self):
58
+ """Require all mandatory fields."""
59
+ with pytest.raises(ValidationError):
60
+ DiscoveryTask(id=1, task="Test") # missing tool, args, rationale
61
+
62
+
63
+ class TestDiscoveryResult:
64
+ """Tests for DiscoveryResult model."""
65
+
66
+ def test_successful_result(self):
67
+ """Create a successful discovery result."""
68
+ result = DiscoveryResult(
69
+ task_id=1,
70
+ tool="get_structure",
71
+ success=True,
72
+ result={"classes": [], "functions": ["websearch"]},
73
+ )
74
+ assert result.success is True
75
+ assert result.error is None
76
+
77
+ def test_failed_result(self):
78
+ """Create a failed discovery result."""
79
+ result = DiscoveryResult(
80
+ task_id=2,
81
+ tool="get_callers",
82
+ success=False,
83
+ result=None,
84
+ error="Function not found",
85
+ )
86
+ assert result.success is False
87
+ assert result.error == "Function not found"
88
+
89
+ def test_result_serialization(self):
90
+ """Result can be serialized to dict."""
91
+ result = DiscoveryResult(
92
+ task_id=1,
93
+ tool="find_tests",
94
+ success=True,
95
+ result=["test_websearch.py"],
96
+ )
97
+ data = result.model_dump()
98
+ assert data["task_id"] == 1
99
+ assert data["result"] == ["test_websearch.py"]
100
+
101
+
102
+ class TestDiscoveryPlan:
103
+ """Tests for DiscoveryPlan model."""
104
+
105
+ def test_plan_with_multiple_tasks(self):
106
+ """Create a plan with multiple tasks."""
107
+ plan = DiscoveryPlan(
108
+ tasks=[
109
+ DiscoveryTask(
110
+ id=1,
111
+ task="Find target",
112
+ tool="get_structure",
113
+ args={"file_path": "test.py"},
114
+ rationale="Locate code",
115
+ ),
116
+ DiscoveryTask(
117
+ id=2,
118
+ task="Find callers",
119
+ tool="get_callers",
120
+ args={"function_name": "test"},
121
+ rationale="Find dependencies",
122
+ priority=2,
123
+ ),
124
+ ]
125
+ )
126
+ assert len(plan.tasks) == 2
127
+ assert plan.tasks[0].priority == 1
128
+ assert plan.tasks[1].priority == 2
129
+
130
+ def test_empty_plan(self):
131
+ """Allow empty task list."""
132
+ plan = DiscoveryPlan(tasks=[])
133
+ assert len(plan.tasks) == 0
134
+
135
+ def test_plan_serialization(self):
136
+ """Plan can be serialized to dict."""
137
+ plan = DiscoveryPlan(
138
+ tasks=[
139
+ DiscoveryTask(
140
+ id=1,
141
+ task="Test",
142
+ tool="test",
143
+ args={},
144
+ rationale="Test",
145
+ )
146
+ ]
147
+ )
148
+ data = plan.model_dump()
149
+ assert "tasks" in data
150
+ assert len(data["tasks"]) == 1
151
+
152
+
153
+ class TestDiscoveryFindings:
154
+ """Tests for DiscoveryFindings model."""
155
+
156
+ def test_findings_with_results(self):
157
+ """Create findings with multiple results."""
158
+ findings = DiscoveryFindings(
159
+ results=[
160
+ DiscoveryResult(
161
+ task_id=1,
162
+ tool="get_structure",
163
+ success=True,
164
+ result={"functions": ["foo"]},
165
+ ),
166
+ DiscoveryResult(
167
+ task_id=2,
168
+ tool="get_callers",
169
+ success=False,
170
+ result=None,
171
+ error="Not found",
172
+ ),
173
+ ]
174
+ )
175
+ assert len(findings.results) == 2
176
+ assert findings.results[0].success is True
177
+ assert findings.results[1].success is False
178
+
179
+ def test_empty_findings(self):
180
+ """Allow empty results."""
181
+ findings = DiscoveryFindings(results=[])
182
+ assert len(findings.results) == 0
183
+
184
+ def test_findings_serialization(self):
185
+ """Findings can be serialized to dict."""
186
+ findings = DiscoveryFindings(
187
+ results=[
188
+ DiscoveryResult(task_id=1, tool="test", success=True, result="data")
189
+ ]
190
+ )
191
+ data = findings.model_dump()
192
+ assert "results" in data
193
+ assert data["results"][0]["success"] is True
@@ -0,0 +1,94 @@
1
+ """Tests for impl-agent v4 graph integration.
2
+
3
+ TDD Phase 5: Wire discovery nodes into impl-agent.yaml.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import yaml
10
+
11
+ GRAPH_PATH = Path(__file__).parent.parent / "impl-agent.yaml"
12
+
13
+
14
+ class TestImplAgentV4Structure:
15
+ """Test the new graph structure."""
16
+
17
+ @pytest.fixture
18
+ def graph_config(self) -> dict:
19
+ """Load the graph YAML."""
20
+ with open(GRAPH_PATH) as f:
21
+ return yaml.safe_load(f)
22
+
23
+ def test_has_plan_discovery_node(self, graph_config):
24
+ """Should have plan_discovery LLM node."""
25
+ nodes = graph_config.get("nodes", {})
26
+ assert "plan_discovery" in nodes
27
+ node = nodes["plan_discovery"]
28
+ assert node.get("prompt") == "examples/codegen/plan_discovery"
29
+ assert node.get("state_key") == "discovery_plan"
30
+
31
+ def test_has_execute_discovery_node(self, graph_config):
32
+ """Should have execute_discovery map node."""
33
+ nodes = graph_config.get("nodes", {})
34
+ assert "execute_discovery" in nodes
35
+ node = nodes["execute_discovery"]
36
+ assert node.get("type") == "map"
37
+ assert "discovery_plan" in node.get("over", "")
38
+ # Sub-node should be tool_call
39
+ sub_node = node.get("node", {})
40
+ assert sub_node.get("type") == "tool_call"
41
+
42
+ def test_has_synthesize_node(self, graph_config):
43
+ """Should have synthesize LLM node."""
44
+ nodes = graph_config.get("nodes", {})
45
+ assert "synthesize" in nodes
46
+ node = nodes["synthesize"]
47
+ assert node.get("prompt") == "examples/codegen/synthesize"
48
+ assert node.get("state_key") == "code_analysis"
49
+
50
+ def test_edge_flow(self, graph_config):
51
+ """Should have correct edge flow through discovery nodes."""
52
+ edges = graph_config.get("edges", [])
53
+
54
+ # Convert to from->to pairs for easier checking
55
+ edge_pairs = [(e["from"], e["to"]) for e in edges]
56
+
57
+ # Must have: parse_story -> plan_discovery -> execute_discovery -> synthesize -> plan
58
+ assert ("parse_story", "plan_discovery") in edge_pairs
59
+ assert ("plan_discovery", "execute_discovery") in edge_pairs
60
+ assert ("execute_discovery", "synthesize") in edge_pairs
61
+ assert ("synthesize", "plan") in edge_pairs
62
+
63
+ def test_no_agent_nodes(self, graph_config):
64
+ """Should not have old agent nodes (discover, analyze)."""
65
+ nodes = graph_config.get("nodes", {})
66
+ # Old agent-based discovery is replaced
67
+ assert "discover" not in nodes
68
+ assert "analyze" not in nodes
69
+
70
+
71
+ class TestImplAgentV4State:
72
+ """Test state schema includes new fields."""
73
+
74
+ @pytest.fixture
75
+ def graph_config(self) -> dict:
76
+ """Load the graph YAML."""
77
+ with open(GRAPH_PATH) as f:
78
+ return yaml.safe_load(f)
79
+
80
+ def test_state_has_discovery_plan(self, graph_config):
81
+ """State should include discovery_plan."""
82
+ state = graph_config.get("state", {})
83
+ # Check for discovery_plan in state definition
84
+ assert "discovery_plan" in state or any(
85
+ "discovery_plan" in str(v) for v in state.values()
86
+ )
87
+
88
+ def test_state_has_discovery_findings(self, graph_config):
89
+ """State should include discovery_findings for collected results."""
90
+ # The collect key from map node
91
+ nodes = graph_config.get("nodes", {})
92
+ if "execute_discovery" in nodes:
93
+ collect_key = nodes["execute_discovery"].get("collect", "")
94
+ assert collect_key # Should have a collect key
@@ -0,0 +1,226 @@
1
+ """Tests for jedi-based code analysis tools."""
2
+
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from examples.codegen.tools.jedi_analysis import (
9
+ JEDI_AVAILABLE,
10
+ find_references,
11
+ get_callees,
12
+ get_callers,
13
+ )
14
+
15
+ # Skip all tests if jedi not available
16
+ pytestmark = pytest.mark.skipif(not JEDI_AVAILABLE, reason="jedi not installed")
17
+
18
+
19
+ class TestFindReferences:
20
+ """Tests for find_references function."""
21
+
22
+ def test_finds_definition(self):
23
+ """Finds the definition of a symbol."""
24
+ with tempfile.TemporaryDirectory() as tmpdir:
25
+ # Create a simple module
26
+ file_path = Path(tmpdir) / "sample.py"
27
+ file_path.write_text("""
28
+ class MyConfig:
29
+ timeout: int = 30
30
+
31
+ config = MyConfig()
32
+ print(config.timeout)
33
+ """)
34
+ result = find_references(str(file_path), "MyConfig", line=2)
35
+
36
+ assert isinstance(result, list)
37
+ assert len(result) >= 1
38
+ # Should find at least the definition
39
+ types = [r.get("type") for r in result]
40
+ assert any(t in ["definition", "name"] for t in types)
41
+
42
+ def test_finds_usages_in_same_file(self):
43
+ """Finds usages of a symbol within the same file."""
44
+ with tempfile.TemporaryDirectory() as tmpdir:
45
+ file_path = Path(tmpdir) / "sample.py"
46
+ file_path.write_text("""
47
+ def helper():
48
+ return 42
49
+
50
+ def main():
51
+ x = helper()
52
+ y = helper()
53
+ return x + y
54
+ """)
55
+ result = find_references(str(file_path), "helper", line=2)
56
+
57
+ assert isinstance(result, list)
58
+ # Should find definition + 2 usages
59
+ assert len(result) >= 3
60
+
61
+ def test_returns_empty_for_unknown_symbol(self):
62
+ """Returns empty list for non-existent symbol."""
63
+ with tempfile.TemporaryDirectory() as tmpdir:
64
+ file_path = Path(tmpdir) / "sample.py"
65
+ file_path.write_text("""
66
+ def existing_function():
67
+ return 42
68
+ """)
69
+ # Search for a symbol that doesn't exist, on a line with a different symbol
70
+ result = find_references(str(file_path), "nonexistent_xyz", line=2)
71
+
72
+ assert isinstance(result, list)
73
+ # jedi may return what's at the position, so filter by name
74
+ matching = [r for r in result if r.get("name") == "nonexistent_xyz"]
75
+ assert len(matching) == 0
76
+
77
+ def test_returns_error_for_missing_file(self):
78
+ """Returns error dict for non-existent file."""
79
+ result = find_references("/nonexistent/file.py", "symbol", line=1)
80
+
81
+ assert isinstance(result, dict)
82
+ assert "error" in result
83
+
84
+ def test_finds_cross_file_references(self):
85
+ """Finds references across multiple files in same project."""
86
+ with tempfile.TemporaryDirectory() as tmpdir:
87
+ # Create module with class
88
+ (Path(tmpdir) / "config.py").write_text("""
89
+ class AppConfig:
90
+ name: str = "app"
91
+ """)
92
+ # Create module that imports it
93
+ (Path(tmpdir) / "main.py").write_text("""
94
+ from config import AppConfig
95
+
96
+ cfg = AppConfig()
97
+ """)
98
+ result = find_references(
99
+ str(Path(tmpdir) / "config.py"),
100
+ "AppConfig",
101
+ line=2,
102
+ project_path=tmpdir,
103
+ )
104
+
105
+ assert isinstance(result, list)
106
+ # Should find definition + import + usage
107
+ files = {r.get("file", "") for r in result}
108
+ assert len(files) >= 1 # At least the defining file
109
+
110
+
111
+ class TestGetCallers:
112
+ """Tests for get_callers function."""
113
+
114
+ def test_finds_callers(self):
115
+ """Finds functions that call a given function."""
116
+ with tempfile.TemporaryDirectory() as tmpdir:
117
+ file_path = Path(tmpdir) / "sample.py"
118
+ file_path.write_text("""
119
+ def target_function():
120
+ return 42
121
+
122
+ def caller_one():
123
+ return target_function()
124
+
125
+ def caller_two():
126
+ x = target_function()
127
+ return x * 2
128
+
129
+ def unrelated():
130
+ return 0
131
+ """)
132
+ result = get_callers(str(file_path), "target_function", line=2)
133
+
134
+ assert isinstance(result, list)
135
+ assert len(result) >= 2
136
+ caller_names = [r.get("caller") for r in result]
137
+ assert "caller_one" in caller_names
138
+ assert "caller_two" in caller_names
139
+ assert "unrelated" not in caller_names
140
+
141
+ def test_returns_empty_for_uncalled_function(self):
142
+ """Returns empty list for function with no callers."""
143
+ with tempfile.TemporaryDirectory() as tmpdir:
144
+ file_path = Path(tmpdir) / "sample.py"
145
+ file_path.write_text("""
146
+ def lonely_function():
147
+ return 42
148
+ """)
149
+ result = get_callers(str(file_path), "lonely_function", line=2)
150
+
151
+ assert isinstance(result, list)
152
+ assert len(result) == 0
153
+
154
+ def test_returns_error_for_missing_file(self):
155
+ """Returns error dict for non-existent file."""
156
+ result = get_callers("/nonexistent/file.py", "func", line=1)
157
+
158
+ assert isinstance(result, dict)
159
+ assert "error" in result
160
+
161
+
162
+ class TestGetCallees:
163
+ """Tests for get_callees function."""
164
+
165
+ def test_finds_callees(self):
166
+ """Finds functions called by a given function."""
167
+ with tempfile.TemporaryDirectory() as tmpdir:
168
+ file_path = Path(tmpdir) / "sample.py"
169
+ file_path.write_text("""
170
+ def helper_a():
171
+ return 1
172
+
173
+ def helper_b():
174
+ return 2
175
+
176
+ def main_function():
177
+ a = helper_a()
178
+ b = helper_b()
179
+ return a + b
180
+ """)
181
+ result = get_callees(str(file_path), "main_function", line=8)
182
+
183
+ assert isinstance(result, list)
184
+ assert len(result) >= 2
185
+ callee_names = [r.get("callee") for r in result]
186
+ assert "helper_a" in callee_names
187
+ assert "helper_b" in callee_names
188
+
189
+ def test_returns_empty_for_function_with_no_calls(self):
190
+ """Returns empty list for function that makes no calls."""
191
+ with tempfile.TemporaryDirectory() as tmpdir:
192
+ file_path = Path(tmpdir) / "sample.py"
193
+ file_path.write_text("""
194
+ def pure_function():
195
+ return 42
196
+ """)
197
+ result = get_callees(str(file_path), "pure_function", line=2)
198
+
199
+ assert isinstance(result, list)
200
+ assert len(result) == 0
201
+
202
+ def test_returns_error_for_missing_file(self):
203
+ """Returns error dict for non-existent file."""
204
+ result = get_callees("/nonexistent/file.py", "func", line=1)
205
+
206
+ assert isinstance(result, dict)
207
+ assert "error" in result
208
+
209
+ def test_includes_line_numbers(self):
210
+ """Each callee includes line number where called."""
211
+ with tempfile.TemporaryDirectory() as tmpdir:
212
+ file_path = Path(tmpdir) / "sample.py"
213
+ file_path.write_text("""
214
+ def helper():
215
+ return 1
216
+
217
+ def caller():
218
+ x = helper()
219
+ return x
220
+ """)
221
+ result = get_callees(str(file_path), "caller", line=5)
222
+
223
+ assert len(result) >= 1
224
+ for callee in result:
225
+ assert "callee" in callee
226
+ assert "line" in callee