feat: add prompt placeholder substitution with {filename} syntax
This commit is contained in:
parent
760eac5a7b
commit
3de3614433
6 changed files with 274 additions and 33 deletions
109
tests/test_prompt.py
Normal file
109
tests/test_prompt.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Tests for hokusai.prompt — prompt resolution and placeholder substitution."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from hokusai.prompt import (
|
||||
extract_placeholder_files,
|
||||
resolve_prompt,
|
||||
substitute_placeholders,
|
||||
)
|
||||
|
||||
|
||||
class TestSubstitutePlaceholders:
|
||||
"""Test placeholder substitution in prompt text."""
|
||||
|
||||
def test_no_placeholders(self, project_dir: Path) -> None:
|
||||
assert substitute_placeholders("Hello world", project_dir) == "Hello world"
|
||||
|
||||
def test_single_placeholder(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "name.txt").write_text("World")
|
||||
result = substitute_placeholders("Hello {name.txt}", project_dir)
|
||||
assert result == "Hello World"
|
||||
|
||||
def test_multiple_placeholders(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "first.txt").write_text("Alice")
|
||||
_ = (project_dir / "last.txt").write_text("Smith")
|
||||
result = substitute_placeholders("Dear {first.txt} {last.txt}", project_dir)
|
||||
assert result == "Dear Alice Smith"
|
||||
|
||||
def test_escaped_placeholder(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "name.txt").write_text("World")
|
||||
result = substitute_placeholders("Hello \\{name.txt}", project_dir)
|
||||
assert result == "Hello {name.txt}"
|
||||
|
||||
def test_double_escaped_placeholder(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "name.txt").write_text("World")
|
||||
result = substitute_placeholders("Hello \\\\{name.txt}", project_dir)
|
||||
assert result == "Hello \\World"
|
||||
|
||||
def test_triple_escaped_placeholder(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "name.txt").write_text("World")
|
||||
result = substitute_placeholders("Hello \\\\\\{name.txt}", project_dir)
|
||||
assert result == "Hello \\{name.txt}"
|
||||
|
||||
def test_placeholder_at_start(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "greeting.txt").write_text("Hi there")
|
||||
result = substitute_placeholders("{greeting.txt}!", project_dir)
|
||||
assert result == "Hi there!"
|
||||
|
||||
def test_placeholder_with_multiline_content(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "block.txt").write_text("line1\nline2\nline3")
|
||||
result = substitute_placeholders("Start: {block.txt} :End", project_dir)
|
||||
assert result == "Start: line1\nline2\nline3 :End"
|
||||
|
||||
|
||||
class TestExtractPlaceholderFiles:
|
||||
"""Test extraction of placeholder filenames from prompts."""
|
||||
|
||||
def test_no_placeholders(self) -> None:
|
||||
assert extract_placeholder_files("Hello world") == []
|
||||
|
||||
def test_single_placeholder(self) -> None:
|
||||
assert extract_placeholder_files("Hello {name.txt}") == ["name.txt"]
|
||||
|
||||
def test_multiple_placeholders(self) -> None:
|
||||
result = extract_placeholder_files("{a.txt} and {b.txt}")
|
||||
assert result == ["a.txt", "b.txt"]
|
||||
|
||||
def test_escaped_placeholder_ignored(self) -> None:
|
||||
assert extract_placeholder_files("\\{name.txt}") == []
|
||||
|
||||
def test_double_escaped_included(self) -> None:
|
||||
assert extract_placeholder_files("\\\\{name.txt}") == ["name.txt"]
|
||||
|
||||
def test_triple_escaped_ignored(self) -> None:
|
||||
assert extract_placeholder_files("\\\\\\{name.txt}") == []
|
||||
|
||||
|
||||
class TestResolvePrompt:
|
||||
"""Test full prompt resolution (file loading + placeholder substitution)."""
|
||||
|
||||
def test_inline_prompt_no_placeholders(self, project_dir: Path) -> None:
|
||||
assert resolve_prompt("Just a string", project_dir) == "Just a string"
|
||||
|
||||
def test_file_prompt(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "prompt.txt").write_text("From file")
|
||||
result = resolve_prompt("prompt.txt", project_dir)
|
||||
assert result == "From file"
|
||||
|
||||
def test_nonexistent_file_treated_as_inline(self, project_dir: Path) -> None:
|
||||
result = resolve_prompt("no_such_file.txt", project_dir)
|
||||
assert result == "no_such_file.txt"
|
||||
|
||||
def test_inline_with_placeholder(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "style.txt").write_text("impressionist")
|
||||
result = resolve_prompt("Paint in {style.txt} style", project_dir)
|
||||
assert result == "Paint in impressionist style"
|
||||
|
||||
def test_multiline_prompt_with_placeholder(self, project_dir: Path) -> None:
|
||||
_ = (project_dir / "detail.txt").write_text("vivid colours")
|
||||
result = resolve_prompt("First line\nWith {detail.txt}", project_dir)
|
||||
assert result == "First line\nWith vivid colours"
|
||||
|
||||
def test_file_prompt_no_placeholder_processing(self, project_dir: Path) -> None:
|
||||
"""When the prompt is a file path, the file is loaded verbatim."""
|
||||
_ = (project_dir / "prompt.txt").write_text("Literal {not_a_file.txt}")
|
||||
result = resolve_prompt("prompt.txt", project_dir)
|
||||
assert result == "Literal {not_a_file.txt}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue