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

180 lines
6.0 KiB
Python

"""Unit tests for macro parser."""
import pytest
from markitect.prompts.templates.parser import MacroParser, MacroParsingError
from markitect.prompts.templates.models import MacroKind
class TestMacroParser:
"""Tests for MacroParser."""
def setup_method(self):
"""Setup parser for each test."""
self.parser = MacroParser()
def test_parse_required_macro(self):
"""Test parsing required macro."""
content = "Some text {{require:glossary}} more text"
macros = self.parser.parse(content)
assert len(macros) == 1
assert macros[0].kind == MacroKind.REQUIRED
assert macros[0].target == "glossary"
assert macros[0].parameters == {}
def test_parse_optional_macro(self):
"""Test parsing optional macro."""
content = "Text {{optional:constraints}}"
macros = self.parser.parse(content)
assert len(macros) == 1
assert macros[0].kind == MacroKind.OPTIONAL
assert macros[0].target == "constraints"
def test_parse_generate_macro(self):
"""Test parsing generate macro."""
content = "{{generate:examples}}"
macros = self.parser.parse(content)
assert len(macros) == 1
assert macros[0].kind == MacroKind.GENERATE
assert macros[0].target == "examples"
def test_parse_macro_with_parameters(self):
"""Test parsing macro with parameters."""
content = "{{generate:code|language=python|framework=fastapi}}"
macros = self.parser.parse(content)
assert len(macros) == 1
assert macros[0].target == "code"
assert macros[0].parameters == {
"language": "python",
"framework": "fastapi",
}
def test_parse_multiple_macros(self):
"""Test parsing multiple macros."""
content = """
{{require:glossary}}
Some text here
{{optional:notes}}
{{generate:examples|lang=python}}
"""
macros = self.parser.parse(content)
assert len(macros) == 3
assert macros[0].kind == MacroKind.REQUIRED
assert macros[1].kind == MacroKind.OPTIONAL
assert macros[2].kind == MacroKind.GENERATE
def test_parse_with_line_numbers(self):
"""Test that line numbers are recorded."""
content = """Line 1
{{require:dep1}}
Line 3
{{optional:dep2}}
"""
macros = self.parser.parse(content)
assert macros[0].line_number == 2
assert macros[1].line_number == 4
def test_parse_case_insensitive(self):
"""Test macro kind is case insensitive."""
content = "{{REQUIRE:test}} {{Optional:test2}} {{Generate:test3}}"
macros = self.parser.parse(content)
assert len(macros) == 3
assert all(m.kind in MacroKind for m in macros)
def test_parse_alias_gen_for_generate(self):
"""Test 'gen' alias for 'generate'."""
content = "{{gen:test}}"
macros = self.parser.parse(content)
assert len(macros) == 1
assert macros[0].kind == MacroKind.GENERATE
def test_parse_alias_required_for_require(self):
"""Test 'required' alias for 'require'."""
content = "{{required:test}}"
macros = self.parser.parse(content)
assert len(macros) == 1
assert macros[0].kind == MacroKind.REQUIRED
def test_parse_preserves_raw_text(self):
"""Test raw text is preserved."""
content = "{{require:test|a=1}}"
macros = self.parser.parse(content)
assert macros[0].raw_text == "{{require:test|a=1}}"
def test_parse_empty_content(self):
"""Test parsing empty content."""
macros = self.parser.parse("")
assert macros == []
def test_parse_no_macros(self):
"""Test parsing content without macros."""
content = "Just regular text without any macros"
macros = self.parser.parse(content)
assert macros == []
def test_parse_invalid_macro_kind_raises_error(self):
"""Test invalid macro kind raises error."""
content = "{{invalid:target}}"
with pytest.raises(MacroParsingError, match="Invalid macro kind"):
self.parser.parse(content)
def test_parse_empty_target_raises_error(self):
"""Test empty target raises error."""
content = "{{require:}}"
with pytest.raises(MacroParsingError, match="target cannot be empty"):
self.parser.parse(content)
def test_find_macro_positions(self):
"""Test finding macro positions."""
content = "Start {{require:dep1}} middle {{optional:dep2}} end"
positions = self.parser.find_macro_positions(content)
assert len(positions) == 2
assert content[positions[0][0]:positions[0][1]] == "{{require:dep1}}"
assert content[positions[1][0]:positions[1][1]] == "{{optional:dep2}}"
def test_count_macros(self):
"""Test macro counting."""
content = """
{{require:dep1}}
{{require:dep2}}
{{optional:dep3}}
{{generate:gen1}}
"""
counts = self.parser.count_macros(content)
assert counts['required'] == 2
assert counts['optional'] == 1
assert counts['generate'] == 1
def test_has_macros_true(self):
"""Test has_macros returns True when macros present."""
content = "Text {{require:test}} text"
assert self.parser.has_macros(content) is True
def test_has_macros_false(self):
"""Test has_macros returns False when no macros."""
content = "Just plain text"
assert self.parser.has_macros(content) is False
def test_parse_macro_with_spaces_in_target(self):
"""Test target with spaces is trimmed."""
content = "{{require: my-artifact }}"
macros = self.parser.parse(content)
assert macros[0].target == "my-artifact"
def test_parse_parameter_with_spaces(self):
"""Test parameters with spaces are trimmed."""
content = "{{generate:test| key = value }}"
macros = self.parser.parse(content)
assert macros[0].parameters == {"key": "value"}