CmdForge/tests/test_dependency_graph.py

294 lines
8.5 KiB
Python

"""Tests for dependency_graph.py - Transitive dependency resolution."""
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch
import tempfile
import yaml
from cmdforge.dependency_graph import (
DependencyNode,
DependencyGraph,
DependencyGraphBuilder,
ResolutionResult,
resolve_dependencies
)
from cmdforge.manifest import Dependency
class TestDependencyNode:
"""Tests for DependencyNode dataclass."""
def test_qualified_name_with_owner(self):
node = DependencyNode(
owner="official",
name="summarize",
version_constraint="^1.0.0",
resolved_version="1.2.3",
source="global",
path=Path("/test")
)
assert node.qualified_name == "official/summarize"
def test_qualified_name_without_owner(self):
node = DependencyNode(
owner="",
name="summarize",
version_constraint="*",
resolved_version=None,
source=None,
path=None
)
assert node.qualified_name == "summarize"
def test_is_resolved(self):
resolved = DependencyNode(
owner="official",
name="test",
version_constraint="*",
resolved_version="1.0.0",
source="global",
path=Path("/test")
)
assert resolved.is_resolved
unresolved = DependencyNode(
owner="official",
name="test",
version_constraint="*",
resolved_version=None,
source=None,
path=None
)
assert not unresolved.is_resolved
class TestDependencyGraph:
"""Tests for DependencyGraph dataclass."""
def test_add_node(self):
graph = DependencyGraph()
node = DependencyNode(
owner="official",
name="test",
version_constraint="*",
resolved_version="1.0.0",
source="global",
path=Path("/test")
)
graph.add_node(node)
assert "official/test" in graph.nodes
def test_get_node(self):
graph = DependencyGraph()
node = DependencyNode(
owner="official",
name="test",
version_constraint="*",
resolved_version="1.0.0",
source="global",
path=Path("/test")
)
graph.add_node(node)
assert graph.get_node("official/test") == node
assert graph.get_node("nonexistent") is None
def test_version_conflict_detection(self):
graph = DependencyGraph()
node1 = DependencyNode(
owner="official",
name="test",
version_constraint="^1.0.0",
resolved_version="1.0.0",
source="global",
path=Path("/test"),
requested_by="root"
)
graph.add_node(node1, requested_by="root")
node2 = DependencyNode(
owner="official",
name="test",
version_constraint="^2.0.0",
resolved_version="2.0.0",
source="global",
path=Path("/test"),
requested_by="other"
)
graph.add_node(node2, requested_by="other")
assert graph.has_conflicts()
assert len(graph.conflicts) == 1
def test_get_unresolved(self):
graph = DependencyGraph()
resolved = DependencyNode(
owner="official",
name="resolved",
version_constraint="*",
resolved_version="1.0.0",
source="global",
path=Path("/test")
)
unresolved = DependencyNode(
owner="official",
name="unresolved",
version_constraint="*",
resolved_version=None,
source=None,
path=None
)
graph.add_node(resolved)
graph.add_node(unresolved)
unresolved_list = graph.get_unresolved()
assert len(unresolved_list) == 1
assert unresolved_list[0].name == "unresolved"
class TestDependencyGraphBuilder:
"""Tests for DependencyGraphBuilder class."""
def test_build_empty_deps(self):
builder = DependencyGraphBuilder(verbose=False)
graph = builder.build([])
assert len(graph.nodes) == 0
def test_topological_sort_simple(self):
"""Test that dependencies come before dependents."""
graph = DependencyGraph()
# A depends on B
node_a = DependencyNode(
owner="official",
name="a",
version_constraint="*",
resolved_version="1.0.0",
source="global",
path=Path("/a"),
children=["official/b"]
)
node_b = DependencyNode(
owner="official",
name="b",
version_constraint="*",
resolved_version="1.0.0",
source="global",
path=Path("/b"),
children=[]
)
graph.add_node(node_a)
graph.add_node(node_b)
graph.root_dependencies = ["official/a"]
builder = DependencyGraphBuilder()
order = builder._topological_sort(graph)
# B should come before A since A depends on B
b_idx = order.index("official/b")
a_idx = order.index("official/a")
assert b_idx < a_idx
def test_cycle_detection(self):
"""Test that cycles are detected."""
# This requires mocking the resolver to create a cycle
# For now, just verify the cycle list exists
graph = DependencyGraph()
assert not graph.has_cycles()
graph.cycles.append(["a", "b", "a"])
assert graph.has_cycles()
class TestResolveDepencies:
"""Tests for resolve_dependencies convenience function."""
@patch('cmdforge.dependency_graph.DependencyGraphBuilder.build')
def test_categorizes_results(self, mock_build):
"""Test that results are properly categorized."""
# Create mock graph
mock_graph = DependencyGraph()
mock_graph.resolution_order = ["official/test"]
node = DependencyNode(
owner="official",
name="test",
version_constraint="*",
resolved_version="1.0.0",
source="registry",
path=None # Not installed yet
)
mock_graph.nodes["official/test"] = node
mock_build.return_value = mock_graph
deps = [Dependency(name="official/test", version="*")]
result = resolve_dependencies(deps)
assert isinstance(result, ResolutionResult)
assert len(result.to_install) == 1
assert result.to_install[0].name == "test"
class TestIntegration:
"""Integration tests with real file system."""
def test_reads_local_tool_deps(self, tmp_path):
"""Test that dependencies can be read from a local tool config."""
# Create a tool config with dependencies
tool_dir = tmp_path / "test-tool"
tool_dir.mkdir()
config = {
"name": "test-tool",
"description": "Test tool",
"dependencies": [
{"name": "official/helper", "version": "^1.0.0"}
],
"steps": [
{"type": "prompt", "prompt": "test", "output_var": "result"}
]
}
(tool_dir / "config.yaml").write_text(yaml.dump(config))
# Create a node pointing to this tool
node = DependencyNode(
owner="local",
name="test-tool",
version_constraint="*",
resolved_version="1.0.0",
source="local",
path=tool_dir
)
builder = DependencyGraphBuilder()
deps = builder._read_deps_from_config(tool_dir)
assert len(deps) == 1
assert deps[0].name == "official/helper"
assert deps[0].version == "^1.0.0"
def test_reads_implicit_tool_deps(self, tmp_path):
"""Test that ToolStep references are discovered as dependencies."""
tool_dir = tmp_path / "meta-tool"
tool_dir.mkdir()
config = {
"name": "meta-tool",
"description": "Meta tool that calls others",
"steps": [
{
"type": "tool",
"tool": "official/summarize",
"output_var": "summary"
}
]
}
(tool_dir / "config.yaml").write_text(yaml.dump(config))
builder = DependencyGraphBuilder()
deps = builder._read_deps_from_config(tool_dir)
assert len(deps) == 1
assert deps[0].name == "official/summarize"