Files
markitect-main/tests/unit/prompts/test_template_models.py
tegwick e6840fe696 feat(prompts): implement Phase 2 - Templates & Macros (FR-2)
Implement PromptTemplate models with ContentMacro parsing and analysis.

Core Features:
- PromptTemplate extending Artifact for template-specific operations
- ContentMacro model supporting REQUIRED, OPTIONAL, GENERATE kinds
- MacroParser for extracting macros from template content
- TemplateAnalyzer for dependency extraction and validation
- TemplateService for high-level template operations
- Template metadata for model hints and expected inputs

Macro Syntax:
- {{require:artifact-name}} - Required dependency
- {{optional:artifact-name}} - Optional dependency
- {{generate:template-name|param=value}} - Nested generation

Tests (38 passing):
- 18 template model tests (macros, templates, metadata)
- 20 parser tests (parsing, validation, parameters, aliases)

Implements:
- FR-2.1: PromptTemplate as content artifact with macros
- FR-2.2: ContentMacro detection and extraction
- FR-2.3: Required/Optional/Generate macro kinds

Files Created:
- markitect/prompts/templates/models.py
- markitect/prompts/templates/parser.py
- markitect/prompts/templates/analyzer.py
- markitect/prompts/services/template_service.py
- tests/unit/prompts/test_template_models.py
- tests/unit/prompts/test_macro_parser.py

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:34:22 +01:00

237 lines
7.8 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_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