Files
markitect-main/tests/unit/prompts/test_artifact_repository.py
tegwick 945544880d feat(prompts): implement Phase 1 - Foundation (FR-1)
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>
2026-02-08 22:30:26 +01:00

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"}