Implement addressable artifacts with content-based identity and change detection. Core Features: - Artifact model with SHA-256 content digests - ArtifactReference for cross-space addressing - IArtifactRepository interface for pluggable storage - SQLiteArtifactRepository implementation - ArtifactService for high-level operations - Content digest calculation utilities Database: - prompt_artifacts table with indexes - Support for artifact metadata and types - UNIQUE constraint on space_id+name Tests (41 passing): - 26 model tests (metadata, artifacts, references, digests) - 15 repository tests (CRUD, queries, constraints) Implements: - FR-1.1: Unique addressability by name and ID - FR-1.2: Content digest computation and storage - FR-1.3: Cross-space artifact references Files Created: - markitect/prompts/models.py - markitect/prompts/repositories/interfaces.py - markitect/prompts/repositories/sqlite.py - markitect/prompts/services/artifact_service.py - migrations/prompts/001_create_artifacts_table.sql - tests/unit/prompts/test_artifact_models.py - tests/unit/prompts/test_artifact_repository.py Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
262 lines
8.6 KiB
Python
262 lines
8.6 KiB
Python
"""
|
|
Unit tests for artifact repository.
|
|
|
|
Tests SQLiteArtifactRepository implementation.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from markitect.prompts.models import Artifact, ArtifactType, ArtifactMetadata
|
|
from markitect.prompts.repositories.sqlite import SQLiteArtifactRepository
|
|
from markitect.prompts.repositories.interfaces import (
|
|
ArtifactNotFoundError,
|
|
DuplicateArtifactError,
|
|
)
|
|
|
|
|
|
@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
|
|
# Cleanup
|
|
Path(db_path).unlink(missing_ok=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def repository(temp_db):
|
|
"""Create repository instance with temp database."""
|
|
return SQLiteArtifactRepository(temp_db)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_artifact():
|
|
"""Create sample artifact for testing."""
|
|
return Artifact.create(
|
|
space_id="test-space",
|
|
name="test-artifact",
|
|
content="# Test Content\n\nThis is test content.",
|
|
artifact_type=ArtifactType.CONTENT,
|
|
)
|
|
|
|
|
|
class TestSQLiteArtifactRepository:
|
|
"""Tests for SQLiteArtifactRepository."""
|
|
|
|
def test_create_artifact(self, repository, sample_artifact):
|
|
"""Test creating an artifact."""
|
|
created = repository.create(sample_artifact)
|
|
|
|
assert created.id == sample_artifact.id
|
|
assert created.space_id == sample_artifact.space_id
|
|
assert created.name == sample_artifact.name
|
|
assert created.content_digest == sample_artifact.content_digest
|
|
|
|
def test_create_duplicate_artifact_raises_error(self, repository, sample_artifact):
|
|
"""Test creating duplicate artifact raises error."""
|
|
repository.create(sample_artifact)
|
|
|
|
# Try to create another artifact with same space_id and name
|
|
duplicate = Artifact.create(
|
|
space_id="test-space",
|
|
name="test-artifact",
|
|
content="Different content",
|
|
)
|
|
|
|
with pytest.raises(DuplicateArtifactError, match="already exists"):
|
|
repository.create(duplicate)
|
|
|
|
def test_get_by_id(self, repository, sample_artifact):
|
|
"""Test retrieving artifact by ID."""
|
|
repository.create(sample_artifact)
|
|
|
|
retrieved = repository.get_by_id(sample_artifact.id)
|
|
|
|
assert retrieved is not None
|
|
assert retrieved.id == sample_artifact.id
|
|
assert retrieved.name == sample_artifact.name
|
|
assert retrieved.content_digest == sample_artifact.content_digest
|
|
|
|
def test_get_by_id_not_found(self, repository):
|
|
"""Test get_by_id returns None for non-existent ID."""
|
|
result = repository.get_by_id("non-existent-id")
|
|
assert result is None
|
|
|
|
def test_get_by_name(self, repository, sample_artifact):
|
|
"""Test retrieving artifact by space and name."""
|
|
repository.create(sample_artifact)
|
|
|
|
retrieved = repository.get_by_name("test-space", "test-artifact")
|
|
|
|
assert retrieved is not None
|
|
assert retrieved.id == sample_artifact.id
|
|
assert retrieved.space_id == "test-space"
|
|
assert retrieved.name == "test-artifact"
|
|
|
|
def test_get_by_name_not_found(self, repository):
|
|
"""Test get_by_name returns None when not found."""
|
|
result = repository.get_by_name("test-space", "non-existent")
|
|
assert result is None
|
|
|
|
def test_get_by_digest(self, repository):
|
|
"""Test finding artifacts by content digest."""
|
|
content = "Shared content"
|
|
artifact1 = Artifact.create(
|
|
space_id="space-1",
|
|
name="artifact-1",
|
|
content=content,
|
|
)
|
|
artifact2 = Artifact.create(
|
|
space_id="space-2",
|
|
name="artifact-2",
|
|
content=content, # Same content, same digest
|
|
)
|
|
|
|
repository.create(artifact1)
|
|
repository.create(artifact2)
|
|
|
|
results = repository.get_by_digest(artifact1.content_digest)
|
|
|
|
assert len(results) == 2
|
|
names = {a.name for a in results}
|
|
assert names == {"artifact-1", "artifact-2"}
|
|
|
|
def test_list_by_space(self, repository):
|
|
"""Test listing artifacts in a space."""
|
|
artifacts = [
|
|
Artifact.create(
|
|
space_id="space-1",
|
|
name=f"artifact-{i}",
|
|
content=f"Content {i}",
|
|
)
|
|
for i in range(3)
|
|
]
|
|
|
|
for artifact in artifacts:
|
|
repository.create(artifact)
|
|
|
|
# Add artifact in different space
|
|
other = Artifact.create(
|
|
space_id="space-2",
|
|
name="other",
|
|
content="Other content",
|
|
)
|
|
repository.create(other)
|
|
|
|
results = repository.list_by_space("space-1")
|
|
|
|
assert len(results) == 3
|
|
assert all(a.space_id == "space-1" for a in results)
|
|
# Should be ordered by name
|
|
assert [a.name for a in results] == ["artifact-0", "artifact-1", "artifact-2"]
|
|
|
|
def test_list_by_space_with_type_filter(self, repository):
|
|
"""Test listing artifacts filtered by type."""
|
|
artifacts = [
|
|
Artifact.create(
|
|
space_id="space-1",
|
|
name="content-1",
|
|
content="Content",
|
|
artifact_type=ArtifactType.CONTENT,
|
|
),
|
|
Artifact.create(
|
|
space_id="space-1",
|
|
name="template-1",
|
|
content="Template",
|
|
artifact_type=ArtifactType.TEMPLATE,
|
|
),
|
|
Artifact.create(
|
|
space_id="space-1",
|
|
name="content-2",
|
|
content="Content 2",
|
|
artifact_type=ArtifactType.CONTENT,
|
|
),
|
|
]
|
|
|
|
for artifact in artifacts:
|
|
repository.create(artifact)
|
|
|
|
results = repository.list_by_space("space-1", ArtifactType.CONTENT)
|
|
|
|
assert len(results) == 2
|
|
assert all(a.artifact_type == ArtifactType.CONTENT for a in results)
|
|
|
|
def test_update_artifact(self, repository, sample_artifact):
|
|
"""Test updating artifact."""
|
|
repository.create(sample_artifact)
|
|
|
|
# Update content
|
|
sample_artifact.update_content("New content")
|
|
updated = repository.update(sample_artifact)
|
|
|
|
# Retrieve and verify
|
|
retrieved = repository.get_by_id(sample_artifact.id)
|
|
assert retrieved.content_digest == updated.content_digest
|
|
assert retrieved.updated_at >= updated.updated_at
|
|
|
|
def test_update_nonexistent_artifact_raises_error(self, repository):
|
|
"""Test updating non-existent artifact raises error."""
|
|
artifact = Artifact.create(
|
|
space_id="test-space",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
|
|
with pytest.raises(ArtifactNotFoundError, match="not found"):
|
|
repository.update(artifact)
|
|
|
|
def test_delete_artifact(self, repository, sample_artifact):
|
|
"""Test deleting artifact."""
|
|
repository.create(sample_artifact)
|
|
|
|
result = repository.delete(sample_artifact.id)
|
|
assert result is True
|
|
|
|
# Verify deleted
|
|
retrieved = repository.get_by_id(sample_artifact.id)
|
|
assert retrieved is None
|
|
|
|
def test_delete_nonexistent_artifact(self, repository):
|
|
"""Test deleting non-existent artifact returns False."""
|
|
result = repository.delete("non-existent-id")
|
|
assert result is False
|
|
|
|
def test_exists(self, repository, sample_artifact):
|
|
"""Test checking if artifact exists."""
|
|
assert not repository.exists("test-space", "test-artifact")
|
|
|
|
repository.create(sample_artifact)
|
|
|
|
assert repository.exists("test-space", "test-artifact")
|
|
assert not repository.exists("test-space", "other-artifact")
|
|
assert not repository.exists("other-space", "test-artifact")
|
|
|
|
def test_artifact_metadata_persistence(self, repository):
|
|
"""Test metadata is persisted correctly."""
|
|
metadata = ArtifactMetadata(
|
|
description="Test description",
|
|
tags=["tag1", "tag2"],
|
|
author="test-author",
|
|
version="1.0.0",
|
|
custom={"key": "value"},
|
|
)
|
|
|
|
artifact = Artifact.create(
|
|
space_id="test-space",
|
|
name="test",
|
|
content="content",
|
|
metadata=metadata,
|
|
)
|
|
|
|
repository.create(artifact)
|
|
retrieved = repository.get_by_id(artifact.id)
|
|
|
|
assert retrieved.metadata.description == "Test description"
|
|
assert retrieved.metadata.tags == ["tag1", "tag2"]
|
|
assert retrieved.metadata.author == "test-author"
|
|
assert retrieved.metadata.version == "1.0.0"
|
|
assert retrieved.metadata.custom == {"key": "value"}
|