- ContentMacro: add __post_init__ to auto-derive raw_text when built
programmatically, preventing str.replace("", X) corruption
- MacroParser: add @{target} shorthand syntax support mapped to REQUIRED kind,
updating parse, has_macros, count_macros, and find_macro_positions
- Artifact: store content in model and SQLite DB, replace resolver placeholder
with actual artifact content, add migration for existing databases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
254 lines
8.4 KiB
Python
254 lines
8.4 KiB
Python
"""Unit tests for template models."""
|
|
|
|
import pytest
|
|
from markitect.prompts.templates.models import (
|
|
ContentMacro,
|
|
MacroKind,
|
|
PromptTemplate,
|
|
TemplateMetadata,
|
|
)
|
|
from markitect.prompts.models import ArtifactType
|
|
|
|
|
|
class TestContentMacro:
|
|
"""Tests for ContentMacro."""
|
|
|
|
def test_create_required_macro(self):
|
|
"""Test creating required macro."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.REQUIRED,
|
|
target="glossary",
|
|
)
|
|
assert macro.kind == MacroKind.REQUIRED
|
|
assert macro.target == "glossary"
|
|
assert macro.parameters == {}
|
|
|
|
def test_create_macro_with_parameters(self):
|
|
"""Test creating macro with parameters."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.GENERATE,
|
|
target="code-examples",
|
|
parameters={"language": "python", "framework": "fastapi"},
|
|
)
|
|
assert macro.parameters == {"language": "python", "framework": "fastapi"}
|
|
|
|
def test_programmatic_macro_gets_auto_derived_raw_text(self):
|
|
"""Test that programmatically-built macro gets auto-derived raw_text."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.REQUIRED,
|
|
target="glossary",
|
|
)
|
|
assert macro.raw_text == "@{glossary}"
|
|
|
|
def test_explicit_raw_text_not_overridden(self):
|
|
"""Test that explicit raw_text is preserved."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.REQUIRED,
|
|
target="glossary",
|
|
raw_text="{{require:glossary}}",
|
|
)
|
|
assert macro.raw_text == "{{require:glossary}}"
|
|
|
|
def test_macro_str_representation(self):
|
|
"""Test string representation."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.REQUIRED,
|
|
target="test",
|
|
)
|
|
assert str(macro) == "{{required:test}}"
|
|
|
|
def test_macro_str_with_parameters(self):
|
|
"""Test string representation with parameters."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.GENERATE,
|
|
target="gen",
|
|
parameters={"key": "value"},
|
|
)
|
|
assert "generate:gen" in str(macro)
|
|
assert "key=value" in str(macro)
|
|
|
|
def test_macro_to_dict(self):
|
|
"""Test serialization to dict."""
|
|
macro = ContentMacro(
|
|
kind=MacroKind.OPTIONAL,
|
|
target="test",
|
|
parameters={"a": "1"},
|
|
raw_text="{{optional:test|a=1}}",
|
|
line_number=42,
|
|
)
|
|
data = macro.to_dict()
|
|
assert data["kind"] == "optional"
|
|
assert data["target"] == "test"
|
|
assert data["parameters"] == {"a": "1"}
|
|
assert data["line_number"] == 42
|
|
|
|
def test_macro_from_dict(self):
|
|
"""Test deserialization from dict."""
|
|
data = {
|
|
"kind": "required",
|
|
"target": "test",
|
|
"parameters": {"x": "y"},
|
|
"raw_text": "{{require:test|x=y}}",
|
|
"line_number": 10,
|
|
}
|
|
macro = ContentMacro.from_dict(data)
|
|
assert macro.kind == MacroKind.REQUIRED
|
|
assert macro.target == "test"
|
|
assert macro.parameters == {"x": "y"}
|
|
assert macro.line_number == 10
|
|
|
|
|
|
class TestTemplateMetadata:
|
|
"""Tests for TemplateMetadata."""
|
|
|
|
def test_create_empty_metadata(self):
|
|
"""Test creating empty metadata."""
|
|
meta = TemplateMetadata()
|
|
assert meta.purpose is None
|
|
assert meta.model_hints == {}
|
|
assert meta.expected_inputs == []
|
|
assert meta.output_type is None
|
|
|
|
def test_create_metadata_with_values(self):
|
|
"""Test creating metadata with values."""
|
|
meta = TemplateMetadata(
|
|
purpose="Generate API docs",
|
|
model_hints={"temperature": 0.7},
|
|
expected_inputs=["api-spec", "examples"],
|
|
output_type="markdown",
|
|
)
|
|
assert meta.purpose == "Generate API docs"
|
|
assert meta.model_hints == {"temperature": 0.7}
|
|
assert meta.expected_inputs == ["api-spec", "examples"]
|
|
assert meta.output_type == "markdown"
|
|
|
|
def test_metadata_to_dict(self):
|
|
"""Test serialization."""
|
|
meta = TemplateMetadata(purpose="Test")
|
|
data = meta.to_dict()
|
|
assert data["purpose"] == "Test"
|
|
assert "model_hints" in data
|
|
|
|
def test_metadata_from_dict(self):
|
|
"""Test deserialization."""
|
|
data = {
|
|
"purpose": "Test",
|
|
"model_hints": {"temp": 0.5},
|
|
"expected_inputs": ["in1"],
|
|
"output_type": "json",
|
|
}
|
|
meta = TemplateMetadata.from_dict(data)
|
|
assert meta.purpose == "Test"
|
|
assert meta.model_hints == {"temp": 0.5}
|
|
|
|
|
|
class TestPromptTemplate:
|
|
"""Tests for PromptTemplate."""
|
|
|
|
def test_create_template(self):
|
|
"""Test template creation."""
|
|
template = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test-template",
|
|
content="# Template\n{{require:glossary}}",
|
|
)
|
|
assert template.space_id == "space-1"
|
|
assert template.name == "test-template"
|
|
assert template.artifact.artifact_type == ArtifactType.TEMPLATE
|
|
assert not template.analyzed
|
|
assert template.macros == []
|
|
|
|
def test_template_properties(self):
|
|
"""Test template properties."""
|
|
template = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
assert template.id # Has UUID
|
|
assert template.space_id == "space-1"
|
|
assert template.name == "test"
|
|
assert template.content_digest # Has digest
|
|
|
|
def test_get_required_dependencies(self):
|
|
"""Test extracting required dependencies."""
|
|
template = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
template.macros = [
|
|
ContentMacro(kind=MacroKind.REQUIRED, target="dep1"),
|
|
ContentMacro(kind=MacroKind.OPTIONAL, target="dep2"),
|
|
ContentMacro(kind=MacroKind.REQUIRED, target="dep3"),
|
|
]
|
|
required = template.get_required_dependencies()
|
|
assert required == ["dep1", "dep3"]
|
|
|
|
def test_get_optional_dependencies(self):
|
|
"""Test extracting optional dependencies."""
|
|
template = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
template.macros = [
|
|
ContentMacro(kind=MacroKind.REQUIRED, target="dep1"),
|
|
ContentMacro(kind=MacroKind.OPTIONAL, target="dep2"),
|
|
ContentMacro(kind=MacroKind.OPTIONAL, target="dep3"),
|
|
]
|
|
optional = template.get_optional_dependencies()
|
|
assert optional == ["dep2", "dep3"]
|
|
|
|
def test_get_generators(self):
|
|
"""Test extracting generators."""
|
|
template = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
template.macros = [
|
|
ContentMacro(kind=MacroKind.GENERATE, target="gen1"),
|
|
ContentMacro(kind=MacroKind.REQUIRED, target="dep1"),
|
|
ContentMacro(kind=MacroKind.GENERATE, target="gen2"),
|
|
]
|
|
generators = template.get_generators()
|
|
assert generators == ["gen1", "gen2"]
|
|
|
|
def test_from_artifact_invalid_type(self):
|
|
"""Test from_artifact raises error for non-template."""
|
|
from markitect.prompts.models import Artifact
|
|
artifact = Artifact.create(
|
|
space_id="space-1",
|
|
name="not-template",
|
|
content="content",
|
|
artifact_type=ArtifactType.CONTENT,
|
|
)
|
|
with pytest.raises(ValueError, match="must be of type TEMPLATE"):
|
|
PromptTemplate.from_artifact(artifact)
|
|
|
|
def test_template_to_dict(self):
|
|
"""Test serialization."""
|
|
template = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
template.analyzed = True
|
|
data = template.to_dict()
|
|
assert "artifact" in data
|
|
assert "template_metadata" in data
|
|
assert data["analyzed"] is True
|
|
|
|
def test_template_from_dict(self):
|
|
"""Test deserialization."""
|
|
original = PromptTemplate.create(
|
|
space_id="space-1",
|
|
name="test",
|
|
content="content",
|
|
)
|
|
data = original.to_dict()
|
|
restored = PromptTemplate.from_dict(data)
|
|
assert restored.id == original.id
|
|
assert restored.name == original.name
|