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.
- examples/__init__.py +1 -0
- examples/codegen/__init__.py +5 -0
- examples/codegen/models/__init__.py +13 -0
- examples/codegen/models/schemas.py +76 -0
- examples/codegen/tests/__init__.py +1 -0
- examples/codegen/tests/test_ai_helpers.py +235 -0
- examples/codegen/tests/test_ast_analysis.py +174 -0
- examples/codegen/tests/test_code_analysis.py +134 -0
- examples/codegen/tests/test_code_context.py +301 -0
- examples/codegen/tests/test_code_nav.py +89 -0
- examples/codegen/tests/test_dependency_tools.py +119 -0
- examples/codegen/tests/test_example_tools.py +185 -0
- examples/codegen/tests/test_git_tools.py +112 -0
- examples/codegen/tests/test_impl_agent_schemas.py +193 -0
- examples/codegen/tests/test_impl_agent_v4_graph.py +94 -0
- examples/codegen/tests/test_jedi_analysis.py +226 -0
- examples/codegen/tests/test_meta_tools.py +250 -0
- examples/codegen/tests/test_plan_discovery_prompt.py +98 -0
- examples/codegen/tests/test_syntax_tools.py +85 -0
- examples/codegen/tests/test_synthesize_prompt.py +94 -0
- examples/codegen/tests/test_template_tools.py +244 -0
- examples/codegen/tools/__init__.py +80 -0
- examples/codegen/tools/ai_helpers.py +420 -0
- examples/codegen/tools/ast_analysis.py +92 -0
- examples/codegen/tools/code_context.py +180 -0
- examples/codegen/tools/code_nav.py +52 -0
- examples/codegen/tools/dependency_tools.py +120 -0
- examples/codegen/tools/example_tools.py +188 -0
- examples/codegen/tools/git_tools.py +151 -0
- examples/codegen/tools/impl_executor.py +614 -0
- examples/codegen/tools/jedi_analysis.py +311 -0
- examples/codegen/tools/meta_tools.py +202 -0
- examples/codegen/tools/syntax_tools.py +26 -0
- examples/codegen/tools/template_tools.py +356 -0
- examples/fastapi_interview.py +167 -0
- examples/npc/api/__init__.py +1 -0
- examples/npc/api/app.py +100 -0
- examples/npc/api/routes/__init__.py +5 -0
- examples/npc/api/routes/encounter.py +182 -0
- examples/npc/api/session.py +330 -0
- examples/npc/demo.py +387 -0
- examples/npc/nodes/__init__.py +5 -0
- examples/npc/nodes/image_node.py +92 -0
- examples/npc/run_encounter.py +230 -0
- examples/shared/__init__.py +0 -0
- examples/shared/replicate_tool.py +238 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +12 -0
- examples/storyboard/nodes/animated_character_node.py +248 -0
- examples/storyboard/nodes/animated_image_node.py +138 -0
- examples/storyboard/nodes/character_node.py +162 -0
- examples/storyboard/nodes/image_node.py +118 -0
- examples/storyboard/nodes/replicate_tool.py +49 -0
- examples/storyboard/retry_images.py +118 -0
- scripts/demo_async_executor.py +212 -0
- scripts/demo_interview_e2e.py +200 -0
- scripts/demo_streaming.py +140 -0
- scripts/run_interview_demo.py +94 -0
- scripts/test_interrupt_fix.py +26 -0
- tests/__init__.py +1 -0
- tests/conftest.py +178 -0
- tests/integration/__init__.py +1 -0
- tests/integration/test_animated_storyboard.py +63 -0
- tests/integration/test_cli_commands.py +242 -0
- tests/integration/test_colocated_prompts.py +139 -0
- tests/integration/test_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +283 -0
- tests/integration/test_npc_api/__init__.py +1 -0
- tests/integration/test_npc_api/test_routes.py +357 -0
- tests/integration/test_npc_api/test_session.py +216 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/integration/test_subgraph_integration.py +295 -0
- tests/integration/test_subgraph_interrupt.py +106 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +355 -0
- tests/unit/test_async_executor.py +346 -0
- tests/unit/test_checkpointer.py +212 -0
- tests/unit/test_checkpointer_factory.py +212 -0
- tests/unit/test_cli.py +121 -0
- tests/unit/test_cli_package.py +81 -0
- tests/unit/test_compile_graph_map.py +132 -0
- tests/unit/test_conditions_routing.py +253 -0
- tests/unit/test_config.py +93 -0
- tests/unit/test_conversation_memory.py +276 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +172 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +149 -0
- tests/unit/test_expressions.py +178 -0
- tests/unit/test_feature_brainstorm.py +194 -0
- tests/unit/test_format_prompt.py +145 -0
- tests/unit/test_generic_report.py +200 -0
- tests/unit/test_graph_commands.py +327 -0
- tests/unit/test_graph_linter.py +627 -0
- tests/unit/test_graph_loader.py +357 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_interrupt_node.py +182 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_json_extract.py +134 -0
- tests/unit/test_langsmith.py +600 -0
- tests/unit/test_langsmith_tools.py +204 -0
- tests/unit/test_llm_factory.py +109 -0
- tests/unit/test_llm_factory_async.py +118 -0
- tests/unit/test_loops.py +403 -0
- tests/unit/test_map_node.py +144 -0
- tests/unit/test_no_backward_compat.py +56 -0
- tests/unit/test_node_factory.py +348 -0
- tests/unit/test_passthrough_node.py +126 -0
- tests/unit/test_prompts.py +324 -0
- tests/unit/test_python_nodes.py +198 -0
- tests/unit/test_reliability.py +298 -0
- tests/unit/test_result_export.py +234 -0
- tests/unit/test_router.py +296 -0
- tests/unit/test_sanitize.py +99 -0
- tests/unit/test_schema_loader.py +295 -0
- tests/unit/test_shell_tools.py +229 -0
- tests/unit/test_state_builder.py +331 -0
- tests/unit/test_state_builder_map.py +104 -0
- tests/unit/test_state_config.py +197 -0
- tests/unit/test_streaming.py +307 -0
- tests/unit/test_subgraph.py +596 -0
- tests/unit/test_template.py +190 -0
- tests/unit/test_tool_call_integration.py +164 -0
- tests/unit/test_tool_call_node.py +178 -0
- tests/unit/test_tool_nodes.py +129 -0
- tests/unit/test_websearch.py +234 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +159 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +231 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +541 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +70 -0
- yamlgraph/error_handlers.py +227 -0
- yamlgraph/executor.py +290 -0
- yamlgraph/executor_async.py +288 -0
- yamlgraph/graph_loader.py +451 -0
- yamlgraph/map_compiler.py +150 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +181 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +768 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +240 -0
- yamlgraph/storage/__init__.py +20 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/checkpointer_factory.py +123 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +320 -0
- yamlgraph/tools/graph_linter.py +388 -0
- yamlgraph/tools/langsmith_tools.py +125 -0
- yamlgraph/tools/nodes.py +126 -0
- yamlgraph/tools/python_tool.py +179 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/tools/websearch.py +242 -0
- yamlgraph/utils/__init__.py +48 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +245 -0
- yamlgraph/utils/json_extract.py +104 -0
- yamlgraph/utils/langsmith.py +416 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +104 -0
- yamlgraph/utils/prompts.py +171 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.3.9.dist-info/METADATA +1105 -0
- yamlgraph-0.3.9.dist-info/RECORD +185 -0
- yamlgraph-0.3.9.dist-info/WHEEL +5 -0
- yamlgraph-0.3.9.dist-info/entry_points.txt +2 -0
- yamlgraph-0.3.9.dist-info/licenses/LICENSE +33 -0
- 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
|