big rework

This commit is contained in:
John Lancaster
2026-08-29 20:10:30 -05:00
parent 73eb490537
commit de95477480
30 changed files with 558 additions and 971 deletions
-1
View File
@@ -1 +0,0 @@
"""Test package marker for intra-suite imports."""
-3
View File
@@ -1,3 +0,0 @@
from __future__ import annotations
# Global lightweight fixtures can be added here as the suite grows.
-57
View File
@@ -1,57 +0,0 @@
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
class TestPromptContentRenderer:
def test_renders_arguments_without_frontmatter(self) -> None:
rendered = render_prompt(
"jsfiddle-page-layout",
{"domain": "public library", "layout_brief": None},
)
assert not rendered.startswith("---")
assert "`domain`: public library" in rendered
assert "`layout_brief`: Not provided" in rendered
def test_rejects_placeholder_drift(self) -> None:
with pytest.raises(ValueError, match="placeholders do not match arguments"):
render_prompt("jsfiddle-page-layout", {"domain": "public library"})
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)
-121
View File
@@ -1,121 +0,0 @@
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() == []
@@ -1,21 +0,0 @@
from __future__ import annotations
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.load import get_docs_registry
pytestmark = pytest.mark.unit
class TestCurrentDocsIngestion:
"""Covers ingestion of the repository's current docs tree."""
def test_registry_includes_docs_and_excludes_skills(self) -> None:
"""Ensures the docs registry cannot duplicate native skill resources."""
registry = get_docs_registry()
assert PurePosixPath("index.md") in registry.docs_markdown_by_path
assert any(path.parts[0] == "prompts" for path in registry.docs_markdown_by_path)
assert all(path.parts[0] != "skills" for path in registry.docs_markdown_by_path)
@@ -1,33 +0,0 @@
from __future__ import annotations
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.models import parse_docs_path
pytestmark = pytest.mark.unit
class TestDocsPathValidation:
"""Covers canonical resource-path contracts."""
def test_parse_docs_path_returns_pure_posix_path(self) -> None:
path = parse_docs_path("guides/demo.md")
assert path == PurePosixPath("guides/demo.md")
assert isinstance(path, PurePosixPath)
@pytest.mark.parametrize(
"value",
(
"/absolute.md",
"../outside.md",
"guides\\demo.md",
"guides//demo.md",
"guides/demo.txt",
),
)
def test_parse_docs_path_rejects_invalid_paths(self, value: str) -> None:
with pytest.raises(ValueError):
parse_docs_path(value)
-27
View File
@@ -1,27 +0,0 @@
from pathlib import Path
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.load import load_markdown
pytestmark = pytest.mark.unit
class TestLoadMarkdown:
"""Covers recursive Markdown discovery and loading."""
def test_loads_markdown_in_stable_order(self, tmp_path: Path) -> None:
nested = tmp_path / "guides"
nested.mkdir()
(nested / "b.md").write_text("caf\u00e9\n", encoding="utf-8")
(nested / "a.md").write_text("alpha\n", encoding="utf-8")
(nested / "ignored.txt").write_text("ignored\n", encoding="utf-8")
docs = load_markdown(tmp_path)
assert list(docs) == [
PurePosixPath("guides/a.md"),
PurePosixPath("guides/b.md"),
]
assert docs[PurePosixPath("guides/b.md")] == "caf\u00e9\n"
-37
View File
@@ -1,37 +0,0 @@
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.load import read_docs_markdown_path
from personal_mcp.registry.models import DocsRegistry
pytestmark = pytest.mark.unit
def _make_registry() -> DocsRegistry:
index_path = PurePosixPath("index.md")
return DocsRegistry(
docs_markdown_by_path={index_path: "# index"},
docs_markdown_path_index=(index_path,),
)
def test_reads_docs_path_from_string_boundary() -> None:
payload = read_docs_markdown_path(_make_registry(), "index.md")
assert payload == {
"uri": "resource://docs/index.md",
"format": "markdown",
"source_path": "docs/index.md",
"content": "# index",
}
def test_rejects_skill_docs_path() -> None:
with pytest.raises(KeyError, match="unknown docs path"):
read_docs_markdown_path(_make_registry(), "skills/demo/SKILL.md")
def test_rejects_non_posix_docs_path() -> None:
with pytest.raises(ValueError, match="POSIX separators"):
read_docs_markdown_path(_make_registry(), "guides\\demo.md")
-67
View File
@@ -1,67 +0,0 @@
from importlib.resources import files
from importlib.resources.abc import Traversable
import pytest
import yaml
from fastmcp import Client
from fastmcp import FastMCP
from fastmcp.utilities.skills import get_skill_manifest
from fastmcp.utilities.skills import list_skills
from personal_mcp.skills import create_skills_provider
pytestmark = pytest.mark.unit
SKILLS_ROOT = files("personal_mcp").joinpath("docs", "skills")
def skill_directories() -> list[Traversable]:
return [
directory
for directory in SKILLS_ROOT.iterdir()
if directory.is_dir() and directory.joinpath("SKILL.md").is_file()
]
class TestSkillsProvider:
"""Covers native FastMCP skill discovery and retrieval."""
@pytest.mark.asyncio
async def test_discovers_authored_skills(self) -> None:
"""Ensures each authored skill is exposed with its description."""
expected_names = {directory.name for directory in skill_directories()}
mcp = FastMCP("skills-test")
mcp.add_provider(create_skills_provider())
async with Client(mcp) as client:
skills = await list_skills(client)
assert {skill.name for skill in skills} == expected_names
assert all(skill.description for skill in skills)
@pytest.mark.asyncio
async def test_reads_manifest_and_supporting_file(self) -> None:
"""Ensures manifests disclose hashed files that remain directly readable."""
mcp = FastMCP("skills-test")
mcp.add_provider(create_skills_provider())
async with Client(mcp) as client:
manifest = await get_skill_manifest(client, "mcp-details")
reference = next(file for file in manifest.files if file.path.startswith("references/"))
contents = await client.read_resource(f"skill://mcp-details/{reference.path}")
assert any(file.path == "SKILL.md" for file in manifest.files)
assert all(file.hash.startswith("sha256:") for file in manifest.files)
assert reference.size > 0
assert contents
def test_frontmatter_names_match_directories(self) -> None:
"""Ensures provider identity and authored skill names remain aligned."""
for directory in skill_directories():
skill_file = directory.joinpath("SKILL.md")
raw = skill_file.read_text(encoding="utf-8")
frontmatter = yaml.safe_load(raw.split("---", 2)[1])
assert set(frontmatter) == {"name", "description"}
assert frontmatter["name"] == directory.name
assert frontmatter["description"]
-55
View File
@@ -1,55 +0,0 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import pytest
import pytest_asyncio
from httpx import ASGITransport
from httpx import AsyncClient
from httpx2 import ASGITransport as McpASGITransport
from httpx2 import AsyncClient as McpAsyncClient
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
from personal_mcp.web.app import create_app
@pytest_asyncio.fixture
async def client() -> AsyncGenerator[AsyncClient]:
"""Provides an AsyncClient bound to a fresh application instance."""
app = create_app()
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
timeout=10.0,
) as test_client:
yield test_client
@pytest.fixture
def mcp_session_factory():
"""Provides an in-process context manager factory for MCP SDK sessions."""
@asynccontextmanager
async def create_session(*, initialize: bool = True) -> AsyncGenerator[ClientSession]:
app = create_app()
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
async with (
app.router.lifespan_context(app),
McpAsyncClient(
transport=McpASGITransport(app=app),
base_url="http://testserver",
timeout=10.0,
) as http_client,
streamable_http_client(
mcp_url,
http_client=http_client,
) as (read_stream, write_stream),
ClientSession(read_stream, write_stream) as session,
):
if initialize:
await session.initialize()
yield session
return create_session
-58
View File
@@ -1,58 +0,0 @@
from __future__ import annotations
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.smoke
class TestMcpHttpEndpoints:
"""Covers smoke-level HTTP checks for mounted MCP runtime endpoints."""
class TestHealthz:
"""Covers health endpoint smoke behavior."""
@pytest.mark.asyncio
async def test_returns_ok_payload(self, client: AsyncClient) -> None:
"""Ensures GET /healthz responds with a healthy status payload."""
response = await client.get("/healthz")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
class TestDocsRoute:
"""Covers static docs route smoke behavior."""
@pytest.mark.asyncio
async def test_serves_docs_entrypoint(self, client: AsyncClient) -> None:
"""Ensures GET /docs returns the docs site entrypoint response."""
response = await client.get("/docs", follow_redirects=True)
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
class TestMcpRoute:
"""Covers MCP transport endpoint smoke behavior."""
@pytest.mark.asyncio
async def test_does_not_publish_legacy_resource_bridge_tools(self, mcp_session_factory) -> None:
"""Ensures deprecated compatibility tools are not exposed on the MCP route."""
async with mcp_session_factory() as mcp_session:
tools_result = await mcp_session.list_tools()
tool_names = {tool.name for tool in tools_result.tools}
assert "search_skills" not in tool_names
assert "list_resources" not in tool_names
assert "read_resource" not in tool_names
@pytest.mark.asyncio
async def test_accepts_initialize_jsonrpc_request(
self,
mcp_session_factory,
) -> None:
"""Ensures POST /mcp accepts an initialize JSON-RPC request."""
async with mcp_session_factory(initialize=False) as mcp_session_uninitialized:
initialize_result = await mcp_session_uninitialized.initialize()
assert initialize_result.protocol_version
assert initialize_result.server_info.name
-79
View File
@@ -1,79 +0,0 @@
from __future__ import annotations
import pytest
from mcp_types import PromptReference
pytestmark = pytest.mark.smoke
EXPECTED_PROMPTS = {
"authoring",
"greenfield-architecture",
"jsfiddle-page-layout",
"mcp-consumer-repo-shim",
"nicegui-component-extraction",
"pytest-fill-scaffold",
"pytest-scaffold",
}
class TestMcpPromptSurface:
"""Covers smoke-level MCP prompt discovery and retrieval paths."""
class TestServerMetadata:
"""Covers client-facing identity and completion capabilities."""
@pytest.mark.asyncio
async def test_advertises_instructions_and_completions(self, mcp_session_factory) -> None:
"""Ensures VS Code receives server guidance and completion support."""
async with mcp_session_factory(initialize=False) as mcp_session:
result = await mcp_session.initialize()
assert result.instructions is not None
assert "skill://<name>/SKILL.md" in result.instructions
assert result.server_info.icons
assert result.server_info.icons[0].src.startswith("data:image/svg+xml;base64,")
assert result.capabilities.completions is not None
class TestPromptDiscovery:
"""Covers MCP prompts/list behavior using native prompt objects."""
@pytest.mark.asyncio
async def test_lists_prompt_objects(self, mcp_session_factory) -> None:
"""Ensures prompts/list returns native prompt objects with stable names."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_prompts()
assert {prompt.name for prompt in result.prompts} == EXPECTED_PROMPTS
assert all(prompt.description for prompt in result.prompts)
assert all(prompt.title for prompt in result.prompts)
@pytest.mark.asyncio
async def test_completes_authored_prompt_choices(self, mcp_session_factory) -> None:
"""Ensures finite prompt choices are available as IDE-style suggestions."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.complete(
PromptReference(type="ref/prompt", name="authoring"),
{"name": "artifact_type", "value": "pr"},
)
assert result.completion.values == ["prompt"]
class TestPromptResolution:
"""Covers MCP prompts/get behavior using native request and response objects."""
@pytest.mark.asyncio
async def test_gets_prompt_as_native_object(self, mcp_session_factory) -> None:
"""Ensures prompts/get resolves a listed prompt into structured message objects."""
async with mcp_session_factory() as mcp_session:
resolved_prompt = await mcp_session.get_prompt(
name="authoring",
arguments={
"artifact_type": "skill",
"artifact_id": "demo-skill",
"goal": "Demonstrate native prompt rendering.",
},
)
assert resolved_prompt.messages
assert all(message.content for message in resolved_prompt.messages)
assert "`artifact_id`: demo-skill" in resolved_prompt.messages[0].content.text
-59
View File
@@ -1,59 +0,0 @@
from __future__ import annotations
import json
import pytest
pytestmark = pytest.mark.smoke
class TestMcpSkillsSurface:
"""Covers native skill resources over the HTTP MCP surface."""
class TestTools:
"""Covers absence of deprecated compatibility tools."""
@pytest.mark.asyncio
async def test_does_not_expose_resource_bridge_tools(self, mcp_session_factory) -> None:
"""Ensures native resources are not projected through legacy compatibility tools."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_tools()
tool_names = {tool.name for tool in result.tools}
assert "search_skills" not in tool_names
assert "list_resources" not in tool_names
assert "read_resource" not in tool_names
class TestResources:
"""Covers native skill resources, manifests, and file templates."""
@pytest.mark.asyncio
async def test_lists_main_file_and_manifest(self, mcp_session_factory) -> None:
"""Ensures resources/list exposes native skill entry points."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_resources()
resource_uris = {str(resource.uri) for resource in result.resources}
assert "skill://mcp-details/SKILL.md" in resource_uris
assert "skill://mcp-details/_manifest" in resource_uris
@pytest.mark.asyncio
async def test_lists_resource_templates(self, mcp_session_factory) -> None:
"""Ensures supporting files use per-skill wildcard templates."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_resource_templates()
template_uris = {template.uri_template for template in result.resource_templates}
assert "skill://mcp-details/{path*}" in template_uris
@pytest.mark.asyncio
async def test_reads_manifest_and_supporting_file(self, mcp_session_factory) -> None:
"""Ensures manifest paths resolve through the supporting-file template."""
async with mcp_session_factory() as mcp_session:
manifest_result = await mcp_session.read_resource("skill://mcp-details/_manifest")
manifest = json.loads(manifest_result.contents[0].text)
reference = next(file["path"] for file in manifest["files"] if file["path"].startswith("references/"))
reference_result = await mcp_session.read_resource(f"skill://mcp-details/{reference}")
assert manifest["skill"] == "mcp-details"
assert reference_result.contents