Files
markitect-main/tests/unit/prompts/test_artifact_models.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

314 lines
11 KiB
Python

"""
Unit tests for artifact models.
Tests Artifact, ArtifactMetadata, ArtifactReference, and content digest
calculation functions.
"""
import pytest
from datetime import datetime
from markitect.prompts.models import (
Artifact,
ArtifactMetadata,
ArtifactReference,
ArtifactType,
calculate_content_digest,
calculate_bundle_digest,
)
class TestArtifactMetadata:
"""Tests for ArtifactMetadata."""
def test_create_empty_metadata(self):
"""Test creating metadata with defaults."""
metadata = ArtifactMetadata()
assert metadata.description is None
assert metadata.tags == []
assert metadata.author is None
assert metadata.version is None
assert metadata.custom == {}
def test_create_metadata_with_values(self):
"""Test creating metadata with values."""
metadata = ArtifactMetadata(
description="Test artifact",
tags=["test", "example"],
author="test-author",
version="1.0.0",
custom={"key": "value"}
)
assert metadata.description == "Test artifact"
assert metadata.tags == ["test", "example"]
assert metadata.author == "test-author"
assert metadata.version == "1.0.0"
assert metadata.custom == {"key": "value"}
def test_metadata_to_dict(self):
"""Test metadata serialization to dict."""
metadata = ArtifactMetadata(
description="Test",
tags=["tag1"],
author="author",
)
data = metadata.to_dict()
assert data["description"] == "Test"
assert data["tags"] == ["tag1"]
assert data["author"] == "author"
assert data["version"] is None
assert data["custom"] == {}
def test_metadata_from_dict(self):
"""Test metadata deserialization from dict."""
data = {
"description": "Test",
"tags": ["tag1", "tag2"],
"author": "author",
"version": "2.0",
"custom": {"extra": "data"}
}
metadata = ArtifactMetadata.from_dict(data)
assert metadata.description == "Test"
assert metadata.tags == ["tag1", "tag2"]
assert metadata.author == "author"
assert metadata.version == "2.0"
assert metadata.custom == {"extra": "data"}
class TestArtifact:
"""Tests for Artifact model."""
def test_create_artifact(self):
"""Test artifact creation with automatic digest calculation."""
content = "# Test Content\n\nThis is a test."
artifact = Artifact.create(
space_id="space-1",
name="test-artifact",
content=content,
)
assert artifact.id # UUID generated
assert artifact.space_id == "space-1"
assert artifact.name == "test-artifact"
assert artifact.artifact_type == ArtifactType.CONTENT
assert artifact.content_digest == calculate_content_digest(content)
assert artifact.content_size == len(content.encode('utf-8'))
assert isinstance(artifact.metadata, ArtifactMetadata)
assert isinstance(artifact.created_at, datetime)
assert isinstance(artifact.updated_at, datetime)
def test_create_artifact_with_type(self):
"""Test creating artifact with specific type."""
artifact = Artifact.create(
space_id="space-1",
name="my-template",
content="Template content",
artifact_type=ArtifactType.TEMPLATE,
)
assert artifact.artifact_type == ArtifactType.TEMPLATE
def test_create_artifact_with_metadata(self):
"""Test creating artifact with metadata."""
metadata = ArtifactMetadata(description="Test", tags=["tag1"])
artifact = Artifact.create(
space_id="space-1",
name="test",
content="content",
metadata=metadata,
)
assert artifact.metadata.description == "Test"
assert artifact.metadata.tags == ["tag1"]
def test_update_content(self):
"""Test updating artifact content."""
artifact = Artifact.create(
space_id="space-1",
name="test",
content="original content",
)
original_digest = artifact.content_digest
original_updated_at = artifact.updated_at
# Small delay to ensure timestamp difference
import time
time.sleep(0.01)
new_content = "updated content"
artifact.update_content(new_content)
assert artifact.content_digest != original_digest
assert artifact.content_digest == calculate_content_digest(new_content)
assert artifact.content_size == len(new_content.encode('utf-8'))
assert artifact.updated_at > original_updated_at
def test_has_changed(self):
"""Test change detection."""
content = "original"
artifact = Artifact.create(
space_id="space-1",
name="test",
content=content,
)
# Same content - no change
assert not artifact.has_changed(calculate_content_digest(content))
# Different content - changed
assert artifact.has_changed(calculate_content_digest("different"))
def test_artifact_to_dict(self):
"""Test artifact serialization."""
artifact = Artifact.create(
space_id="space-1",
name="test",
content="content",
artifact_type=ArtifactType.TEMPLATE,
)
data = artifact.to_dict()
assert data["id"] == artifact.id
assert data["space_id"] == "space-1"
assert data["name"] == "test"
assert data["artifact_type"] == "template"
assert data["content_digest"] == artifact.content_digest
assert data["content_size"] == artifact.content_size
assert "metadata" in data
assert "created_at" in data
assert "updated_at" in data
def test_artifact_from_dict(self):
"""Test artifact deserialization."""
original = Artifact.create(
space_id="space-1",
name="test",
content="content",
)
data = original.to_dict()
restored = Artifact.from_dict(data)
assert restored.id == original.id
assert restored.space_id == original.space_id
assert restored.name == original.name
assert restored.artifact_type == original.artifact_type
assert restored.content_digest == original.content_digest
assert restored.content_size == original.content_size
class TestArtifactReference:
"""Tests for ArtifactReference."""
def test_simple_reference(self):
"""Test simple artifact name reference."""
ref = ArtifactReference(name="my-artifact")
assert ref.name == "my-artifact"
assert ref.space_id is None
assert ref.version is None
assert str(ref) == "my-artifact"
def test_reference_with_space(self):
"""Test reference with space ID."""
ref = ArtifactReference(name="artifact", space_id="space-1")
assert ref.name == "artifact"
assert ref.space_id == "space-1"
assert str(ref) == "space-1:artifact"
def test_reference_with_version(self):
"""Test reference with version."""
ref = ArtifactReference(name="artifact", version="1.0.0")
assert ref.name == "artifact"
assert ref.version == "1.0.0"
assert str(ref) == "artifact@1.0.0"
def test_reference_with_space_and_version(self):
"""Test fully qualified reference."""
ref = ArtifactReference(
name="artifact",
space_id="space-1",
version="2.0.0"
)
assert str(ref) == "space-1:artifact@2.0.0"
def test_parse_simple_reference(self):
"""Test parsing simple reference."""
ref = ArtifactReference.parse("my-artifact")
assert ref.name == "my-artifact"
assert ref.space_id is None
assert ref.version is None
def test_parse_space_reference(self):
"""Test parsing space:artifact reference."""
ref = ArtifactReference.parse("space-1:my-artifact")
assert ref.name == "my-artifact"
assert ref.space_id == "space-1"
assert ref.version is None
def test_parse_version_reference(self):
"""Test parsing artifact@version reference."""
ref = ArtifactReference.parse("my-artifact@1.0.0")
assert ref.name == "my-artifact"
assert ref.space_id is None
assert ref.version == "1.0.0"
def test_parse_full_reference(self):
"""Test parsing space:artifact@version reference."""
ref = ArtifactReference.parse("space-1:my-artifact@2.0.0")
assert ref.name == "my-artifact"
assert ref.space_id == "space-1"
assert ref.version == "2.0.0"
def test_parse_invalid_reference(self):
"""Test parsing invalid reference raises error."""
with pytest.raises(ValueError, match="Invalid artifact reference"):
ArtifactReference.parse("space-1:")
with pytest.raises(ValueError, match="Invalid artifact reference"):
ArtifactReference.parse("@1.0.0")
class TestContentDigest:
"""Tests for content digest functions."""
def test_calculate_content_digest(self):
"""Test SHA-256 digest calculation."""
content = "Hello, World!"
digest = calculate_content_digest(content)
# SHA-256 produces 64 hex characters
assert len(digest) == 64
assert digest.isalnum()
def test_digest_consistency(self):
"""Test digest is consistent for same content."""
content = "Test content"
digest1 = calculate_content_digest(content)
digest2 = calculate_content_digest(content)
assert digest1 == digest2
def test_digest_changes_with_content(self):
"""Test digest changes when content changes."""
digest1 = calculate_content_digest("content 1")
digest2 = calculate_content_digest("content 2")
assert digest1 != digest2
def test_calculate_bundle_digest(self):
"""Test bundle digest calculation."""
components = {
"template": "abc123",
"dep1": "def456",
"dep2": "ghi789",
}
digest = calculate_bundle_digest(components)
assert len(digest) == 64
def test_bundle_digest_order_independent(self):
"""Test bundle digest is deterministic regardless of dict order."""
components1 = {"a": "1", "b": "2", "c": "3"}
components2 = {"c": "3", "a": "1", "b": "2"}
assert calculate_bundle_digest(components1) == calculate_bundle_digest(components2)
def test_bundle_digest_sensitive_to_values(self):
"""Test bundle digest changes when values change."""
digest1 = calculate_bundle_digest({"a": "1", "b": "2"})
digest2 = calculate_bundle_digest({"a": "1", "b": "3"})
assert digest1 != digest2