This commit is contained in:
1 parent
4db3f45164
commit
d25c04cdd2
175 files changed
+56243
-15
No files matched your search
@@ -0,0 +1,139 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import httpx2
|
||||
import yaml
|
||||
from mcp import Client
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.types import TextContent
|
||||
|
||||
from gewu.adapters.git_repository import GitCanonicalRepository
|
||||
from gewu.adapters.sqlite_identity import SQLiteIdentityStore
|
||||
from gewu.application.reading import CanonicalNodeReader
|
||||
from gewu.bootstrap.app import create_app
|
||||
from tests.domain.proposal_fixtures import NODE, SOURCE_REVISION
|
||||
|
||||
|
||||
def test_mcp_exposes_only_authenticated_canonical_read(tmp_path: Path) -> None:
|
||||
async def exercise() -> None:
|
||||
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
|
||||
project_key = hashlib.sha256(NODE.project_id.encode()).hexdigest()
|
||||
node_key = hashlib.sha256(NODE.node_id.encode()).hexdigest()
|
||||
source_key = hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()
|
||||
source_revision_key = hashlib.sha256(SOURCE_REVISION.revision_id.encode()).hexdigest()
|
||||
repository.publish_files(
|
||||
expected_head=repository.head(),
|
||||
files={
|
||||
f"projects/{project_key}/nodes/{node_key}.yaml": yaml.safe_dump(
|
||||
NODE.model_dump(mode="json")
|
||||
).encode(),
|
||||
f"sources/{source_key}/{source_revision_key}.yaml": yaml.safe_dump(
|
||||
SOURCE_REVISION.model_dump(mode="json")
|
||||
).encode(),
|
||||
},
|
||||
message="Publish test node",
|
||||
index_directory=tmp_path / "publishing",
|
||||
)
|
||||
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
|
||||
token = identities.provision(
|
||||
credential_id="reader-key",
|
||||
principal_id="research-agent",
|
||||
roles=("agent",),
|
||||
project_ids=(NODE.project_id,),
|
||||
)
|
||||
app = create_app(
|
||||
identity_provider=identities,
|
||||
node_reader=CanonicalNodeReader(repository),
|
||||
)
|
||||
async with app.router.lifespan_context(app):
|
||||
async with httpx2.AsyncClient(
|
||||
transport=httpx2.ASGITransport(app=app),
|
||||
base_url="http://localhost",
|
||||
headers={"Authorization": f"Bearer {token}"},
|
||||
) as http:
|
||||
async with Client(
|
||||
streamable_http_client("http://localhost/mcp", http_client=http)
|
||||
) as mcp:
|
||||
tools = await mcp.list_tools()
|
||||
assert {tool.name for tool in tools.tools} == {
|
||||
"diagnostics",
|
||||
"get_node",
|
||||
"node_history",
|
||||
"get_source",
|
||||
}
|
||||
diagnostic_result = await mcp.call_tool(
|
||||
"diagnostics", {"projects": [NODE.project_id]}
|
||||
)
|
||||
assert not diagnostic_result.is_error
|
||||
diagnostic_text = diagnostic_result.content[0]
|
||||
assert isinstance(diagnostic_text, TextContent)
|
||||
diagnostics = json.loads(diagnostic_text.text)
|
||||
assert diagnostics["transport"] == "mcp"
|
||||
assert diagnostics["selected_scope"]["projects"] == [NODE.project_id]
|
||||
assert diagnostics["index_state"] == "not_configured"
|
||||
assert "get_node" in diagnostics["authorized_operations"]
|
||||
assert "proposal_submit" not in diagnostics["authorized_operations"]
|
||||
assert token not in diagnostic_text.text
|
||||
denied_diagnostics = await mcp.call_tool(
|
||||
"diagnostics", {"projects": ["interactive-video"]}
|
||||
)
|
||||
assert denied_diagnostics.is_error
|
||||
result = await mcp.call_tool(
|
||||
"get_node",
|
||||
{"project_id": NODE.project_id, "node_id": NODE.node_id},
|
||||
)
|
||||
assert not result.is_error
|
||||
content = result.content[0]
|
||||
assert content.type == "text"
|
||||
envelope = json.loads(content.text)
|
||||
assert envelope["canonical_revision"] == repository.head()
|
||||
assert envelope["data"][0]["node_id"] == NODE.node_id
|
||||
assert envelope["citations"][0]["quote"] == NODE.claims[0].evidence[0].quote
|
||||
history = await mcp.call_tool(
|
||||
"node_history",
|
||||
{"project_id": NODE.project_id, "node_id": NODE.node_id},
|
||||
)
|
||||
assert not history.is_error
|
||||
assert isinstance(history.content[0], TextContent)
|
||||
assert len(json.loads(history.content[0].text)["data"]) == 1
|
||||
source = await mcp.call_tool(
|
||||
"get_source",
|
||||
{
|
||||
"project_id": NODE.project_id,
|
||||
"revision_id": SOURCE_REVISION.revision_id,
|
||||
},
|
||||
)
|
||||
assert not source.is_error
|
||||
assert isinstance(source.content[0], TextContent)
|
||||
assert json.loads(source.content[0].text)["data"][0]["revision_id"] == (
|
||||
SOURCE_REVISION.revision_id
|
||||
)
|
||||
assert identities.revoke("reader-key")
|
||||
revoked = await mcp.call_tool(
|
||||
"get_node",
|
||||
{"project_id": NODE.project_id, "node_id": NODE.node_id},
|
||||
)
|
||||
assert revoked.is_error
|
||||
revoked_message = revoked.content[0]
|
||||
assert isinstance(revoked_message, TextContent)
|
||||
assert NODE.node_id not in revoked_message.text
|
||||
|
||||
async with httpx2.AsyncClient(
|
||||
transport=httpx2.ASGITransport(app=app),
|
||||
base_url="http://localhost",
|
||||
) as http:
|
||||
async with Client(
|
||||
streamable_http_client("http://localhost/mcp", http_client=http)
|
||||
) as mcp:
|
||||
unauthenticated = await mcp.call_tool(
|
||||
"get_node",
|
||||
{"project_id": NODE.project_id, "node_id": NODE.node_id},
|
||||
)
|
||||
assert unauthenticated.is_error
|
||||
error_message = unauthenticated.content[0]
|
||||
assert isinstance(error_message, TextContent)
|
||||
assert NODE.node_id not in error_message.text
|
||||
|
||||
asyncio.run(exercise())
|
||||
Reference in new issue
Block a user