Files
markitect-main/tests/unit/prompts/test_dependency_queries.py
tegwick 9ce157400e feat(prompts): implement Phase 5 - Dependency Tracking (FR-6)
Add directed dependency graph with cycle detection, topological sort,
and query service for finding dependents/dependencies transitively.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:18:18 +01:00

325 lines
12 KiB
Python

"""
Unit tests for DependencyQueryService.
Tests direct/transitive dependents and dependencies, dependency chains,
cycle validation, would_create_cycle, and build order.
"""
import pytest
import tempfile
from pathlib import Path
from markitect.prompts.dependencies.models import (
DependencyEdge,
DependencyGraph,
EdgeType,
CircularDependencyError,
)
from markitect.prompts.dependencies.repository import SQLiteDependencyRepository
from markitect.prompts.dependencies.queries import DependencyQueryService
@pytest.fixture
def temp_db():
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
yield db_path
Path(db_path).unlink(missing_ok=True)
@pytest.fixture
def repository(temp_db):
"""Create repository instance with temp database."""
return SQLiteDependencyRepository(temp_db)
@pytest.fixture
def query_service(repository):
"""Create DependencyQueryService instance."""
return DependencyQueryService(repository)
def _create_edge(repository, src, tgt, run_id="run-1", edge_type=EdgeType.REQUIRES):
"""Helper to create and persist a dependency edge."""
edge = DependencyEdge.create(
source_artifact_id=src,
target_artifact_id=tgt,
run_id=run_id,
edge_type=edge_type,
)
return repository.create(edge)
class TestFindDependents:
"""Tests for find_dependents (who depends on X)."""
def test_find_direct_dependents(self, repository, query_service):
"""Test finding direct dependents of an artifact."""
_create_edge(repository, "A", "B")
_create_edge(repository, "C", "B")
dependents = query_service.find_dependents("B")
assert dependents == {"A", "C"}
def test_find_dependents_none(self, repository, query_service):
"""Test finding dependents when none exist."""
_create_edge(repository, "A", "B")
dependents = query_service.find_dependents("A")
assert dependents == set()
def test_find_dependents_nonexistent(self, query_service):
"""Test finding dependents of non-existent artifact."""
dependents = query_service.find_dependents("nonexistent")
assert dependents == set()
class TestFindDependencies:
"""Tests for find_dependencies (what X depends on)."""
def test_find_direct_dependencies(self, repository, query_service):
"""Test finding direct dependencies of an artifact."""
_create_edge(repository, "A", "B")
_create_edge(repository, "A", "C")
dependencies = query_service.find_dependencies("A")
assert dependencies == {"B", "C"}
def test_find_dependencies_none(self, repository, query_service):
"""Test finding dependencies when none exist."""
_create_edge(repository, "A", "B")
dependencies = query_service.find_dependencies("B")
assert dependencies == set()
class TestTransitiveDependents:
"""Tests for find_transitive_dependents."""
def test_transitive_dependents(self, repository, query_service):
"""Test finding transitive dependents (upstream impact)."""
# A -> B -> C
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
# Who is transitively affected by C?
transitive = query_service.find_transitive_dependents("C")
assert transitive == {"A", "B"}
def test_transitive_dependents_diamond(self, repository, query_service):
"""Test transitive dependents in diamond graph."""
# A -> B -> D, A -> C -> D
_create_edge(repository, "A", "B")
_create_edge(repository, "A", "C")
_create_edge(repository, "B", "D")
_create_edge(repository, "C", "D")
transitive = query_service.find_transitive_dependents("D")
assert transitive == {"A", "B", "C"}
def test_transitive_dependents_none(self, repository, query_service):
"""Test transitive dependents when none exist."""
_create_edge(repository, "A", "B")
transitive = query_service.find_transitive_dependents("A")
assert transitive == set()
class TestTransitiveDependencies:
"""Tests for find_transitive_dependencies."""
def test_transitive_dependencies(self, repository, query_service):
"""Test finding transitive dependencies (full dep tree)."""
# A -> B -> C
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
# What does A transitively depend on?
transitive = query_service.find_transitive_dependencies("A")
assert transitive == {"B", "C"}
def test_transitive_dependencies_complex(self, repository, query_service):
"""Test transitive dependencies in complex graph."""
# A -> B -> D, A -> C -> D, D -> E
_create_edge(repository, "A", "B")
_create_edge(repository, "A", "C")
_create_edge(repository, "B", "D")
_create_edge(repository, "C", "D")
_create_edge(repository, "D", "E")
transitive = query_service.find_transitive_dependencies("A")
assert transitive == {"B", "C", "D", "E"}
def test_transitive_dependencies_leaf(self, repository, query_service):
"""Test transitive dependencies of a leaf node."""
_create_edge(repository, "A", "B")
transitive = query_service.find_transitive_dependencies("B")
assert transitive == set()
class TestDependencyChain:
"""Tests for get_dependency_chain (BFS path finding)."""
def test_direct_chain(self, repository, query_service):
"""Test finding a direct dependency chain."""
_create_edge(repository, "A", "B")
chain = query_service.get_dependency_chain("A", "B")
assert chain == ["A", "B"]
def test_multi_hop_chain(self, repository, query_service):
"""Test finding a multi-hop dependency chain."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
_create_edge(repository, "C", "D")
chain = query_service.get_dependency_chain("A", "D")
assert chain is not None
assert chain[0] == "A"
assert chain[-1] == "D"
assert len(chain) == 4
def test_no_chain_exists(self, repository, query_service):
"""Test when no chain exists between artifacts."""
_create_edge(repository, "A", "B")
_create_edge(repository, "C", "D")
chain = query_service.get_dependency_chain("A", "D")
assert chain is None
def test_same_source_target(self, query_service):
"""Test chain from node to itself."""
chain = query_service.get_dependency_chain("A", "A")
assert chain == ["A"]
def test_shortest_chain(self, repository, query_service):
"""Test BFS finds shortest chain."""
# A -> B -> C (length 3)
# A -> C (length 2, shorter)
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
_create_edge(repository, "A", "C")
chain = query_service.get_dependency_chain("A", "C")
assert chain == ["A", "C"]
class TestCircularDependencyDetection:
"""Tests for detect_circular_dependencies."""
def test_no_cycles(self, repository, query_service):
"""Test detection when no cycles exist."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
cycles = query_service.detect_circular_dependencies()
assert cycles == []
def test_detect_simple_cycle(self, repository, query_service):
"""Test detecting a simple cycle."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "A")
cycles = query_service.detect_circular_dependencies()
assert len(cycles) > 0
def test_detect_three_node_cycle(self, repository, query_service):
"""Test detecting a 3-node cycle."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
_create_edge(repository, "C", "A")
cycles = query_service.detect_circular_dependencies()
assert len(cycles) > 0
class TestWouldCreateCycle:
"""Tests for would_create_cycle pre-validation."""
def test_would_not_create_cycle(self, repository, query_service):
"""Test edge that would not create a cycle."""
_create_edge(repository, "A", "B")
assert query_service.would_create_cycle("B", "C") is False
def test_would_create_cycle_direct(self, repository, query_service):
"""Test edge that would create a direct cycle."""
_create_edge(repository, "A", "B")
assert query_service.would_create_cycle("B", "A") is True
def test_would_create_cycle_transitive(self, repository, query_service):
"""Test edge that would create a transitive cycle."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
assert query_service.would_create_cycle("C", "A") is True
def test_self_reference_creates_cycle(self, query_service):
"""Test self-reference always creates a cycle."""
assert query_service.would_create_cycle("A", "A") is True
def test_safe_parallel_edge(self, repository, query_service):
"""Test parallel forward edge is safe."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
# Adding A -> C doesn't create a cycle
assert query_service.would_create_cycle("A", "C") is False
class TestBuildOrder:
"""Tests for get_build_order (topological sort)."""
def test_simple_build_order(self, repository, query_service):
"""Test simple build order (dependencies before dependents)."""
# A requires B, B requires C → build C first, then B, then A
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
order = query_service.get_build_order()
assert order.index("C") < order.index("B")
assert order.index("B") < order.index("A")
def test_build_order_diamond(self, repository, query_service):
"""Test build order with diamond dependencies."""
# A requires B and C, B and C require D → build D first, then B/C, then A
_create_edge(repository, "A", "B")
_create_edge(repository, "A", "C")
_create_edge(repository, "B", "D")
_create_edge(repository, "C", "D")
order = query_service.get_build_order()
assert order.index("D") < order.index("B")
assert order.index("D") < order.index("C")
assert order.index("B") < order.index("A")
assert order.index("C") < order.index("A")
def test_build_order_scoped(self, repository, query_service):
"""Test build order with scoped artifact set."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
_create_edge(repository, "C", "D")
order = query_service.get_build_order(artifact_ids={"A", "B", "C"})
assert len(order) == 3
assert "D" not in order
assert order.index("C") < order.index("B")
assert order.index("B") < order.index("A")
def test_build_order_with_cycle_raises_error(self, repository, query_service):
"""Test build order raises error on cycle."""
_create_edge(repository, "A", "B")
_create_edge(repository, "B", "C")
_create_edge(repository, "C", "A")
with pytest.raises(CircularDependencyError):
query_service.get_build_order()
def test_build_order_empty(self, query_service):
"""Test build order with no edges."""
order = query_service.get_build_order()
assert order == []