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>
282 lines
9.5 KiB
Python
282 lines
9.5 KiB
Python
"""
|
|
Unit tests for dependency repository.
|
|
|
|
Tests CRUD operations, duplicate handling, and query by
|
|
source, target, run, and type.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from markitect.prompts.dependencies.models import DependencyEdge, EdgeType
|
|
from markitect.prompts.dependencies.repository import (
|
|
SQLiteDependencyRepository,
|
|
DuplicateDependencyError,
|
|
)
|
|
|
|
|
|
@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 sample_edge():
|
|
"""Create sample dependency edge for testing."""
|
|
return DependencyEdge.create(
|
|
source_artifact_id="artifact-1",
|
|
target_artifact_id="artifact-2",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
|
|
|
|
class TestSQLiteDependencyRepository:
|
|
"""Tests for SQLiteDependencyRepository."""
|
|
|
|
def test_create_edge(self, repository, sample_edge):
|
|
"""Test creating a dependency edge."""
|
|
created = repository.create(sample_edge)
|
|
assert created.id == sample_edge.id
|
|
assert created.source_artifact_id == "artifact-1"
|
|
assert created.target_artifact_id == "artifact-2"
|
|
|
|
def test_create_duplicate_edge_raises_error(self, repository):
|
|
"""Test creating duplicate edge raises error."""
|
|
edge1 = DependencyEdge.create(
|
|
source_artifact_id="a",
|
|
target_artifact_id="b",
|
|
run_id="r1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge1)
|
|
|
|
edge2 = DependencyEdge.create(
|
|
source_artifact_id="a",
|
|
target_artifact_id="b",
|
|
run_id="r1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
|
|
with pytest.raises(DuplicateDependencyError, match="already exists"):
|
|
repository.create(edge2)
|
|
|
|
def test_same_edge_different_runs_allowed(self, repository):
|
|
"""Test same source/target with different runs is allowed."""
|
|
edge1 = DependencyEdge.create(
|
|
source_artifact_id="a",
|
|
target_artifact_id="b",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
edge2 = DependencyEdge.create(
|
|
source_artifact_id="a",
|
|
target_artifact_id="b",
|
|
run_id="run-2",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge1)
|
|
repository.create(edge2) # Should not raise
|
|
|
|
def test_get_by_id(self, repository, sample_edge):
|
|
"""Test retrieving edge by ID."""
|
|
repository.create(sample_edge)
|
|
retrieved = repository.get_by_id(sample_edge.id)
|
|
|
|
assert retrieved is not None
|
|
assert retrieved.id == sample_edge.id
|
|
assert retrieved.source_artifact_id == sample_edge.source_artifact_id
|
|
assert retrieved.edge_type == sample_edge.edge_type
|
|
|
|
def test_get_by_id_not_found(self, repository):
|
|
"""Test retrieving non-existent edge returns None."""
|
|
assert repository.get_by_id("nonexistent") is None
|
|
|
|
def test_get_by_source(self, repository):
|
|
"""Test querying edges by source artifact."""
|
|
edge1 = DependencyEdge.create(
|
|
source_artifact_id="src-1",
|
|
target_artifact_id="tgt-1",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
edge2 = DependencyEdge.create(
|
|
source_artifact_id="src-1",
|
|
target_artifact_id="tgt-2",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.GENERATES,
|
|
)
|
|
edge3 = DependencyEdge.create(
|
|
source_artifact_id="src-2",
|
|
target_artifact_id="tgt-3",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge1)
|
|
repository.create(edge2)
|
|
repository.create(edge3)
|
|
|
|
edges = repository.get_by_source("src-1")
|
|
assert len(edges) == 2
|
|
assert all(e.source_artifact_id == "src-1" for e in edges)
|
|
|
|
def test_get_by_target(self, repository):
|
|
"""Test querying edges by target artifact."""
|
|
edge1 = DependencyEdge.create(
|
|
source_artifact_id="src-1",
|
|
target_artifact_id="tgt-1",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
edge2 = DependencyEdge.create(
|
|
source_artifact_id="src-2",
|
|
target_artifact_id="tgt-1",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge1)
|
|
repository.create(edge2)
|
|
|
|
edges = repository.get_by_target("tgt-1")
|
|
assert len(edges) == 2
|
|
assert all(e.target_artifact_id == "tgt-1" for e in edges)
|
|
|
|
def test_get_by_run(self, repository):
|
|
"""Test querying edges by run ID."""
|
|
edge1 = DependencyEdge.create(
|
|
source_artifact_id="a",
|
|
target_artifact_id="b",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
edge2 = DependencyEdge.create(
|
|
source_artifact_id="c",
|
|
target_artifact_id="d",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.GENERATES,
|
|
)
|
|
edge3 = DependencyEdge.create(
|
|
source_artifact_id="e",
|
|
target_artifact_id="f",
|
|
run_id="run-2",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge1)
|
|
repository.create(edge2)
|
|
repository.create(edge3)
|
|
|
|
edges = repository.get_by_run("run-1")
|
|
assert len(edges) == 2
|
|
assert all(e.run_id == "run-1" for e in edges)
|
|
|
|
def test_get_by_type(self, repository):
|
|
"""Test querying edges by type."""
|
|
edge1 = DependencyEdge.create(
|
|
source_artifact_id="a",
|
|
target_artifact_id="b",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
edge2 = DependencyEdge.create(
|
|
source_artifact_id="c",
|
|
target_artifact_id="d",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.GENERATES,
|
|
)
|
|
edge3 = DependencyEdge.create(
|
|
source_artifact_id="e",
|
|
target_artifact_id="f",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge1)
|
|
repository.create(edge2)
|
|
repository.create(edge3)
|
|
|
|
requires_edges = repository.get_by_type(EdgeType.REQUIRES)
|
|
assert len(requires_edges) == 2
|
|
assert all(e.edge_type == EdgeType.REQUIRES for e in requires_edges)
|
|
|
|
generates_edges = repository.get_by_type(EdgeType.GENERATES)
|
|
assert len(generates_edges) == 1
|
|
|
|
def test_get_all(self, repository):
|
|
"""Test getting all edges."""
|
|
for i in range(3):
|
|
edge = DependencyEdge.create(
|
|
source_artifact_id=f"src-{i}",
|
|
target_artifact_id=f"tgt-{i}",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge)
|
|
|
|
all_edges = repository.get_all()
|
|
assert len(all_edges) == 3
|
|
|
|
def test_get_all_empty(self, repository):
|
|
"""Test getting all edges from empty repository."""
|
|
assert repository.get_all() == []
|
|
|
|
def test_delete_edge(self, repository, sample_edge):
|
|
"""Test deleting an edge."""
|
|
repository.create(sample_edge)
|
|
assert repository.delete(sample_edge.id) is True
|
|
assert repository.get_by_id(sample_edge.id) is None
|
|
|
|
def test_delete_nonexistent_edge(self, repository):
|
|
"""Test deleting non-existent edge returns False."""
|
|
assert repository.delete("nonexistent") is False
|
|
|
|
def test_delete_by_run(self, repository):
|
|
"""Test deleting all edges for a run."""
|
|
for i in range(3):
|
|
edge = DependencyEdge.create(
|
|
source_artifact_id=f"src-{i}",
|
|
target_artifact_id=f"tgt-{i}",
|
|
run_id="run-1",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(edge)
|
|
|
|
other_edge = DependencyEdge.create(
|
|
source_artifact_id="other-src",
|
|
target_artifact_id="other-tgt",
|
|
run_id="run-2",
|
|
edge_type=EdgeType.REQUIRES,
|
|
)
|
|
repository.create(other_edge)
|
|
|
|
deleted = repository.delete_by_run("run-1")
|
|
assert deleted == 3
|
|
assert len(repository.get_by_run("run-1")) == 0
|
|
assert len(repository.get_by_run("run-2")) == 1
|
|
|
|
def test_delete_by_run_no_matches(self, repository):
|
|
"""Test deleting by run with no matches returns 0."""
|
|
assert repository.delete_by_run("nonexistent") == 0
|
|
|
|
def test_edge_type_preserved(self, repository):
|
|
"""Test that edge type is correctly stored and retrieved."""
|
|
for edge_type in EdgeType:
|
|
edge = DependencyEdge.create(
|
|
source_artifact_id=f"src-{edge_type.value}",
|
|
target_artifact_id=f"tgt-{edge_type.value}",
|
|
run_id="run-1",
|
|
edge_type=edge_type,
|
|
)
|
|
repository.create(edge)
|
|
retrieved = repository.get_by_id(edge.id)
|
|
assert retrieved.edge_type == edge_type
|