Files

439 lines
20 KiB
Python
Raw Permalink Normal View History

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.adapters.sqlite_search import SQLiteSearchIndex
from gewu.application.reading import CanonicalNodeReader
from gewu.application.search import CanonicalSearchService
from gewu.bootstrap.app import create_app
from gewu.domain.contracts import EdgeEndpoint, Scope, SearchRequest, TypedEdge
from gewu.domain.validation import evidence_digest
from tests.domain.proposal_fixtures import CLAIM, EVIDENCE, NODE, SOURCE_REVISION
def _canonical_path(prefix: str, stable_id: str) -> str:
key = hashlib.sha256(stable_id.encode()).hexdigest()
return f"{prefix}/{key}.yaml"
def test_project_include_resolution_across_http_mcp_and_direct_reads(tmp_path: Path) -> None:
async def exercise() -> None:
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
dome_node = NODE.model_copy(
update={
"node_id": "dome:scope-default-node",
"summary": "scopeprobe dome project record",
}
)
video_source = SOURCE_REVISION.model_copy(
update={
"source_id": "video:scope-source",
"revision_id": "video:scope-source:r1",
"title": "Video-only scope source",
"ingested_for": ("interactive-video",),
"artifact_sha256": None,
"media_type": "application/octet-stream",
"byte_length": 0,
"original_url": "https://video.example/scope-source",
"archive_status": "reference-only",
}
)
video_evidence = EVIDENCE.model_copy(
update={"source_revision_id": video_source.revision_id}
)
video_assessment = CLAIM.assessment.model_copy(
update={"evidence_digests": (evidence_digest(video_evidence),)}
)
video_claim = CLAIM.model_copy(
update={"evidence": (video_evidence,), "assessment": video_assessment}
)
video_node = NODE.model_copy(
update={
"project_id": "interactive-video",
"node_id": "video:scope-default-node",
"title": "Video scope fixture",
"summary": "scopeprobe interactive video project record",
"claims": (video_claim,),
}
)
edge = TypedEdge(
edge_id="scope:cross-project-edge",
edge_type="compared_with",
source=EdgeEndpoint(kind="node", id=dome_node.node_id),
target=EdgeEndpoint(kind="node", id=video_node.node_id),
)
files = {
f"projects/{hashlib.sha256(dome_node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(dome_node.node_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
dome_node.model_dump(mode="json")
).encode(),
f"projects/{hashlib.sha256(video_node.project_id.encode()).hexdigest()}/nodes/"
f"{hashlib.sha256(video_node.node_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
video_node.model_dump(mode="json")
).encode(),
(
f"sources/{hashlib.sha256(video_source.source_id.encode()).hexdigest()}/"
f"{hashlib.sha256(video_source.revision_id.encode()).hexdigest()}.yaml"
): yaml.safe_dump(video_source.model_dump(mode="json")).encode(),
_canonical_path("edges", edge.edge_id): yaml.safe_dump(
edge.model_dump(mode="json")
).encode(),
}
repository.publish_files(
expected_head=repository.head(),
files=files,
message="Add project-scope fixtures",
index_directory=tmp_path / "publishing",
)
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
all_scope_token = identities.provision(
credential_id="all-scope-reader",
principal_id="all-scope-reader",
roles=("viewer",),
project_ids=("dome-performance", "interactive-video", "shared"),
)
dome_only_token = identities.provision(
credential_id="dome-only-reader",
principal_id="dome-only-reader",
roles=("viewer",),
project_ids=("dome-performance", "shared"),
)
search_index = SQLiteSearchIndex(tmp_path / "search.sqlite3", repository)
indexed_nodes = search_index.canonical_nodes()[1]
assert {node.node_id for node in indexed_nodes} == {dome_node.node_id, video_node.node_id}
lexical_scope_results = search_index.search(
query="scopeprobe",
project_ids=("dome-performance", "interactive-video"),
include_shared=False,
limit=10,
)
assert len(lexical_scope_results.nodes) == 2, [
item.node.summary for item in lexical_scope_results.nodes
]
search = CanonicalSearchService(
index=search_index,
project_ids_provider=lambda: ("dome-performance", "interactive-video"),
canonical_head=repository.head,
default_includes_provider=lambda project_id: (
("interactive-video", "shared") if project_id == "dome-performance" else ()
),
)
app = create_app(
identity_provider=identities,
node_reader=CanonicalNodeReader(repository),
search_service=search,
)
direct_service_result = search.search(
SearchRequest(
query="scopeprobe",
scope=Scope(projects=("dome-performance",), include_shared=False),
limit=10,
budget_tokens=50000,
),
principal=identities.authenticate(all_scope_token),
)
assert len(direct_service_result.data) == 2
async with app.router.lifespan_context(app):
async with httpx2.AsyncClient(
transport=httpx2.ASGITransport(app=app), base_url="http://localhost"
) as http:
default = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default.status_code == 200, default.text
assert {item["node"]["project_id"] for item in default.json()["data"]} == {
"dome-performance",
"interactive-video",
}, default.json()
assert video_source.revision_id in {
item["source_revision_id"] for item in default.json()["citations"]
}
explicit_empty = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_empty.status_code == 200, explicit_empty.text
assert {item["node"]["project_id"] for item in explicit_empty.json()["data"]} == {
"dome-performance"
}
assert video_source.revision_id not in {
item["source_revision_id"] for item in explicit_empty.json()["citations"]
}
explicit_include = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": ["interactive-video"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_include.status_code == 200, explicit_include.text
assert len(explicit_include.json()["data"]) == 2
denied_default = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert denied_default.status_code == 403
denied_explicit_include = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": ["interactive-video"],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert denied_explicit_include.status_code == 403
allowed_empty = await http.post(
"/v1/search",
json={
"query": "scopeprobe",
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert allowed_empty.status_code == 200
default_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": dome_node.node_id,
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default_neighbors.status_code == 200, default_neighbors.text
assert default_neighbors.json()["data"][0]["neighbor"]["id"] == video_node.node_id
restricted_neighbors = await http.post(
"/v1/neighbors",
json={
"node_id": dome_node.node_id,
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert restricted_neighbors.status_code == 200
assert restricted_neighbors.json()["data"] == []
default_sources = await http.post(
"/v1/sources",
json={
"scope": {"projects": ["dome-performance"], "include_shared": False},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert default_sources.status_code == 200, default_sources.text
assert video_source.revision_id in {
item["source"]["revision_id"] for item in default_sources.json()["data"]
}
explicit_empty_sources = await http.post(
"/v1/sources",
json={
"scope": {
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
},
"limit": 10,
"budget_tokens": 50000,
},
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert explicit_empty_sources.status_code == 200
assert video_source.revision_id not in {
item["source"]["revision_id"] for item in explicit_empty_sources.json()["data"]
}
direct_node_denied = await http.get(
f"/v1/projects/interactive-video/nodes/{video_node.node_id}",
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert direct_node_denied.status_code == 403
direct_source_denied = await http.get(
f"/v1/projects/interactive-video/sources/{video_source.revision_id}",
headers={"Authorization": f"Bearer {dome_only_token}"},
)
assert direct_source_denied.status_code == 403
direct_node_allowed = await http.get(
f"/v1/projects/interactive-video/nodes/{video_node.node_id}",
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert direct_node_allowed.status_code == 200
direct_source_allowed = await http.get(
f"/v1/projects/interactive-video/sources/{video_source.revision_id}",
headers={"Authorization": f"Bearer {all_scope_token}"},
)
assert direct_source_allowed.status_code == 200
http.headers["Authorization"] = f"Bearer {all_scope_token}"
async with Client(
streamable_http_client("http://localhost/mcp", http_client=http)
) as mcp:
mcp_default = await mcp.call_tool(
"search",
{
"query": "scopeprobe",
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default.is_error
assert isinstance(mcp_default.content[0], TextContent)
default_payload = json.loads(mcp_default.content[0].text)
assert len(default_payload["data"]) == 2
mcp_default_sources = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default_sources.is_error
assert isinstance(mcp_default_sources.content[0], TextContent)
assert video_source.revision_id in {
row["source"]["revision_id"]
for row in json.loads(mcp_default_sources.content[0].text)["data"]
}
mcp_default_neighbors = await mcp.call_tool(
"neighbors",
{
"node_id": dome_node.node_id,
"projects": ["dome-performance"],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_default_neighbors.is_error
assert isinstance(mcp_default_neighbors.content[0], TextContent)
assert (
json.loads(mcp_default_neighbors.content[0].text)["data"][0]["neighbor"][
"id"
]
== video_node.node_id
)
mcp_explicit_empty = await mcp.call_tool(
"search",
{
"query": "scopeprobe",
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_explicit_empty.is_error
assert isinstance(mcp_explicit_empty.content[0], TextContent)
assert len(json.loads(mcp_explicit_empty.content[0].text)["data"]) == 1
mcp_empty_sources = await mcp.call_tool(
"list_sources",
{
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_empty_sources.is_error
assert isinstance(mcp_empty_sources.content[0], TextContent)
assert video_source.revision_id not in {
row["source"]["revision_id"]
for row in json.loads(mcp_empty_sources.content[0].text)["data"]
}
mcp_empty_neighbors = await mcp.call_tool(
"neighbors",
{
"node_id": dome_node.node_id,
"projects": ["dome-performance"],
"include_projects": [],
"include_shared": False,
"all_projects": False,
"edge_types": [],
"limit": 10,
"budget_tokens": 50000,
},
)
assert not mcp_empty_neighbors.is_error
assert isinstance(mcp_empty_neighbors.content[0], TextContent)
assert json.loads(mcp_empty_neighbors.content[0].text)["data"] == []
asyncio.run(exercise())