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,250 @@
1
+ """Tests for YAMLGraph meta-template tools."""
2
+
3
+ from pathlib import Path
4
+
5
+ from examples.codegen.tools.meta_tools import (
6
+ extract_graph_template,
7
+ extract_prompt_template,
8
+ )
9
+
10
+ # ============================================================================
11
+ # extract_graph_template tests
12
+ # ============================================================================
13
+
14
+
15
+ class TestExtractGraphTemplate:
16
+ """Tests for extract_graph_template."""
17
+
18
+ def test_extracts_node_types(self, tmp_path: Path):
19
+ """Extract node type patterns from graph."""
20
+ graph = tmp_path / "graph.yaml"
21
+ graph.write_text("""
22
+ name: test-graph
23
+ version: "1.0"
24
+
25
+ nodes:
26
+ step1:
27
+ type: llm
28
+ prompt: prompts/step1
29
+ state_key: result1
30
+
31
+ step2:
32
+ type: agent
33
+ prompt: prompts/step2
34
+ tools: [tool1, tool2]
35
+ max_iterations: 5
36
+
37
+ edges:
38
+ - START -> step1
39
+ - step1 -> step2
40
+ - step2 -> END
41
+ """)
42
+ result = extract_graph_template(str(graph))
43
+
44
+ assert "node_types" in result
45
+ assert "llm" in result["node_types"]
46
+ assert "agent" in result["node_types"]
47
+
48
+ def test_extracts_edge_patterns(self, tmp_path: Path):
49
+ """Extract edge patterns (sequential, conditional)."""
50
+ graph = tmp_path / "graph.yaml"
51
+ graph.write_text("""
52
+ name: conditional-graph
53
+
54
+ nodes:
55
+ check:
56
+ type: llm
57
+ prompt: check
58
+ branch_a:
59
+ type: llm
60
+ prompt: a
61
+ branch_b:
62
+ type: llm
63
+ prompt: b
64
+
65
+ edges:
66
+ - START -> check
67
+ - check -> branch_a:
68
+ condition: state.result == "a"
69
+ - check -> branch_b:
70
+ condition: state.result == "b"
71
+ - branch_a -> END
72
+ - branch_b -> END
73
+ """)
74
+ result = extract_graph_template(str(graph))
75
+
76
+ assert "edge_patterns" in result
77
+ patterns = result["edge_patterns"]
78
+ assert "conditional" in patterns or any("condition" in str(p) for p in patterns)
79
+
80
+ def test_extracts_state_fields(self, tmp_path: Path):
81
+ """Extract state field patterns."""
82
+ graph = tmp_path / "graph.yaml"
83
+ graph.write_text("""
84
+ name: stateful-graph
85
+
86
+ state:
87
+ input: str
88
+ results: list
89
+ errors: list[PipelineError]
90
+
91
+ nodes:
92
+ process:
93
+ type: llm
94
+ prompt: process
95
+ state_key: results
96
+ """)
97
+ result = extract_graph_template(str(graph))
98
+
99
+ assert "state_fields" in result
100
+ fields = result["state_fields"]
101
+ assert "input" in fields or any("input" in str(f) for f in fields)
102
+
103
+ def test_extracts_tool_patterns(self, tmp_path: Path):
104
+ """Extract tool declaration patterns."""
105
+ graph = tmp_path / "graph.yaml"
106
+ graph.write_text("""
107
+ name: tool-graph
108
+
109
+ tools:
110
+ search:
111
+ type: python
112
+ module: mymodule
113
+ function: search_func
114
+ description: "Search for things"
115
+
116
+ fetch:
117
+ type: shell
118
+ command: "curl {url}"
119
+
120
+ nodes:
121
+ agent:
122
+ type: agent
123
+ tools: [search, fetch]
124
+ """)
125
+ result = extract_graph_template(str(graph))
126
+
127
+ assert "tool_patterns" in result
128
+ patterns = result["tool_patterns"]
129
+ assert "python" in patterns or any("python" in str(p) for p in patterns)
130
+
131
+ def test_handles_missing_file(self):
132
+ """Returns error for missing file."""
133
+ result = extract_graph_template("/nonexistent/graph.yaml")
134
+ assert "error" in result
135
+
136
+ def test_handles_invalid_yaml(self, tmp_path: Path):
137
+ """Returns error for invalid YAML."""
138
+ graph = tmp_path / "bad.yaml"
139
+ graph.write_text("{ invalid yaml: [")
140
+
141
+ result = extract_graph_template(str(graph))
142
+ assert "error" in result
143
+
144
+
145
+ # ============================================================================
146
+ # extract_prompt_template tests
147
+ # ============================================================================
148
+
149
+
150
+ class TestExtractPromptTemplate:
151
+ """Tests for extract_prompt_template."""
152
+
153
+ def test_extracts_system_structure(self, tmp_path: Path):
154
+ """Extract system prompt structure."""
155
+ prompt = tmp_path / "prompt.yaml"
156
+ prompt.write_text("""
157
+ metadata:
158
+ provider: anthropic
159
+ model: claude-sonnet-4-20250514
160
+
161
+ system: |
162
+ You are an expert assistant.
163
+
164
+ ## Instructions
165
+ - Be helpful
166
+ - Be concise
167
+
168
+ ## Output Format
169
+ Respond in JSON.
170
+
171
+ user: "Process this: {input}"
172
+ """)
173
+ result = extract_prompt_template(str(prompt))
174
+
175
+ assert "system_structure" in result
176
+ structure = result["system_structure"]
177
+ assert "sections" in structure or len(structure) > 0
178
+
179
+ def test_extracts_variable_patterns(self, tmp_path: Path):
180
+ """Extract variable injection patterns."""
181
+ prompt = tmp_path / "prompt.yaml"
182
+ prompt.write_text("""
183
+ system: "You are a {role} assistant."
184
+ user: |
185
+ Input: {input}
186
+ Context: {context}
187
+ Previous: {history}
188
+ """)
189
+ result = extract_prompt_template(str(prompt))
190
+
191
+ assert "variables" in result
192
+ variables = result["variables"]
193
+ assert "input" in variables
194
+ assert "context" in variables
195
+
196
+ def test_extracts_schema_patterns(self, tmp_path: Path):
197
+ """Extract output schema patterns."""
198
+ prompt = tmp_path / "prompt.yaml"
199
+ prompt.write_text("""
200
+ system: "Analyze the input."
201
+ user: "{input}"
202
+
203
+ schema:
204
+ type: object
205
+ properties:
206
+ result:
207
+ type: string
208
+ confidence:
209
+ type: number
210
+ required: [result]
211
+ """)
212
+ result = extract_prompt_template(str(prompt))
213
+
214
+ assert "schema_patterns" in result
215
+ patterns = result["schema_patterns"]
216
+ assert "result" in str(patterns) or len(patterns) > 0
217
+
218
+ def test_extracts_jinja_patterns(self, tmp_path: Path):
219
+ """Extract Jinja2 template patterns."""
220
+ prompt = tmp_path / "prompt.yaml"
221
+ prompt.write_text("""
222
+ system: |
223
+ {% if context %}
224
+ Context: {{ context }}
225
+ {% endif %}
226
+
227
+ {% for item in items %}
228
+ - {{ item.name }}: {{ item.value }}
229
+ {% endfor %}
230
+
231
+ user: "Process {{ input }}"
232
+ """)
233
+ result = extract_prompt_template(str(prompt))
234
+
235
+ assert "jinja_patterns" in result
236
+ patterns = result["jinja_patterns"]
237
+ assert "if" in patterns or "for" in patterns
238
+
239
+ def test_handles_missing_file(self):
240
+ """Returns error for missing file."""
241
+ result = extract_prompt_template("/nonexistent/prompt.yaml")
242
+ assert "error" in result
243
+
244
+ def test_handles_invalid_yaml(self, tmp_path: Path):
245
+ """Returns error for invalid YAML."""
246
+ prompt = tmp_path / "bad.yaml"
247
+ prompt.write_text("{ invalid yaml: [")
248
+
249
+ result = extract_prompt_template(str(prompt))
250
+ assert "error" in result
@@ -0,0 +1,98 @@
1
+ """Tests for plan_discovery prompt."""
2
+
3
+ from pathlib import Path
4
+
5
+ from examples.codegen.models.schemas import DiscoveryPlan, DiscoveryTask
6
+ from yamlgraph.utils.prompts import load_prompt
7
+
8
+
9
+ class TestPlanDiscoveryPrompt:
10
+ """Tests for plan_discovery.yaml prompt structure."""
11
+
12
+ def test_prompt_file_exists(self):
13
+ """Prompt file exists and is loadable."""
14
+ prompt_path = Path("examples/codegen/prompts/plan_discovery.yaml")
15
+ assert prompt_path.exists(), "plan_discovery.yaml not found"
16
+
17
+ prompt = load_prompt("examples/codegen/plan_discovery")
18
+ assert "system" in prompt
19
+ assert "user" in prompt
20
+ assert "schema" in prompt
21
+
22
+ def test_prompt_has_required_sections(self):
23
+ """Prompt contains key instruction sections."""
24
+ prompt = load_prompt("examples/codegen/plan_discovery")
25
+ system = prompt["system"]
26
+
27
+ # Check for key sections
28
+ assert "Available Tools" in system
29
+ assert "Minimum Checklist" in system
30
+ assert "Priority" in system
31
+
32
+ def test_prompt_lists_core_tools(self):
33
+ """Prompt documents core discovery tools."""
34
+ prompt = load_prompt("examples/codegen/plan_discovery")
35
+ system = prompt["system"]
36
+
37
+ # Core tools should be listed
38
+ assert "list_modules" in system
39
+ assert "get_structure" in system
40
+ assert "find_tests" in system
41
+ assert "get_callers" in system
42
+ assert "find_similar_code" in system
43
+
44
+ def test_schema_matches_discovery_plan(self):
45
+ """Output schema is compatible with DiscoveryPlan model."""
46
+ prompt = load_prompt("examples/codegen/plan_discovery")
47
+ schema = prompt["schema"]
48
+
49
+ # Schema should have name and define tasks field
50
+ assert "name" in schema
51
+ assert schema["name"] == "DiscoveryPlan"
52
+ assert "fields" in schema
53
+ assert "tasks" in schema["fields"]
54
+
55
+ def test_discovery_plan_accepts_valid_output(self):
56
+ """DiscoveryPlan can parse expected LLM output format."""
57
+ # Simulate LLM output
58
+ llm_output = {
59
+ "tasks": [
60
+ {
61
+ "id": 1,
62
+ "task": "Find websearch module structure",
63
+ "tool": "get_structure",
64
+ "args": {"file_path": "yamlgraph/tools/websearch.py"},
65
+ "rationale": "Locate target function",
66
+ "priority": 1,
67
+ },
68
+ {
69
+ "id": 2,
70
+ "task": "Find callers of websearch",
71
+ "tool": "get_callers",
72
+ "args": {
73
+ "file_path": "yamlgraph/tools/websearch.py",
74
+ "function_name": "websearch",
75
+ "line": 10,
76
+ },
77
+ "rationale": "Understand dependencies",
78
+ "priority": 2,
79
+ },
80
+ ]
81
+ }
82
+
83
+ # Should parse successfully
84
+ plan = DiscoveryPlan(tasks=[DiscoveryTask(**t) for t in llm_output["tasks"]])
85
+ assert len(plan.tasks) == 2
86
+ assert plan.tasks[0].tool == "get_structure"
87
+ assert plan.tasks[1].priority == 2
88
+
89
+ def test_minimum_checklist_coverage(self):
90
+ """Prompt explicitly requires minimum discovery coverage."""
91
+ prompt = load_prompt("examples/codegen/plan_discovery")
92
+ system = prompt["system"]
93
+
94
+ # Minimum checklist items
95
+ assert "Location" in system
96
+ assert "Dependencies" in system
97
+ assert "Tests" in system
98
+ assert "Patterns" in system
@@ -0,0 +1,85 @@
1
+ """Tests for syntax validation tools."""
2
+
3
+ from examples.codegen.tools.syntax_tools import syntax_check
4
+
5
+
6
+ class TestSyntaxCheck:
7
+ """Tests for syntax_check function."""
8
+
9
+ def test_valid_python_returns_valid(self):
10
+ """Valid Python code returns valid=True."""
11
+ code = "def hello():\n return 'world'"
12
+ result = syntax_check(code)
13
+
14
+ assert result["valid"] is True
15
+ assert "error" not in result
16
+
17
+ def test_valid_class_definition(self):
18
+ """Valid class definition passes."""
19
+ code = """
20
+ class MyClass:
21
+ def __init__(self, name: str):
22
+ self.name = name
23
+
24
+ def greet(self) -> str:
25
+ return f"Hello, {self.name}"
26
+ """
27
+ result = syntax_check(code)
28
+ assert result["valid"] is True
29
+
30
+ def test_invalid_syntax_returns_error(self):
31
+ """Invalid syntax returns valid=False with error."""
32
+ code = "def broken(\n return"
33
+ result = syntax_check(code)
34
+
35
+ assert result["valid"] is False
36
+ assert "error" in result
37
+ assert isinstance(result["error"], str)
38
+
39
+ def test_missing_colon_error(self):
40
+ """Missing colon is detected."""
41
+ code = "if True\n print('oops')"
42
+ result = syntax_check(code)
43
+
44
+ assert result["valid"] is False
45
+ assert "error" in result
46
+
47
+ def test_indentation_error(self):
48
+ """Indentation errors are detected."""
49
+ code = "def foo():\nreturn 1"
50
+ result = syntax_check(code)
51
+
52
+ assert result["valid"] is False
53
+ assert "error" in result
54
+
55
+ def test_empty_string_is_valid(self):
56
+ """Empty string is valid Python."""
57
+ result = syntax_check("")
58
+ assert result["valid"] is True
59
+
60
+ def test_comment_only_is_valid(self):
61
+ """Comment-only code is valid."""
62
+ code = "# Just a comment"
63
+ result = syntax_check(code)
64
+ assert result["valid"] is True
65
+
66
+ def test_import_statement_valid(self):
67
+ """Import statements are valid."""
68
+ code = "from typing import Optional\nimport os"
69
+ result = syntax_check(code)
70
+ assert result["valid"] is True
71
+
72
+ def test_error_includes_line_number(self):
73
+ """Error message includes line information."""
74
+ code = "line1 = 1\nline2 =\nline3 = 3"
75
+ result = syntax_check(code)
76
+
77
+ assert result["valid"] is False
78
+ # Error should mention line 2
79
+ assert "line" in result["error"].lower() or "2" in result["error"]
80
+
81
+ def test_multiline_string_valid(self):
82
+ """Multiline strings are valid."""
83
+ code = '"""This is a\nmultiline\ndocstring."""'
84
+ result = syntax_check(code)
85
+ assert result["valid"] is True
@@ -0,0 +1,94 @@
1
+ """Tests for synthesize prompt.
2
+
3
+ TDD Phase 4: LLM combines discovery findings into analysis.
4
+ """
5
+
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+ import yaml
10
+
11
+ PROMPT_PATH = Path(__file__).parent.parent / "prompts" / "synthesize.yaml"
12
+
13
+
14
+ class TestSynthesizePromptStructure:
15
+ """Test prompt file structure and content."""
16
+
17
+ @pytest.fixture
18
+ def prompt_content(self) -> dict:
19
+ """Load the prompt YAML."""
20
+ assert PROMPT_PATH.exists(), f"Prompt not found: {PROMPT_PATH}"
21
+ with open(PROMPT_PATH) as f:
22
+ return yaml.safe_load(f)
23
+
24
+ def test_has_required_sections(self, prompt_content):
25
+ """Should have system, user, and schema sections."""
26
+ assert "system" in prompt_content
27
+ assert "user" in prompt_content
28
+ assert "schema" in prompt_content
29
+
30
+ def test_has_metadata(self, prompt_content):
31
+ """Should have metadata with description and model."""
32
+ assert "metadata" in prompt_content
33
+ metadata = prompt_content["metadata"]
34
+ assert "description" in metadata
35
+ assert "model" in metadata or "provider" in metadata
36
+
37
+ def test_system_covers_focus_areas(self, prompt_content):
38
+ """System prompt should mention key focus areas."""
39
+ system = prompt_content["system"].lower()
40
+ # Must analyze these aspects
41
+ assert "file" in system or "target" in system
42
+ assert "depend" in system or "caller" in system
43
+ assert "test" in system
44
+ assert "pattern" in system
45
+
46
+ def test_user_includes_discovery_findings(self, prompt_content):
47
+ """User template should include discovery_findings variable."""
48
+ user = prompt_content["user"]
49
+ assert "{discovery_findings}" in user or "discovery_findings" in user
50
+
51
+ def test_user_includes_story(self, prompt_content):
52
+ """User template should include story context."""
53
+ user = prompt_content["user"]
54
+ assert "{story}" in user or "story" in user
55
+
56
+
57
+ class TestSynthesizeSchema:
58
+ """Test the output schema structure."""
59
+
60
+ @pytest.fixture
61
+ def schema(self) -> dict:
62
+ """Load the schema from prompt."""
63
+ with open(PROMPT_PATH) as f:
64
+ content = yaml.safe_load(f)
65
+ return content.get("schema", {})
66
+
67
+ def test_schema_has_name(self, schema):
68
+ """Schema should have a name."""
69
+ assert "name" in schema
70
+
71
+ def test_schema_has_summary(self, schema):
72
+ """Schema should include summary field."""
73
+ fields = schema.get("fields", {})
74
+ assert "summary" in fields
75
+
76
+ def test_schema_has_target_files(self, schema):
77
+ """Schema should include target_files array."""
78
+ fields = schema.get("fields", {})
79
+ assert "target_files" in fields
80
+
81
+ def test_schema_has_dependencies(self, schema):
82
+ """Schema should include dependencies."""
83
+ fields = schema.get("fields", {})
84
+ assert "dependencies" in fields
85
+
86
+ def test_schema_has_test_coverage(self, schema):
87
+ """Schema should include test coverage info."""
88
+ fields = schema.get("fields", {})
89
+ assert "test_coverage" in fields
90
+
91
+ def test_schema_has_patterns(self, schema):
92
+ """Schema should include patterns to follow."""
93
+ fields = schema.get("fields", {})
94
+ assert "patterns_to_follow" in fields