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>
This commit is contained in:
321
tests/integration/prompts/test_dependency_graph.py
Normal file
321
tests/integration/prompts/test_dependency_graph.py
Normal file
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
Integration tests for dependency graph full workflow.
|
||||
|
||||
Tests the complete workflow: artifacts + edges + graph + queries
|
||||
across multiple information spaces.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from markitect.prompts.models import Artifact, ArtifactType
|
||||
from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository
|
||||
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.dependencies.queries import DependencyQueryService
|
||||
from markitect.prompts.execution.manifest import 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 artifact_repo(temp_db):
|
||||
"""Create artifact repository."""
|
||||
return SQLiteArtifactRepository(temp_db)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dep_repo(temp_db):
|
||||
"""Create dependency repository (shares same DB)."""
|
||||
return SQLiteDependencyRepository(temp_db)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def builder(dep_repo):
|
||||
"""Create GraphBuilder."""
|
||||
return GraphBuilder(dep_repo)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def query_service(dep_repo):
|
||||
"""Create DependencyQueryService."""
|
||||
return DependencyQueryService(dep_repo)
|
||||
|
||||
|
||||
def _create_artifact(artifact_repo, space_id, name, content="test content"):
|
||||
"""Helper to create and persist an artifact."""
|
||||
artifact = Artifact.create(
|
||||
space_id=space_id,
|
||||
name=name,
|
||||
content=content,
|
||||
artifact_type=ArtifactType.CONTENT,
|
||||
)
|
||||
return artifact_repo.create(artifact)
|
||||
|
||||
|
||||
class TestFullWorkflow:
|
||||
"""Tests for the complete dependency tracking workflow."""
|
||||
|
||||
def test_manifest_to_graph_workflow(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test full workflow: create artifacts, manifest, persist edges, query."""
|
||||
# Step 1: Create artifacts in a space
|
||||
art_a = _create_artifact(artifact_repo, "space-1", "artifact-a", "content-a")
|
||||
art_b = _create_artifact(artifact_repo, "space-1", "artifact-b", "content-b")
|
||||
art_c = _create_artifact(artifact_repo, "space-1", "artifact-c", "content-c")
|
||||
|
||||
# Step 2: Create a manifest with dependency edges
|
||||
manifest = RunManifest.create(
|
||||
run_id="run-1",
|
||||
template_id=art_a.id,
|
||||
template_name="artifact-a",
|
||||
template_digest=art_a.content_digest,
|
||||
)
|
||||
manifest.add_dependency_edge(art_a.id, art_b.id, "requires")
|
||||
manifest.add_dependency_edge(art_b.id, art_c.id, "requires")
|
||||
|
||||
# Step 3: Persist edges from manifest
|
||||
persisted = builder.persist_edges(manifest)
|
||||
assert len(persisted) == 2
|
||||
|
||||
# Step 4: Build graph
|
||||
graph = builder.build_graph()
|
||||
assert graph.has_edge(art_a.id, art_b.id)
|
||||
assert graph.has_edge(art_b.id, art_c.id)
|
||||
|
||||
# Step 5: Query dependencies
|
||||
deps = query_service.find_dependencies(art_a.id)
|
||||
assert art_b.id in deps
|
||||
|
||||
transitive_deps = query_service.find_transitive_dependencies(art_a.id)
|
||||
assert art_b.id in transitive_deps
|
||||
assert art_c.id in transitive_deps
|
||||
|
||||
# Step 6: Query dependents
|
||||
dependents = query_service.find_dependents(art_c.id)
|
||||
assert art_b.id in dependents
|
||||
|
||||
transitive_dependents = query_service.find_transitive_dependents(art_c.id)
|
||||
assert art_a.id in transitive_dependents
|
||||
assert art_b.id in transitive_dependents
|
||||
|
||||
def test_build_order_workflow(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test build order across a dependency graph."""
|
||||
art_a = _create_artifact(artifact_repo, "space-1", "lib-core")
|
||||
art_b = _create_artifact(artifact_repo, "space-1", "lib-utils")
|
||||
art_c = _create_artifact(artifact_repo, "space-1", "app")
|
||||
|
||||
# app depends on lib-utils, lib-utils depends on lib-core
|
||||
manifest = RunManifest.create(
|
||||
run_id="run-build",
|
||||
template_id=art_c.id,
|
||||
template_name="app",
|
||||
template_digest=art_c.content_digest,
|
||||
)
|
||||
manifest.add_dependency_edge(art_c.id, art_b.id, "requires")
|
||||
manifest.add_dependency_edge(art_b.id, art_a.id, "requires")
|
||||
|
||||
builder.persist_edges(manifest)
|
||||
|
||||
# Build order should have core first, then utils, then app
|
||||
order = query_service.get_build_order()
|
||||
assert order.index(art_a.id) < order.index(art_b.id)
|
||||
assert order.index(art_b.id) < order.index(art_c.id)
|
||||
|
||||
def test_dependency_chain_workflow(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test finding dependency chains between artifacts."""
|
||||
art_a = _create_artifact(artifact_repo, "space-1", "source")
|
||||
art_b = _create_artifact(artifact_repo, "space-1", "intermediate")
|
||||
art_c = _create_artifact(artifact_repo, "space-1", "target")
|
||||
|
||||
manifest = RunManifest.create(
|
||||
run_id="run-chain",
|
||||
template_id=art_a.id,
|
||||
template_name="source",
|
||||
template_digest=art_a.content_digest,
|
||||
)
|
||||
manifest.add_dependency_edge(art_a.id, art_b.id, "requires")
|
||||
manifest.add_dependency_edge(art_b.id, art_c.id, "requires")
|
||||
|
||||
builder.persist_edges(manifest)
|
||||
|
||||
chain = query_service.get_dependency_chain(art_a.id, art_c.id)
|
||||
assert chain is not None
|
||||
assert chain[0] == art_a.id
|
||||
assert chain[-1] == art_c.id
|
||||
assert len(chain) == 3
|
||||
|
||||
|
||||
class TestCrossSpaceGraph:
|
||||
"""Tests for dependency tracking across information spaces (FR-6.2)."""
|
||||
|
||||
def test_cross_space_dependencies(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test dependencies between artifacts in different spaces."""
|
||||
# Create artifacts in different spaces
|
||||
shared = _create_artifact(artifact_repo, "shared", "common-lib")
|
||||
team_a = _create_artifact(artifact_repo, "team-a", "service-a")
|
||||
team_b = _create_artifact(artifact_repo, "team-b", "service-b")
|
||||
|
||||
# Both services depend on shared lib
|
||||
manifest = RunManifest.create(
|
||||
run_id="run-cross-space",
|
||||
template_id=team_a.id,
|
||||
template_name="service-a",
|
||||
template_digest=team_a.content_digest,
|
||||
)
|
||||
manifest.add_dependency_edge(team_a.id, shared.id, "requires")
|
||||
manifest.add_dependency_edge(team_b.id, shared.id, "requires")
|
||||
|
||||
builder.persist_edges(manifest)
|
||||
|
||||
# Shared lib should have both services as dependents
|
||||
dependents = query_service.find_dependents(shared.id)
|
||||
assert team_a.id in dependents
|
||||
assert team_b.id in dependents
|
||||
|
||||
# Each service should have shared as a dependency
|
||||
assert shared.id in query_service.find_dependencies(team_a.id)
|
||||
assert shared.id in query_service.find_dependencies(team_b.id)
|
||||
|
||||
def test_cross_space_transitive(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test transitive dependencies across spaces."""
|
||||
# shared/foundation -> shared/utils -> team-a/app
|
||||
foundation = _create_artifact(artifact_repo, "shared", "foundation")
|
||||
utils = _create_artifact(artifact_repo, "shared", "utils")
|
||||
app = _create_artifact(artifact_repo, "team-a", "app")
|
||||
|
||||
manifest = RunManifest.create(
|
||||
run_id="run-trans",
|
||||
template_id=app.id,
|
||||
template_name="app",
|
||||
template_digest=app.content_digest,
|
||||
)
|
||||
manifest.add_dependency_edge(app.id, utils.id, "requires")
|
||||
manifest.add_dependency_edge(utils.id, foundation.id, "requires")
|
||||
|
||||
builder.persist_edges(manifest)
|
||||
|
||||
# App should transitively depend on foundation
|
||||
transitive = query_service.find_transitive_dependencies(app.id)
|
||||
assert utils.id in transitive
|
||||
assert foundation.id in transitive
|
||||
|
||||
def test_cross_space_build_order(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test build order across spaces."""
|
||||
core = _create_artifact(artifact_repo, "shared", "core")
|
||||
api = _create_artifact(artifact_repo, "team-a", "api")
|
||||
web = _create_artifact(artifact_repo, "team-b", "web")
|
||||
|
||||
manifest = RunManifest.create(
|
||||
run_id="run-order",
|
||||
template_id=web.id,
|
||||
template_name="web",
|
||||
template_digest=web.content_digest,
|
||||
)
|
||||
manifest.add_dependency_edge(api.id, core.id, "requires")
|
||||
manifest.add_dependency_edge(web.id, api.id, "requires")
|
||||
|
||||
builder.persist_edges(manifest)
|
||||
|
||||
order = query_service.get_build_order()
|
||||
assert order.index(core.id) < order.index(api.id)
|
||||
assert order.index(api.id) < order.index(web.id)
|
||||
|
||||
|
||||
class TestMultipleRuns:
|
||||
"""Tests for dependency tracking across multiple runs."""
|
||||
|
||||
def test_edges_from_multiple_runs(
|
||||
self, artifact_repo, dep_repo, builder, query_service
|
||||
):
|
||||
"""Test edges from multiple runs are combined in graph."""
|
||||
art_a = _create_artifact(artifact_repo, "space-1", "a")
|
||||
art_b = _create_artifact(artifact_repo, "space-1", "b")
|
||||
art_c = _create_artifact(artifact_repo, "space-1", "c")
|
||||
|
||||
# Run 1: A -> B
|
||||
manifest1 = RunManifest.create(
|
||||
run_id="run-1",
|
||||
template_id=art_a.id,
|
||||
template_name="a",
|
||||
template_digest=art_a.content_digest,
|
||||
)
|
||||
manifest1.add_dependency_edge(art_a.id, art_b.id, "requires")
|
||||
builder.persist_edges(manifest1)
|
||||
|
||||
# Run 2: B -> C
|
||||
manifest2 = RunManifest.create(
|
||||
run_id="run-2",
|
||||
template_id=art_b.id,
|
||||
template_name="b",
|
||||
template_digest=art_b.content_digest,
|
||||
)
|
||||
manifest2.add_dependency_edge(art_b.id, art_c.id, "requires")
|
||||
builder.persist_edges(manifest2)
|
||||
|
||||
# Full graph should have A -> B -> C
|
||||
graph = builder.build_graph()
|
||||
assert graph.has_edge(art_a.id, art_b.id)
|
||||
assert graph.has_edge(art_b.id, art_c.id)
|
||||
|
||||
# Transitive query should work across runs
|
||||
transitive = query_service.find_transitive_dependencies(art_a.id)
|
||||
assert art_b.id in transitive
|
||||
assert art_c.id in transitive
|
||||
|
||||
def test_run_scoped_graph(self, artifact_repo, dep_repo, builder):
|
||||
"""Test building graph scoped to a specific run."""
|
||||
art_a = _create_artifact(artifact_repo, "space-1", "a")
|
||||
art_b = _create_artifact(artifact_repo, "space-1", "b")
|
||||
art_c = _create_artifact(artifact_repo, "space-1", "c")
|
||||
|
||||
# Persist edges for different runs
|
||||
edge1 = DependencyEdge.create(
|
||||
source_artifact_id=art_a.id,
|
||||
target_artifact_id=art_b.id,
|
||||
run_id="run-1",
|
||||
edge_type=EdgeType.REQUIRES,
|
||||
)
|
||||
dep_repo.create(edge1)
|
||||
|
||||
edge2 = DependencyEdge.create(
|
||||
source_artifact_id=art_b.id,
|
||||
target_artifact_id=art_c.id,
|
||||
run_id="run-2",
|
||||
edge_type=EdgeType.GENERATES,
|
||||
)
|
||||
dep_repo.create(edge2)
|
||||
|
||||
# Graph for run-1 only
|
||||
graph1 = builder.build_graph_for_run("run-1")
|
||||
assert graph1.has_edge(art_a.id, art_b.id)
|
||||
assert not graph1.has_edge(art_b.id, art_c.id)
|
||||
|
||||
# Graph for run-2 only
|
||||
graph2 = builder.build_graph_for_run("run-2")
|
||||
assert not graph2.has_edge(art_a.id, art_b.id)
|
||||
assert graph2.has_edge(art_b.id, art_c.id)
|
||||
Reference in New Issue
Block a user