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.
- examples/__init__.py +1 -0
- examples/storyboard/__init__.py +1 -0
- examples/storyboard/generate_videos.py +335 -0
- examples/storyboard/nodes/__init__.py +10 -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 +238 -0
- examples/storyboard/retry_images.py +118 -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_map_demo.py +50 -0
- tests/integration/test_memory_demo.py +281 -0
- tests/integration/test_pipeline_flow.py +105 -0
- tests/integration/test_providers.py +163 -0
- tests/integration/test_resume.py +75 -0
- tests/unit/__init__.py +1 -0
- tests/unit/test_agent_nodes.py +200 -0
- tests/unit/test_checkpointer.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 +270 -0
- tests/unit/test_database.py +145 -0
- tests/unit/test_deprecation.py +104 -0
- tests/unit/test_executor.py +60 -0
- tests/unit/test_executor_async.py +179 -0
- tests/unit/test_export.py +150 -0
- tests/unit/test_expressions.py +178 -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_loader.py +299 -0
- tests/unit/test_graph_schema.py +193 -0
- tests/unit/test_inline_schema.py +151 -0
- tests/unit/test_issues.py +164 -0
- tests/unit/test_jinja2_prompts.py +85 -0
- tests/unit/test_langsmith.py +319 -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 +225 -0
- tests/unit/test_prompts.py +166 -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_template.py +190 -0
- tests/unit/test_tool_nodes.py +129 -0
- yamlgraph/__init__.py +35 -0
- yamlgraph/builder.py +110 -0
- yamlgraph/cli/__init__.py +139 -0
- yamlgraph/cli/__main__.py +6 -0
- yamlgraph/cli/commands.py +232 -0
- yamlgraph/cli/deprecation.py +92 -0
- yamlgraph/cli/graph_commands.py +382 -0
- yamlgraph/cli/validators.py +37 -0
- yamlgraph/config.py +67 -0
- yamlgraph/constants.py +66 -0
- yamlgraph/error_handlers.py +226 -0
- yamlgraph/executor.py +275 -0
- yamlgraph/executor_async.py +122 -0
- yamlgraph/graph_loader.py +337 -0
- yamlgraph/map_compiler.py +138 -0
- yamlgraph/models/__init__.py +36 -0
- yamlgraph/models/graph_schema.py +141 -0
- yamlgraph/models/schemas.py +124 -0
- yamlgraph/models/state_builder.py +236 -0
- yamlgraph/node_factory.py +240 -0
- yamlgraph/routing.py +87 -0
- yamlgraph/schema_loader.py +160 -0
- yamlgraph/storage/__init__.py +17 -0
- yamlgraph/storage/checkpointer.py +72 -0
- yamlgraph/storage/database.py +320 -0
- yamlgraph/storage/export.py +269 -0
- yamlgraph/tools/__init__.py +1 -0
- yamlgraph/tools/agent.py +235 -0
- yamlgraph/tools/nodes.py +124 -0
- yamlgraph/tools/python_tool.py +178 -0
- yamlgraph/tools/shell.py +205 -0
- yamlgraph/utils/__init__.py +47 -0
- yamlgraph/utils/conditions.py +157 -0
- yamlgraph/utils/expressions.py +111 -0
- yamlgraph/utils/langsmith.py +308 -0
- yamlgraph/utils/llm_factory.py +118 -0
- yamlgraph/utils/llm_factory_async.py +105 -0
- yamlgraph/utils/logging.py +127 -0
- yamlgraph/utils/prompts.py +116 -0
- yamlgraph/utils/sanitize.py +98 -0
- yamlgraph/utils/template.py +102 -0
- yamlgraph/utils/validators.py +181 -0
- yamlgraph-0.1.1.dist-info/METADATA +854 -0
- yamlgraph-0.1.1.dist-info/RECORD +111 -0
- yamlgraph-0.1.1.dist-info/WHEEL +5 -0
- yamlgraph-0.1.1.dist-info/entry_points.txt +2 -0
- yamlgraph-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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
|