yamlgraph 0.1.1__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.

Potentially problematic release.


This version of yamlgraph might be problematic. Click here for more details.

Files changed (111) hide show
  1. examples/__init__.py +1 -0
  2. examples/storyboard/__init__.py +1 -0
  3. examples/storyboard/generate_videos.py +335 -0
  4. examples/storyboard/nodes/__init__.py +10 -0
  5. examples/storyboard/nodes/animated_character_node.py +248 -0
  6. examples/storyboard/nodes/animated_image_node.py +138 -0
  7. examples/storyboard/nodes/character_node.py +162 -0
  8. examples/storyboard/nodes/image_node.py +118 -0
  9. examples/storyboard/nodes/replicate_tool.py +238 -0
  10. examples/storyboard/retry_images.py +118 -0
  11. tests/__init__.py +1 -0
  12. tests/conftest.py +178 -0
  13. tests/integration/__init__.py +1 -0
  14. tests/integration/test_animated_storyboard.py +63 -0
  15. tests/integration/test_cli_commands.py +242 -0
  16. tests/integration/test_map_demo.py +50 -0
  17. tests/integration/test_memory_demo.py +281 -0
  18. tests/integration/test_pipeline_flow.py +105 -0
  19. tests/integration/test_providers.py +163 -0
  20. tests/integration/test_resume.py +75 -0
  21. tests/unit/__init__.py +1 -0
  22. tests/unit/test_agent_nodes.py +200 -0
  23. tests/unit/test_checkpointer.py +212 -0
  24. tests/unit/test_cli.py +121 -0
  25. tests/unit/test_cli_package.py +81 -0
  26. tests/unit/test_compile_graph_map.py +132 -0
  27. tests/unit/test_conditions_routing.py +253 -0
  28. tests/unit/test_config.py +93 -0
  29. tests/unit/test_conversation_memory.py +270 -0
  30. tests/unit/test_database.py +145 -0
  31. tests/unit/test_deprecation.py +104 -0
  32. tests/unit/test_executor.py +60 -0
  33. tests/unit/test_executor_async.py +179 -0
  34. tests/unit/test_export.py +150 -0
  35. tests/unit/test_expressions.py +178 -0
  36. tests/unit/test_format_prompt.py +145 -0
  37. tests/unit/test_generic_report.py +200 -0
  38. tests/unit/test_graph_commands.py +327 -0
  39. tests/unit/test_graph_loader.py +299 -0
  40. tests/unit/test_graph_schema.py +193 -0
  41. tests/unit/test_inline_schema.py +151 -0
  42. tests/unit/test_issues.py +164 -0
  43. tests/unit/test_jinja2_prompts.py +85 -0
  44. tests/unit/test_langsmith.py +319 -0
  45. tests/unit/test_llm_factory.py +109 -0
  46. tests/unit/test_llm_factory_async.py +118 -0
  47. tests/unit/test_loops.py +403 -0
  48. tests/unit/test_map_node.py +144 -0
  49. tests/unit/test_no_backward_compat.py +56 -0
  50. tests/unit/test_node_factory.py +225 -0
  51. tests/unit/test_prompts.py +166 -0
  52. tests/unit/test_python_nodes.py +198 -0
  53. tests/unit/test_reliability.py +298 -0
  54. tests/unit/test_result_export.py +234 -0
  55. tests/unit/test_router.py +296 -0
  56. tests/unit/test_sanitize.py +99 -0
  57. tests/unit/test_schema_loader.py +295 -0
  58. tests/unit/test_shell_tools.py +229 -0
  59. tests/unit/test_state_builder.py +331 -0
  60. tests/unit/test_state_builder_map.py +104 -0
  61. tests/unit/test_state_config.py +197 -0
  62. tests/unit/test_template.py +190 -0
  63. tests/unit/test_tool_nodes.py +129 -0
  64. yamlgraph/__init__.py +35 -0
  65. yamlgraph/builder.py +110 -0
  66. yamlgraph/cli/__init__.py +139 -0
  67. yamlgraph/cli/__main__.py +6 -0
  68. yamlgraph/cli/commands.py +232 -0
  69. yamlgraph/cli/deprecation.py +92 -0
  70. yamlgraph/cli/graph_commands.py +382 -0
  71. yamlgraph/cli/validators.py +37 -0
  72. yamlgraph/config.py +67 -0
  73. yamlgraph/constants.py +66 -0
  74. yamlgraph/error_handlers.py +226 -0
  75. yamlgraph/executor.py +275 -0
  76. yamlgraph/executor_async.py +122 -0
  77. yamlgraph/graph_loader.py +337 -0
  78. yamlgraph/map_compiler.py +138 -0
  79. yamlgraph/models/__init__.py +36 -0
  80. yamlgraph/models/graph_schema.py +141 -0
  81. yamlgraph/models/schemas.py +124 -0
  82. yamlgraph/models/state_builder.py +236 -0
  83. yamlgraph/node_factory.py +240 -0
  84. yamlgraph/routing.py +87 -0
  85. yamlgraph/schema_loader.py +160 -0
  86. yamlgraph/storage/__init__.py +17 -0
  87. yamlgraph/storage/checkpointer.py +72 -0
  88. yamlgraph/storage/database.py +320 -0
  89. yamlgraph/storage/export.py +269 -0
  90. yamlgraph/tools/__init__.py +1 -0
  91. yamlgraph/tools/agent.py +235 -0
  92. yamlgraph/tools/nodes.py +124 -0
  93. yamlgraph/tools/python_tool.py +178 -0
  94. yamlgraph/tools/shell.py +205 -0
  95. yamlgraph/utils/__init__.py +47 -0
  96. yamlgraph/utils/conditions.py +157 -0
  97. yamlgraph/utils/expressions.py +111 -0
  98. yamlgraph/utils/langsmith.py +308 -0
  99. yamlgraph/utils/llm_factory.py +118 -0
  100. yamlgraph/utils/llm_factory_async.py +105 -0
  101. yamlgraph/utils/logging.py +127 -0
  102. yamlgraph/utils/prompts.py +116 -0
  103. yamlgraph/utils/sanitize.py +98 -0
  104. yamlgraph/utils/template.py +102 -0
  105. yamlgraph/utils/validators.py +181 -0
  106. yamlgraph-0.1.1.dist-info/METADATA +854 -0
  107. yamlgraph-0.1.1.dist-info/RECORD +111 -0
  108. yamlgraph-0.1.1.dist-info/WHEEL +5 -0
  109. yamlgraph-0.1.1.dist-info/entry_points.txt +2 -0
  110. yamlgraph-0.1.1.dist-info/licenses/LICENSE +21 -0
  111. yamlgraph-0.1.1.dist-info/top_level.txt +3 -0
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env python3
2
+ """Retry image generation from existing animated storyboard metadata.
3
+
4
+ Usage:
5
+ python examples/storyboard/retry_images.py outputs/storyboard/20260117_112419/animated
6
+
7
+ Options:
8
+ --model MODEL Image model to use (default: hidream)
9
+ --reference PATH Override reference image path
10
+ --magic FLOAT Magic value for img2img (default: 0.25)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ # Add project root to path for imports
21
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
22
+
23
+ from examples.storyboard.nodes.animated_character_node import (
24
+ generate_animated_character_images,
25
+ )
26
+
27
+
28
+ def main():
29
+ parser = argparse.ArgumentParser(
30
+ description="Retry image generation from existing metadata"
31
+ )
32
+ parser.add_argument(
33
+ "output_dir",
34
+ type=Path,
35
+ help="Path to animated output directory (contains animated_character_story.json)",
36
+ )
37
+ parser.add_argument(
38
+ "--model",
39
+ default="hidream",
40
+ help="Image model to use (default: hidream)",
41
+ )
42
+ parser.add_argument(
43
+ "--reference",
44
+ type=Path,
45
+ help="Override reference image path",
46
+ )
47
+ parser.add_argument(
48
+ "--new-id",
49
+ help="New thread ID for output (default: adds _retry suffix)",
50
+ )
51
+ args = parser.parse_args()
52
+
53
+ # Find metadata file
54
+ output_dir = args.output_dir
55
+ if output_dir.name != "animated":
56
+ output_dir = output_dir / "animated"
57
+
58
+ metadata_path = output_dir / "animated_character_story.json"
59
+ if not metadata_path.exists():
60
+ print(f"❌ Metadata not found: {metadata_path}")
61
+ sys.exit(1)
62
+
63
+ print(f"📂 Loading: {metadata_path}")
64
+ metadata = json.loads(metadata_path.read_text())
65
+
66
+ # Reconstruct animated_panels from metadata
67
+ animated_panels = []
68
+ for panel in metadata["panels"]:
69
+ animated_panels.append(
70
+ {
71
+ "_map_index": panel["index"] - 1,
72
+ **panel["prompts"],
73
+ }
74
+ )
75
+
76
+ # Determine thread_id
77
+ original_thread = output_dir.parent.name
78
+ thread_id = args.new_id or f"{original_thread}_retry"
79
+
80
+ # Build state
81
+ state = {
82
+ "concept": metadata["concept"],
83
+ "model": args.model,
84
+ "thread_id": thread_id,
85
+ "story": {
86
+ "title": metadata["title"],
87
+ "narrative": metadata["narrative"],
88
+ "character_prompt": metadata["character_prompt"],
89
+ },
90
+ "animated_panels": animated_panels,
91
+ }
92
+
93
+ # Handle reference image
94
+ if args.reference:
95
+ state["reference_image"] = str(args.reference)
96
+ elif metadata.get("reference_image"):
97
+ ref_path = Path(metadata["reference_image"])
98
+ if ref_path.exists():
99
+ state["reference_image"] = str(ref_path)
100
+ print(f"🎭 Using existing reference: {ref_path}")
101
+
102
+ print("🎬 Retrying image generation...")
103
+ print(f" Model: {args.model}")
104
+ print(f" Panels: {len(animated_panels)}")
105
+ print(f" Output: outputs/storyboard/{thread_id}/animated/")
106
+
107
+ # Run image generation
108
+ result = generate_animated_character_images(state)
109
+
110
+ if result.get("error"):
111
+ print(f"❌ Error: {result['error']}")
112
+ sys.exit(1)
113
+
114
+ print(f"\n✅ Generated images in: {result['output_dir']}")
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Test suite for yamlgraph."""
tests/conftest.py ADDED
@@ -0,0 +1,178 @@
1
+ """Shared test fixtures for yamlgraph tests.
2
+
3
+ This module provides test-only Pydantic models and fixtures for testing.
4
+ These models are intentionally NOT imported from yamlgraph.models to
5
+ demonstrate that the framework is truly generic and works with any schema.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Generator
10
+ from unittest.mock import MagicMock
11
+
12
+ import pytest
13
+ from pydantic import BaseModel, Field
14
+
15
+ from yamlgraph.models import create_initial_state
16
+ from yamlgraph.storage import YamlGraphDB
17
+
18
+ # =============================================================================
19
+ # Test-Only Pydantic Models (Fixtures)
20
+ # =============================================================================
21
+ # These replicate demo model structures but are defined here to prove
22
+ # the framework is generic and doesn't depend on demo-specific schemas.
23
+ # Named with "Fixture" suffix to avoid pytest collection warnings.
24
+
25
+
26
+ class FixtureGeneratedContent(BaseModel):
27
+ """Test fixture for generated content."""
28
+
29
+ title: str = Field(description="Title of the generated content")
30
+ content: str = Field(description="The main generated text")
31
+ word_count: int = Field(description="Approximate word count")
32
+ tags: list[str] = Field(default_factory=list, description="Relevant tags")
33
+
34
+
35
+ class FixtureAnalysis(BaseModel):
36
+ """Test fixture for content analysis."""
37
+
38
+ summary: str = Field(description="Brief summary of the content")
39
+ key_points: list[str] = Field(description="Main points extracted")
40
+ sentiment: str = Field(description="Overall sentiment")
41
+ confidence: float = Field(ge=0.0, le=1.0, description="Confidence score 0-1")
42
+
43
+
44
+ class FixtureToneClassification(BaseModel):
45
+ """Test fixture for tone classification."""
46
+
47
+ tone: str = Field(description="Detected tone: positive, negative, or neutral")
48
+ confidence: float = Field(ge=0.0, le=1.0, description="Confidence score 0-1")
49
+ reasoning: str = Field(description="Explanation for the classification")
50
+
51
+
52
+ class FixtureDraftContent(BaseModel):
53
+ """Test fixture for draft content."""
54
+
55
+ content: str = Field(description="The draft content")
56
+ version: int = Field(default=1, description="Draft version number")
57
+
58
+
59
+ class FixtureCritique(BaseModel):
60
+ """Test fixture for critique output."""
61
+
62
+ score: float = Field(ge=0.0, le=1.0, description="Quality score 0-1")
63
+ feedback: str = Field(description="Specific improvement suggestions")
64
+ issues: list[str] = Field(
65
+ default_factory=list, description="List of identified issues"
66
+ )
67
+ should_refine: bool = Field(
68
+ default=True, description="Whether refinement is needed"
69
+ )
70
+
71
+
72
+ class FixtureGitReport(BaseModel):
73
+ """Test fixture for git report."""
74
+
75
+ title: str = Field(description="Report title")
76
+ summary: str = Field(description="Executive summary")
77
+ key_findings: list[str] = Field(description="Main findings")
78
+ recommendations: list[str] = Field(default_factory=list, description="Suggestions")
79
+
80
+
81
+ # =============================================================================
82
+ # Fixtures
83
+ # =============================================================================
84
+
85
+
86
+ @pytest.fixture
87
+ def sample_generated_content() -> FixtureGeneratedContent:
88
+ """Sample generated content for testing."""
89
+ return FixtureGeneratedContent(
90
+ title="Test Article",
91
+ content="This is test content about artificial intelligence. " * 20,
92
+ word_count=100,
93
+ tags=["test", "ai"],
94
+ )
95
+
96
+
97
+ @pytest.fixture
98
+ def sample_analysis() -> FixtureAnalysis:
99
+ """Sample analysis for testing."""
100
+ return FixtureAnalysis(
101
+ summary="This is a test summary of the content.",
102
+ key_points=["Point 1", "Point 2", "Point 3"],
103
+ sentiment="positive",
104
+ confidence=0.85,
105
+ )
106
+
107
+
108
+ @pytest.fixture
109
+ def sample_state(sample_generated_content, sample_analysis) -> dict:
110
+ """Complete sample state for testing."""
111
+ state = create_initial_state(
112
+ topic="artificial intelligence",
113
+ style="informative",
114
+ word_count=300,
115
+ thread_id="test123",
116
+ )
117
+ state["generated"] = sample_generated_content
118
+ state["analysis"] = sample_analysis
119
+ state["final_summary"] = "This is the final summary."
120
+ state["current_step"] = "summarize"
121
+ return state
122
+
123
+
124
+ @pytest.fixture
125
+ def empty_state() -> dict:
126
+ """Initial empty state for testing."""
127
+ return create_initial_state(
128
+ topic="test topic",
129
+ style="casual",
130
+ word_count=200,
131
+ )
132
+
133
+
134
+ @pytest.fixture
135
+ def temp_db(tmp_path: Path) -> Generator[YamlGraphDB, None, None]:
136
+ """Temporary database for testing."""
137
+ db_path = tmp_path / "test.db"
138
+ db = YamlGraphDB(db_path=db_path)
139
+ yield db
140
+
141
+
142
+ @pytest.fixture
143
+ def temp_output_dir(tmp_path: Path) -> Path:
144
+ """Temporary output directory for testing."""
145
+ output_dir = tmp_path / "outputs"
146
+ output_dir.mkdir()
147
+ return output_dir
148
+
149
+
150
+ @pytest.fixture
151
+ def mock_llm_response():
152
+ """Mock LLM that returns predictable responses."""
153
+
154
+ def _create_mock(response_content: str | dict = "Mocked response"):
155
+ mock = MagicMock()
156
+ mock_response = MagicMock()
157
+ mock_response.content = response_content
158
+ mock.invoke.return_value = mock_response
159
+ return mock
160
+
161
+ return _create_mock
162
+
163
+
164
+ @pytest.fixture
165
+ def mock_structured_llm(sample_generated_content, sample_analysis):
166
+ """Mock LLM with structured output support."""
167
+
168
+ def _create_mock(model_type: str):
169
+ mock = MagicMock()
170
+ if model_type == "generate":
171
+ mock.invoke.return_value = sample_generated_content
172
+ elif model_type == "analyze":
173
+ mock.invoke.return_value = sample_analysis
174
+ else:
175
+ mock.invoke.return_value = "Mocked summary"
176
+ return mock
177
+
178
+ return _create_mock
@@ -0,0 +1 @@
1
+ """Integration tests for yamlgraph."""
@@ -0,0 +1,63 @@
1
+ """Tests for animated storyboard graph."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
6
+
7
+
8
+ class TestAnimatedStoryboardGraph:
9
+ """Tests for the animated-character-storyboard graph."""
10
+
11
+ def test_config_loads(self) -> None:
12
+ """Animated character storyboard config loads successfully."""
13
+ config = load_graph_config("examples/storyboard/animated-character-graph.yaml")
14
+ assert config.name == "animated-character-storyboard"
15
+ assert "expand_story" in config.nodes
16
+ assert "animate_panels" in config.nodes
17
+ assert "generate_images" in config.nodes
18
+
19
+ def test_animate_panels_is_map_node(self) -> None:
20
+ """animate_panels node is type: map."""
21
+ config = load_graph_config("examples/storyboard/animated-character-graph.yaml")
22
+ animate_node = config.nodes["animate_panels"]
23
+
24
+ assert animate_node["type"] == "map"
25
+ assert animate_node["over"] == "{state.story.panels}"
26
+ assert animate_node["as"] == "panel_prompt"
27
+ assert animate_node["collect"] == "animated_panels"
28
+
29
+ def test_graph_compiles(self) -> None:
30
+ """Animated character storyboard graph compiles to StateGraph."""
31
+ config = load_graph_config("examples/storyboard/animated-character-graph.yaml")
32
+
33
+ with patch("yamlgraph.graph_loader.compile_map_node") as mock_compile_map:
34
+ mock_map_edge_fn = MagicMock()
35
+ mock_compile_map.return_value = (
36
+ mock_map_edge_fn,
37
+ "_map_animate_panels_sub",
38
+ )
39
+
40
+ compile_graph(config)
41
+
42
+ # Should have called compile_map_node for animate_panels
43
+ mock_compile_map.assert_called_once()
44
+ call_args = mock_compile_map.call_args
45
+ assert call_args[0][0] == "animate_panels"
46
+
47
+ def test_state_has_animated_panels_sorted_reducer(self) -> None:
48
+ """State class has sorted_add reducer for animated_panels."""
49
+ from typing import Annotated, get_args, get_origin
50
+
51
+ from yamlgraph.models.state_builder import build_state_class, sorted_add
52
+
53
+ config = load_graph_config("examples/storyboard/animated-character-graph.yaml")
54
+ state_class = build_state_class(config.raw_config)
55
+
56
+ annotations = state_class.__annotations__
57
+ assert "animated_panels" in annotations
58
+
59
+ field_type = annotations["animated_panels"]
60
+ assert get_origin(field_type) is Annotated
61
+ args = get_args(field_type)
62
+ assert args[0] is list
63
+ assert args[1] is sorted_add
@@ -0,0 +1,242 @@
1
+ """Integration tests for CLI commands.
2
+
3
+ Tests actual command execution with real (but minimal) operations.
4
+ """
5
+
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ class TestGraphCommands:
12
+ """Integration tests for graph subcommands."""
13
+
14
+ def test_graph_list_returns_graphs(self):
15
+ """'graph list' shows available graphs."""
16
+ result = subprocess.run(
17
+ [sys.executable, "-m", "yamlgraph.cli", "graph", "list"],
18
+ capture_output=True,
19
+ text=True,
20
+ cwd=Path(__file__).parent.parent.parent,
21
+ )
22
+ assert result.returncode == 0
23
+ assert "yamlgraph.yaml" in result.stdout
24
+ assert "Available graphs" in result.stdout
25
+
26
+ def test_graph_list_shows_all_demos(self):
27
+ """'graph list' shows all demo graphs."""
28
+ result = subprocess.run(
29
+ [sys.executable, "-m", "yamlgraph.cli", "graph", "list"],
30
+ capture_output=True,
31
+ text=True,
32
+ cwd=Path(__file__).parent.parent.parent,
33
+ )
34
+ assert result.returncode == 0
35
+ # Should list all main demo graphs
36
+ assert "router-demo.yaml" in result.stdout
37
+ assert "reflexion-demo.yaml" in result.stdout
38
+ assert "git-report.yaml" in result.stdout
39
+
40
+ def test_graph_validate_valid_graph(self):
41
+ """'graph validate' succeeds for valid graph."""
42
+ result = subprocess.run(
43
+ [
44
+ sys.executable,
45
+ "-m",
46
+ "yamlgraph.cli",
47
+ "graph",
48
+ "validate",
49
+ "graphs/yamlgraph.yaml",
50
+ ],
51
+ capture_output=True,
52
+ text=True,
53
+ cwd=Path(__file__).parent.parent.parent,
54
+ )
55
+ assert result.returncode == 0
56
+ assert "VALID" in result.stdout
57
+
58
+ def test_graph_validate_all_demos(self):
59
+ """'graph validate' succeeds for all demo graphs."""
60
+ demos = [
61
+ "graphs/yamlgraph.yaml",
62
+ "graphs/router-demo.yaml",
63
+ "graphs/reflexion-demo.yaml",
64
+ "graphs/git-report.yaml",
65
+ "graphs/memory-demo.yaml",
66
+ "graphs/map-demo.yaml",
67
+ ]
68
+ for demo in demos:
69
+ result = subprocess.run(
70
+ [sys.executable, "-m", "yamlgraph.cli", "graph", "validate", demo],
71
+ capture_output=True,
72
+ text=True,
73
+ cwd=Path(__file__).parent.parent.parent,
74
+ )
75
+ assert result.returncode == 0, f"Failed to validate {demo}: {result.stderr}"
76
+
77
+ def test_graph_validate_invalid_path(self):
78
+ """'graph validate' fails for missing file."""
79
+ result = subprocess.run(
80
+ [
81
+ sys.executable,
82
+ "-m",
83
+ "yamlgraph.cli",
84
+ "graph",
85
+ "validate",
86
+ "nonexistent.yaml",
87
+ ],
88
+ capture_output=True,
89
+ text=True,
90
+ cwd=Path(__file__).parent.parent.parent,
91
+ )
92
+ assert result.returncode != 0
93
+
94
+ def test_graph_info_shows_nodes(self):
95
+ """'graph info' shows node details."""
96
+ result = subprocess.run(
97
+ [
98
+ sys.executable,
99
+ "-m",
100
+ "yamlgraph.cli",
101
+ "graph",
102
+ "info",
103
+ "graphs/yamlgraph.yaml",
104
+ ],
105
+ capture_output=True,
106
+ text=True,
107
+ cwd=Path(__file__).parent.parent.parent,
108
+ )
109
+ assert result.returncode == 0
110
+ assert "Nodes:" in result.stdout or "nodes" in result.stdout.lower()
111
+
112
+ def test_graph_info_shows_edges(self):
113
+ """'graph info' shows edge details."""
114
+ result = subprocess.run(
115
+ [
116
+ sys.executable,
117
+ "-m",
118
+ "yamlgraph.cli",
119
+ "graph",
120
+ "info",
121
+ "graphs/yamlgraph.yaml",
122
+ ],
123
+ capture_output=True,
124
+ text=True,
125
+ cwd=Path(__file__).parent.parent.parent,
126
+ )
127
+ assert result.returncode == 0
128
+ assert "Edges:" in result.stdout or "edges" in result.stdout.lower()
129
+
130
+ def test_graph_info_router_demo(self):
131
+ """'graph info' shows router-demo structure."""
132
+ result = subprocess.run(
133
+ [
134
+ sys.executable,
135
+ "-m",
136
+ "yamlgraph.cli",
137
+ "graph",
138
+ "info",
139
+ "graphs/router-demo.yaml",
140
+ ],
141
+ capture_output=True,
142
+ text=True,
143
+ cwd=Path(__file__).parent.parent.parent,
144
+ )
145
+ assert result.returncode == 0
146
+ assert "classify" in result.stdout
147
+ assert "router" in result.stdout.lower()
148
+
149
+ def test_graph_run_nonexistent_file_shows_error(self):
150
+ """'graph run' with nonexistent file shows error."""
151
+ result = subprocess.run(
152
+ [
153
+ sys.executable,
154
+ "-m",
155
+ "yamlgraph.cli",
156
+ "graph",
157
+ "run",
158
+ "graphs/does-not-exist.yaml",
159
+ ],
160
+ capture_output=True,
161
+ text=True,
162
+ cwd=Path(__file__).parent.parent.parent,
163
+ )
164
+ # Should fail with file not found
165
+ assert result.returncode != 0
166
+ assert (
167
+ "not found" in result.stdout.lower() + result.stderr.lower()
168
+ or "Error" in result.stdout + result.stderr
169
+ )
170
+
171
+ def test_graph_run_invalid_var_format(self):
172
+ """'graph run' with invalid --var format shows error."""
173
+ result = subprocess.run(
174
+ [
175
+ sys.executable,
176
+ "-m",
177
+ "yamlgraph.cli",
178
+ "graph",
179
+ "run",
180
+ "graphs/yamlgraph.yaml",
181
+ "--var",
182
+ "invalid_no_equals",
183
+ ],
184
+ capture_output=True,
185
+ text=True,
186
+ cwd=Path(__file__).parent.parent.parent,
187
+ )
188
+ assert result.returncode != 0
189
+ assert (
190
+ "Invalid" in result.stdout + result.stderr
191
+ or "key=value" in result.stdout + result.stderr
192
+ )
193
+
194
+
195
+ class TestListRunsCommand:
196
+ """Integration tests for list-runs command."""
197
+
198
+ def test_list_runs_with_empty_db(self, tmp_path, monkeypatch):
199
+ """'list-runs' handles empty database gracefully."""
200
+ # Use temp db
201
+ monkeypatch.setenv("SHOWCASE_DB_PATH", str(tmp_path / "test.db"))
202
+
203
+ result = subprocess.run(
204
+ [sys.executable, "-m", "yamlgraph.cli", "list-runs"],
205
+ capture_output=True,
206
+ text=True,
207
+ cwd=Path(__file__).parent.parent.parent,
208
+ env={
209
+ **subprocess.os.environ,
210
+ "SHOWCASE_DB_PATH": str(tmp_path / "test.db"),
211
+ },
212
+ )
213
+ # Should succeed even with no runs
214
+ assert result.returncode == 0
215
+ assert "No runs found" in result.stdout or "runs" in result.stdout.lower()
216
+
217
+
218
+ class TestHelpOutput:
219
+ """Test help messages work correctly."""
220
+
221
+ def test_main_help(self):
222
+ """Main --help shows available commands."""
223
+ result = subprocess.run(
224
+ [sys.executable, "-m", "yamlgraph.cli", "--help"],
225
+ capture_output=True,
226
+ text=True,
227
+ )
228
+ assert result.returncode == 0
229
+ assert "graph" in result.stdout
230
+ assert "list-runs" in result.stdout
231
+
232
+ def test_graph_help(self):
233
+ """'graph --help' shows subcommands."""
234
+ result = subprocess.run(
235
+ [sys.executable, "-m", "yamlgraph.cli", "graph", "--help"],
236
+ capture_output=True,
237
+ text=True,
238
+ )
239
+ assert result.returncode == 0
240
+ assert "run" in result.stdout
241
+ assert "list" in result.stdout
242
+ assert "validate" in result.stdout
@@ -0,0 +1,50 @@
1
+ """Integration tests for map-demo graph."""
2
+
3
+ from unittest.mock import MagicMock, patch
4
+
5
+ from yamlgraph.graph_loader import compile_graph, load_graph_config
6
+
7
+
8
+ class TestMapDemoGraph:
9
+ """Integration tests for the map-demo graph."""
10
+
11
+ def test_map_demo_config_loads(self) -> None:
12
+ """Map demo graph config loads successfully."""
13
+ config = load_graph_config("graphs/map-demo.yaml")
14
+ assert config.name == "map-demo"
15
+ assert "expand" in config.nodes
16
+ assert config.nodes["expand"]["type"] == "map"
17
+
18
+ def test_map_demo_graph_compiles(self) -> None:
19
+ """Map demo graph compiles to StateGraph."""
20
+ config = load_graph_config("graphs/map-demo.yaml")
21
+
22
+ # Mock compile_map_node to avoid needing prompt execution
23
+ with patch("yamlgraph.graph_loader.compile_map_node") as mock_compile_map:
24
+ mock_map_edge_fn = MagicMock()
25
+ mock_compile_map.return_value = (mock_map_edge_fn, "_map_expand_sub")
26
+
27
+ compile_graph(config)
28
+
29
+ # Should have called compile_map_node for expand
30
+ mock_compile_map.assert_called_once()
31
+ call_args = mock_compile_map.call_args
32
+ assert call_args[0][0] == "expand"
33
+
34
+ def test_map_demo_state_has_sorted_reducer(self) -> None:
35
+ """Map demo compiled state has sorted_add reducer for expansions."""
36
+ from typing import Annotated, get_args, get_origin
37
+
38
+ from yamlgraph.models.state_builder import build_state_class, sorted_add
39
+
40
+ config = load_graph_config("graphs/map-demo.yaml")
41
+ state_class = build_state_class(config.raw_config)
42
+
43
+ annotations = state_class.__annotations__
44
+ assert "expansions" in annotations
45
+
46
+ field_type = annotations["expansions"]
47
+ assert get_origin(field_type) is Annotated
48
+ args = get_args(field_type)
49
+ assert args[0] is list
50
+ assert args[1] is sorted_add