"""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"