Complete implementation of Phase 8, the final phase of prompt dependency resolution infrastructure, adding full observability and traceability. ## Features (FR-11) ### FR-11.1: Complete Artifact Provenance Tracing - TraceabilityService: composition layer for full artifact lineage - Trace any artifact to producing PromptTemplate, input artifacts, generator runs, and quality validation results - ProvenanceTrace model with complete dependency chain reconstruction - RunSummary and ArtifactLineage models for structured trace output ### FR-11.2: Recomputation Query Infrastructure - PromptQueryService: cross-service complex queries - Run history queries with template and status filters - Stale artifact detection via impact debt analysis - Dependency graph statistics (nodes, edges, cycles, roots, leaves) - Content-based artifact lookups by digest ### Visualization Support - GraphExporter: DOT (Graphviz) and Mermaid format export - Supports all edge types (requires, generates, includes) - Handles isolated nodes, linear chains, diamonds, and complex graphs ### CLI Commands (prompt group) - `prompt trace <artifact_id>` - Full provenance trace as JSON - `prompt graph <artifact_id>` - Dependency graph (DOT/Mermaid) - `prompt runs` - List execution runs with filters - `prompt debt` - Show impact debt and stale artifacts - `prompt stats` - Dependency graph statistics ## Implementation Source files (8): - markitect/prompts/traceability/models.py - Trace data models - markitect/prompts/traceability/service.py - TraceabilityService - markitect/prompts/visualization/graph.py - Graph export - markitect/prompts/queries/operations.py - PromptQueryService - markitect/prompts/cli.py - Click CLI commands - Package __init__.py files (3) Tests (64 total, all passing): - tests/unit/prompts/test_traceability_service.py (21 tests) - tests/unit/prompts/test_visualization.py (14 tests) - tests/unit/prompts/test_query_operations.py (12 tests) - tests/integration/prompts/test_traceability_workflow.py (7 tests) - tests/integration/prompts/test_prompt_cli.py (10 tests) ## Architecture TraceabilityService is a composition layer that delegates to: - DependencyQueryService (transitive dependency lookups) - QualityValidator (validation history) - IncrementalExecutionEngine (impact debt queries) - Direct repository access (artifacts, edges) No duplicate data storage - all data comes from existing Phase 1-7 infrastructure (artifact repo, dependency repo, validation DB, debt DB). ## Verification All 2250 tests pass with 0 regressions. Phase 8 completes the full 8-phase implementation roadmap. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
"""
|
|
Unit tests for GraphExporter.
|
|
|
|
Tests DOT and Mermaid export with various graph shapes.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from markitect.prompts.dependencies.models import DependencyGraph, EdgeType
|
|
from markitect.prompts.visualization.graph import GraphExporter
|
|
|
|
|
|
@pytest.fixture
|
|
def empty_graph():
|
|
"""Empty graph with no nodes."""
|
|
return DependencyGraph()
|
|
|
|
|
|
@pytest.fixture
|
|
def single_node_graph():
|
|
"""Graph with a single isolated node."""
|
|
g = DependencyGraph()
|
|
g._forward["node-a"] = set()
|
|
g._reverse["node-a"] = set()
|
|
return g
|
|
|
|
|
|
@pytest.fixture
|
|
def linear_graph():
|
|
"""Linear chain: A -> B -> C."""
|
|
g = DependencyGraph()
|
|
g.add_edge("A", "B", EdgeType.REQUIRES)
|
|
g.add_edge("B", "C", EdgeType.GENERATES)
|
|
return g
|
|
|
|
|
|
@pytest.fixture
|
|
def diamond_graph():
|
|
"""Diamond: A -> B, A -> C, B -> D, C -> D."""
|
|
g = DependencyGraph()
|
|
g.add_edge("A", "B", EdgeType.REQUIRES)
|
|
g.add_edge("A", "C", EdgeType.REQUIRES)
|
|
g.add_edge("B", "D", EdgeType.GENERATES)
|
|
g.add_edge("C", "D", EdgeType.INCLUDES)
|
|
return g
|
|
|
|
|
|
class TestToDot:
|
|
"""Tests for to_dot export."""
|
|
|
|
def test_empty_graph(self, empty_graph):
|
|
"""Test DOT output for empty graph."""
|
|
dot = GraphExporter.to_dot(empty_graph)
|
|
assert 'digraph "Dependencies"' in dot
|
|
assert "rankdir=LR" in dot
|
|
|
|
def test_single_node(self, single_node_graph):
|
|
"""Test DOT output with single node."""
|
|
dot = GraphExporter.to_dot(single_node_graph)
|
|
assert "node_a" in dot
|
|
assert 'label="node-a"' in dot
|
|
|
|
def test_linear_graph(self, linear_graph):
|
|
"""Test DOT output for linear chain."""
|
|
dot = GraphExporter.to_dot(linear_graph)
|
|
assert "A -> B" in dot
|
|
assert "B -> C" in dot
|
|
assert 'label="requires"' in dot
|
|
assert 'label="generates"' in dot
|
|
|
|
def test_diamond_graph(self, diamond_graph):
|
|
"""Test DOT output for diamond graph."""
|
|
dot = GraphExporter.to_dot(diamond_graph)
|
|
assert "A -> B" in dot
|
|
assert "A -> C" in dot
|
|
assert "B -> D" in dot
|
|
assert "C -> D" in dot
|
|
|
|
def test_custom_title(self, linear_graph):
|
|
"""Test DOT output with custom title."""
|
|
dot = GraphExporter.to_dot(linear_graph, title="My Graph")
|
|
assert 'digraph "My Graph"' in dot
|
|
assert 'label="My Graph"' in dot
|
|
|
|
def test_edge_styles(self, linear_graph):
|
|
"""Test DOT edge styles for different edge types."""
|
|
dot = GraphExporter.to_dot(linear_graph)
|
|
assert 'style="solid"' in dot # REQUIRES
|
|
assert 'style="dashed"' in dot # GENERATES
|
|
|
|
def test_dot_is_valid_structure(self, diamond_graph):
|
|
"""Test DOT output has valid opening/closing braces."""
|
|
dot = GraphExporter.to_dot(diamond_graph)
|
|
assert dot.startswith('digraph')
|
|
assert dot.endswith("}")
|
|
|
|
|
|
class TestToMermaid:
|
|
"""Tests for to_mermaid export."""
|
|
|
|
def test_empty_graph(self, empty_graph):
|
|
"""Test Mermaid output for empty graph."""
|
|
mermaid = GraphExporter.to_mermaid(empty_graph)
|
|
assert "graph LR" in mermaid
|
|
|
|
def test_single_node(self, single_node_graph):
|
|
"""Test Mermaid output with single node."""
|
|
mermaid = GraphExporter.to_mermaid(single_node_graph)
|
|
assert "node-a" in mermaid
|
|
|
|
def test_linear_graph(self, linear_graph):
|
|
"""Test Mermaid output for linear chain."""
|
|
mermaid = GraphExporter.to_mermaid(linear_graph)
|
|
assert "A-->|requires|B" in mermaid
|
|
assert "B-.->|generates|C" in mermaid
|
|
|
|
def test_diamond_graph(self, diamond_graph):
|
|
"""Test Mermaid output for diamond graph."""
|
|
mermaid = GraphExporter.to_mermaid(diamond_graph)
|
|
assert "A-->|requires|B" in mermaid
|
|
assert "A-->|requires|C" in mermaid
|
|
assert "B-.->|generates|D" in mermaid
|
|
assert "C==>|includes|D" in mermaid
|
|
|
|
def test_custom_title(self, linear_graph):
|
|
"""Test Mermaid output with custom title."""
|
|
mermaid = GraphExporter.to_mermaid(linear_graph, title="Build Graph")
|
|
assert "Build Graph" in mermaid
|
|
|
|
def test_edge_arrows(self, diamond_graph):
|
|
"""Test Mermaid edge arrows for different types."""
|
|
mermaid = GraphExporter.to_mermaid(diamond_graph)
|
|
assert "-->" in mermaid # REQUIRES
|
|
assert "-.->" in mermaid # GENERATES
|
|
assert "==>" in mermaid # INCLUDES
|
|
|
|
def test_mermaid_starts_with_graph(self, linear_graph):
|
|
"""Test Mermaid output starts with graph directive."""
|
|
mermaid = GraphExporter.to_mermaid(linear_graph)
|
|
lines = mermaid.strip().split("\n")
|
|
assert "graph LR" in lines[1]
|