122 lines
4.7 KiB
Python
122 lines
4.7 KiB
Python
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
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
EXPECTED_PROMPTS = {
|
|
"authoring",
|
|
"greenfield-architecture",
|
|
"jsfiddle-page-layout",
|
|
"mcp-consumer-repo-shim",
|
|
"nicegui-component-extraction",
|
|
"pytest-fill-scaffold",
|
|
"pytest-scaffold",
|
|
}
|
|
|
|
|
|
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")
|
|
mcp.add_provider(create_prompts_provider())
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
|
|
assert {prompt.name for prompt in prompts} == EXPECTED_PROMPTS
|
|
assert all(prompt.description for prompt in prompts)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_exposes_typed_arguments_and_renders_markdown(self) -> None:
|
|
mcp = FastMCP("prompts-test")
|
|
mcp.add_provider(create_prompts_provider())
|
|
|
|
async with Client(mcp) as client:
|
|
prompts = await client.list_prompts()
|
|
authoring = next(prompt for prompt in prompts if prompt.name == "authoring")
|
|
result = await client.get_prompt(
|
|
"authoring",
|
|
{
|
|
"artifact_type": "skill",
|
|
"artifact_id": "demo-skill",
|
|
"goal": "Demonstrate typed prompts.",
|
|
},
|
|
)
|
|
|
|
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() == []
|