prompt markdown
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from personal_mcp.prompts.content import load_prompt_definition
|
||||
from personal_mcp.prompts.content import render_prompt
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
@@ -23,3 +26,32 @@ class TestPromptContentRenderer:
|
||||
def test_rejects_invalid_prompt_id(self) -> None:
|
||||
with pytest.raises(ValueError, match="lowercase kebab-case"):
|
||||
render_prompt("../outside", {})
|
||||
|
||||
def test_rejects_missing_prompt_metadata(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text("---\nicon: lucide/messages-square\n---\n\n# Body\n", encoding="utf-8")
|
||||
|
||||
with pytest.raises(ValueError, match="missing the 'prompt' block"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
def test_rejects_unknown_prompt_metadata(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text(
|
||||
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: {}, unknown: true}\n"
|
||||
"---\n\n# Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Extra inputs are not permitted"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
def test_rejects_declared_placeholder_drift(self, tmp_path: Path) -> None:
|
||||
document = tmp_path / "PROMPT.md"
|
||||
document.write_text(
|
||||
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: "
|
||||
"{topic: {description: Topic, required: true}}}\n---\n\n# Body\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="placeholders do not match arguments"):
|
||||
load_prompt_definition("demo", document)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.exceptions import PromptError
|
||||
|
||||
from personal_mcp.prompts import create_prompts_provider
|
||||
|
||||
@@ -17,7 +20,20 @@ EXPECTED_PROMPTS = {
|
||||
}
|
||||
|
||||
|
||||
class TestPromptFileSystemProvider:
|
||||
def write_prompt(document: Path, *, description: str, heading: str = "Demo") -> None:
|
||||
document.parent.mkdir(parents=True, exist_ok=True)
|
||||
document.write_text(
|
||||
"---\n"
|
||||
f"prompt: {{version: '1.0.0', description: {description!r}, tags: [demo], arguments: "
|
||||
"{kind: {description: 'Kind to render.', required: true, choices: [first, second]}, "
|
||||
"note: {description: 'Optional note.', required: false}}}\n"
|
||||
"---\n\n"
|
||||
f"# {heading}\n\nKind: {{{{kind}}}}\n\nNote: {{{{note}}}}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class TestMarkdownPromptsProvider:
|
||||
@pytest.mark.asyncio
|
||||
async def test_discovers_exact_authored_set(self) -> None:
|
||||
mcp = FastMCP("prompts-test")
|
||||
@@ -47,6 +63,59 @@ class TestPromptFileSystemProvider:
|
||||
)
|
||||
|
||||
required = {argument.name for argument in authoring.arguments or [] if argument.required}
|
||||
artifact_type = next(argument for argument in authoring.arguments or [] if argument.name == "artifact_type")
|
||||
assert required == {"artifact_type", "artifact_id", "goal"}
|
||||
assert artifact_type.description == "Artifact type to create.\n\nAccepted values: skill, prompt, shim."
|
||||
assert result.messages
|
||||
assert "`artifact_id`: demo-skill" in result.messages[0].content.text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enforces_required_arguments_and_choices(self) -> None:
|
||||
provider = create_prompts_provider()
|
||||
prompt = await provider.get_prompt("authoring")
|
||||
|
||||
assert prompt is not None
|
||||
with pytest.raises(PromptError, match="Missing required arguments"):
|
||||
await prompt.render({"artifact_type": "skill"})
|
||||
with pytest.raises(PromptError, match="must be one of"):
|
||||
await prompt.render(
|
||||
{
|
||||
"artifact_type": "unsupported",
|
||||
"artifact_id": "demo-skill",
|
||||
"goal": "Demonstrate validation.",
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_live_loads_edits_without_python_components(self, tmp_path: Path) -> None:
|
||||
prompts_root = tmp_path / "prompts"
|
||||
prompts_root.mkdir()
|
||||
provider = create_prompts_provider(prompts_root)
|
||||
mcp = FastMCP("prompts-test")
|
||||
mcp.add_provider(provider)
|
||||
|
||||
async with Client(mcp) as client:
|
||||
assert await client.list_prompts() == []
|
||||
|
||||
document = prompts_root / "dynamic-demo" / "PROMPT.md"
|
||||
write_prompt(document, description="Initial description")
|
||||
|
||||
prompts = await client.list_prompts()
|
||||
assert [prompt.name for prompt in prompts] == ["dynamic-demo"]
|
||||
assert prompts[0].description == "Initial description"
|
||||
result = await client.get_prompt("dynamic-demo", {"kind": "first"})
|
||||
assert "# Demo" in result.messages[0].content.text
|
||||
assert "Note: Not provided" in result.messages[0].content.text
|
||||
|
||||
write_prompt(document, description="Updated description", heading="Updated")
|
||||
|
||||
prompts = await client.list_prompts()
|
||||
assert prompts[0].description == "Updated description"
|
||||
result = await client.get_prompt("dynamic-demo", {"kind": "second", "note": "ready"})
|
||||
assert "# Updated" in result.messages[0].content.text
|
||||
assert "Note: ready" in result.messages[0].content.text
|
||||
|
||||
document.unlink()
|
||||
document.parent.rmdir()
|
||||
|
||||
assert await client.list_prompts() == []
|
||||
|
||||
Reference in New Issue
Block a user