fix(prompts): fix three infrastructure bugs in prompt dependency resolution
- 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>
This commit is contained in:
@@ -259,3 +259,32 @@ class TestSQLiteArtifactRepository:
|
||||
assert retrieved.metadata.author == "test-author"
|
||||
assert retrieved.metadata.version == "1.0.0"
|
||||
assert retrieved.metadata.custom == {"key": "value"}
|
||||
|
||||
def test_content_round_trip(self, repository):
|
||||
"""Test that artifact content survives store and retrieve."""
|
||||
original_content = "# Test Content\n\nThis is the full content."
|
||||
artifact = Artifact.create(
|
||||
space_id="test-space",
|
||||
name="content-test",
|
||||
content=original_content,
|
||||
)
|
||||
|
||||
repository.create(artifact)
|
||||
retrieved = repository.get_by_id(artifact.id)
|
||||
|
||||
assert retrieved.content == original_content
|
||||
|
||||
def test_content_persisted_after_update(self, repository):
|
||||
"""Test that updated content is persisted."""
|
||||
artifact = Artifact.create(
|
||||
space_id="test-space",
|
||||
name="update-test",
|
||||
content="Original content",
|
||||
)
|
||||
repository.create(artifact)
|
||||
|
||||
artifact.update_content("Updated content")
|
||||
repository.update(artifact)
|
||||
|
||||
retrieved = repository.get_by_id(artifact.id)
|
||||
assert retrieved.content == "Updated content"
|
||||
|
||||
@@ -97,7 +97,7 @@ class TestContextCompiler:
|
||||
|
||||
# Macro should be replaced with resolved content
|
||||
assert "{{require:intro}}" not in compiled.content
|
||||
assert "[Content of intro from space-1]" in compiled.content
|
||||
assert "Introduction text" in compiled.content
|
||||
assert "intro" in compiled.dependency_digests
|
||||
|
||||
def test_compile_with_optional_macros_substitutes_empty(
|
||||
@@ -126,7 +126,7 @@ class TestContextCompiler:
|
||||
|
||||
# Optional missing macro should be removed
|
||||
assert "{{optional:missing}}" not in compiled.content
|
||||
assert compiled.content == "Start [Content of present from space-1] middle end"
|
||||
assert compiled.content == "Start Present content middle end"
|
||||
|
||||
def test_compile_failed_resolution_raises_error(
|
||||
self, compiler, analyzer, resolver
|
||||
@@ -257,3 +257,32 @@ class TestContextCompiler:
|
||||
assert info["dependency_count"] == 1
|
||||
assert "dep" in info["dependencies"]
|
||||
assert info["is_partial"] is False
|
||||
|
||||
def test_compile_with_programmatic_macros_no_corruption(
|
||||
self, compiler, analyzer, resolver, artifact_service
|
||||
):
|
||||
"""Test that compilation with programmatic macros doesn't corrupt content."""
|
||||
artifact_service.create_artifact(
|
||||
space_id="space-1",
|
||||
name="dep",
|
||||
content="Dependency content",
|
||||
)
|
||||
|
||||
# Use @{target} shorthand syntax (parsed into programmatic macros)
|
||||
content = "Before @{dep} after"
|
||||
template = PromptTemplate.create(
|
||||
space_id="space-1",
|
||||
name="test",
|
||||
content=content,
|
||||
)
|
||||
analyzer.analyze(template, content)
|
||||
|
||||
config = ResolutionConfig(space_id="space-1")
|
||||
result = resolver.resolve_template(template, config)
|
||||
|
||||
compiled = compiler.compile(template, content, result)
|
||||
|
||||
# Content should be cleanly substituted, not corrupted
|
||||
assert "@{dep}" not in compiled.content
|
||||
assert "Dependency content" in compiled.content
|
||||
assert compiled.content == "Before Dependency content after"
|
||||
|
||||
@@ -177,3 +177,66 @@ class TestMacroParser:
|
||||
content = "{{generate:test| key = value }}"
|
||||
macros = self.parser.parse(content)
|
||||
assert macros[0].parameters == {"key": "value"}
|
||||
|
||||
# --- Shorthand @{target} syntax tests ---
|
||||
|
||||
def test_parse_shorthand_macro(self):
|
||||
"""Test parsing @{target} shorthand syntax."""
|
||||
content = "Some text @{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].raw_text == "@{glossary}"
|
||||
assert macros[0].parameters == {}
|
||||
|
||||
def test_parse_shorthand_with_line_numbers(self):
|
||||
"""Test shorthand macros record line numbers."""
|
||||
content = "Line 1\n@{dep1}\nLine 3\n@{dep2}"
|
||||
macros = self.parser.parse(content)
|
||||
|
||||
assert len(macros) == 2
|
||||
assert macros[0].line_number == 2
|
||||
assert macros[1].line_number == 4
|
||||
|
||||
def test_parse_mixed_syntax(self):
|
||||
"""Test parsing both {{kind:target}} and @{target} in same content."""
|
||||
content = "{{require:dep1}} and @{dep2} and {{optional:dep3}}"
|
||||
macros = self.parser.parse(content)
|
||||
|
||||
assert len(macros) == 3
|
||||
assert macros[0].kind == MacroKind.REQUIRED
|
||||
assert macros[0].target == "dep1"
|
||||
assert macros[1].kind == MacroKind.OPTIONAL
|
||||
assert macros[1].target == "dep3"
|
||||
assert macros[2].kind == MacroKind.REQUIRED
|
||||
assert macros[2].target == "dep2"
|
||||
|
||||
def test_shorthand_has_macros(self):
|
||||
"""Test has_macros returns True for shorthand syntax."""
|
||||
assert self.parser.has_macros("@{target}") is True
|
||||
assert self.parser.has_macros("no macros here") is False
|
||||
|
||||
def test_shorthand_count_macros(self):
|
||||
"""Test count_macros includes shorthand macros."""
|
||||
content = "@{dep1} @{dep2} {{optional:dep3}}"
|
||||
counts = self.parser.count_macros(content)
|
||||
assert counts['required'] == 2
|
||||
assert counts['optional'] == 1
|
||||
|
||||
def test_shorthand_find_macro_positions(self):
|
||||
"""Test find_macro_positions includes shorthand macros."""
|
||||
content = "Start @{dep1} middle {{require:dep2}} end"
|
||||
positions = self.parser.find_macro_positions(content)
|
||||
|
||||
assert len(positions) == 2
|
||||
# Should be sorted by position
|
||||
assert positions[0][2] == "@{dep1}"
|
||||
assert positions[1][2] == "{{require:dep2}}"
|
||||
|
||||
def test_shorthand_target_trimmed(self):
|
||||
"""Test shorthand target with spaces is trimmed."""
|
||||
content = "@{ my-artifact }"
|
||||
macros = self.parser.parse(content)
|
||||
assert macros[0].target == "my-artifact"
|
||||
|
||||
@@ -32,6 +32,23 @@ class TestContentMacro:
|
||||
)
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user