Files
markitect-main/tests/unit/prompts/test_graph_builder.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

232 lines
7.8 KiB
Python

"""
Unit tests for GraphBuilder.
Tests extracting edges from manifest, persisting them,
building graphs, and artifact-scoped subgraphs.
"""
import pytest
import tempfile
from pathlib import Path
from markitect.prompts.dependencies.models import (
DependencyEdge,
DependencyGraph,
EdgeType,
)
from markitect.prompts.dependencies.repository import SQLiteDependencyRepository
from markitect.prompts.dependencies.graph import GraphBuilder
from markitect.prompts.execution.manifest import (
DependencyEdge as ManifestDependencyEdge,
RunManifest,
)
@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 builder(repository):
"""Create GraphBuilder instance."""
return GraphBuilder(repository)
@pytest.fixture
def sample_manifest():
"""Create a sample RunManifest with dependency edges."""
manifest = RunManifest.create(
run_id="run-1",
template_id="template-1",
template_name="test-template",
template_digest="abc123",
)
manifest.add_dependency_edge("artifact-1", "artifact-2", "requires")
manifest.add_dependency_edge("artifact-2", "artifact-3", "generates")
manifest.add_dependency_edge("artifact-1", "artifact-3", "includes")
return manifest
class TestGraphBuilderExtract:
"""Tests for extracting edges from manifests."""
def test_extract_edges_from_manifest(self, builder, sample_manifest):
"""Test extracting persistent edges from a manifest."""
edges = builder.extract_edges(sample_manifest)
assert len(edges) == 3
assert all(isinstance(e, DependencyEdge) for e in edges)
def test_extracted_edge_fields(self, builder, sample_manifest):
"""Test extracted edges have correct fields."""
edges = builder.extract_edges(sample_manifest)
# First edge: artifact-1 -> artifact-2, requires
edge = edges[0]
assert edge.source_artifact_id == "artifact-1"
assert edge.target_artifact_id == "artifact-2"
assert edge.run_id == "run-1"
assert edge.edge_type == EdgeType.REQUIRES
assert edge.id # UUID assigned
def test_extracted_edges_have_unique_ids(self, builder, sample_manifest):
"""Test each extracted edge gets a unique ID."""
edges = builder.extract_edges(sample_manifest)
ids = [e.id for e in edges]
assert len(set(ids)) == len(ids)
def test_extract_edges_empty_manifest(self, builder):
"""Test extracting from manifest with no edges."""
manifest = RunManifest.create(
run_id="run-empty",
template_id="template-1",
template_name="empty",
template_digest="xyz",
)
edges = builder.extract_edges(manifest)
assert edges == []
def test_extract_edges_preserves_types(self, builder, sample_manifest):
"""Test all edge types are correctly mapped."""
edges = builder.extract_edges(sample_manifest)
types = [e.edge_type for e in edges]
assert EdgeType.REQUIRES in types
assert EdgeType.GENERATES in types
assert EdgeType.INCLUDES in types
class TestGraphBuilderPersist:
"""Tests for persisting edges from manifests."""
def test_persist_edges(self, builder, repository, sample_manifest):
"""Test persisting edges to repository."""
persisted = builder.persist_edges(sample_manifest)
assert len(persisted) == 3
# Verify in repository
all_edges = repository.get_all()
assert len(all_edges) == 3
def test_persisted_edges_retrievable(self, builder, repository, sample_manifest):
"""Test persisted edges can be retrieved from repository."""
builder.persist_edges(sample_manifest)
edges_from_source = repository.get_by_source("artifact-1")
assert len(edges_from_source) == 2 # artifact-1 -> artifact-2 and artifact-3
edges_from_run = repository.get_by_run("run-1")
assert len(edges_from_run) == 3
class TestGraphBuilderBuildGraph:
"""Tests for building dependency graphs."""
def test_build_graph_from_all_edges(self, builder, repository):
"""Test building graph from all stored edges."""
# Manually create some edges
for src, tgt in [("A", "B"), ("B", "C"), ("A", "C")]:
edge = DependencyEdge.create(
source_artifact_id=src,
target_artifact_id=tgt,
run_id="run-1",
edge_type=EdgeType.REQUIRES,
)
repository.create(edge)
graph = builder.build_graph()
assert len(graph.nodes) == 3
assert graph.has_edge("A", "B")
assert graph.has_edge("B", "C")
assert graph.has_edge("A", "C")
def test_build_graph_scoped(self, builder, repository):
"""Test building scoped graph with subset of artifacts."""
for src, tgt in [("A", "B"), ("B", "C"), ("C", "D")]:
edge = DependencyEdge.create(
source_artifact_id=src,
target_artifact_id=tgt,
run_id="run-1",
edge_type=EdgeType.REQUIRES,
)
repository.create(edge)
graph = builder.build_graph(artifact_ids={"A", "B", "C"})
assert graph.has_edge("A", "B")
assert graph.has_edge("B", "C")
assert not graph.has_edge("C", "D")
assert "D" not in graph.nodes
def test_build_graph_scoped_includes_isolated_nodes(self, builder, repository):
"""Test scoped graph includes nodes with no edges in scope."""
edge = DependencyEdge.create(
source_artifact_id="A",
target_artifact_id="B",
run_id="run-1",
edge_type=EdgeType.REQUIRES,
)
repository.create(edge)
graph = builder.build_graph(artifact_ids={"A", "B", "C"})
assert "C" in graph.nodes
def test_build_graph_empty(self, builder):
"""Test building graph from empty repository."""
graph = builder.build_graph()
assert len(graph.nodes) == 0
assert graph.edge_count == 0
def test_build_graph_for_run(self, builder, repository):
"""Test building graph from a specific run's edges."""
# Run 1 edges
for src, tgt in [("A", "B"), ("B", "C")]:
edge = DependencyEdge.create(
source_artifact_id=src,
target_artifact_id=tgt,
run_id="run-1",
edge_type=EdgeType.REQUIRES,
)
repository.create(edge)
# Run 2 edges
edge = DependencyEdge.create(
source_artifact_id="X",
target_artifact_id="Y",
run_id="run-2",
edge_type=EdgeType.GENERATES,
)
repository.create(edge)
graph = builder.build_graph_for_run("run-1")
assert graph.has_edge("A", "B")
assert graph.has_edge("B", "C")
assert not graph.has_edge("X", "Y")
def test_build_graph_preserves_edge_types(self, builder, repository):
"""Test built graph preserves edge types."""
edge = DependencyEdge.create(
source_artifact_id="A",
target_artifact_id="B",
run_id="run-1",
edge_type=EdgeType.GENERATES,
)
repository.create(edge)
graph = builder.build_graph()
assert graph.get_edge_type("A", "B") == EdgeType.GENERATES