2026-10-14 19:21:35 +08:00
|
|
|
import asyncio
|
|
|
|
|
import hashlib
|
|
|
|
|
import json
|
|
|
|
|
from datetime import UTC, datetime
|
|
|
|
|
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, NodeType, TypedEdge
|
|
|
|
|
from tests.domain.proposal_fixtures import NODE, PROJECTS, SOURCE_REVISION
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_authenticated_search_scope_through_http_and_mcp(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.revision_id.encode()).hexdigest()
|
|
|
|
|
stale_node = NODE.model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"as_of": datetime(2024, 1, 1, tzinfo=UTC),
|
|
|
|
|
"stale_after": datetime(2025, 1, 1, tzinfo=UTC),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
problem_node = NODE.model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"node_id": "dome:problem-browse-fixture",
|
|
|
|
|
"node_type": NodeType.PROBLEM,
|
|
|
|
|
"title": "Problem browse fixture",
|
|
|
|
|
"value": None,
|
|
|
|
|
"unit": None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
problem_key = hashlib.sha256(problem_node.node_id.encode()).hexdigest()
|
|
|
|
|
older_source = SOURCE_REVISION.model_copy(
|
|
|
|
|
update={
|
|
|
|
|
"source_id": "dome:older-pilot-report",
|
|
|
|
|
"revision_id": "dome:older-pilot-report:r1",
|
|
|
|
|
"title": "Older pilot installation report",
|
|
|
|
|
"captured_at": datetime(2025, 1, 1, tzinfo=UTC),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
related = NODE.model_copy(update={"node_id": "z:dome-related"})
|
|
|
|
|
related_key = hashlib.sha256(related.node_id.encode()).hexdigest()
|
|
|
|
|
related_two = NODE.model_copy(update={"node_id": "zz:dome-related-two"})
|
|
|
|
|
related_two_key = hashlib.sha256(related_two.node_id.encode()).hexdigest()
|
|
|
|
|
edge = TypedEdge(
|
|
|
|
|
edge_id="dome:related-edge",
|
|
|
|
|
edge_type="compared_with",
|
|
|
|
|
source=EdgeEndpoint(kind="node", id=NODE.node_id),
|
|
|
|
|
target=EdgeEndpoint(kind="node", id=related.node_id),
|
|
|
|
|
)
|
|
|
|
|
edge_two = TypedEdge(
|
|
|
|
|
edge_id="dome:related-edge-two",
|
|
|
|
|
edge_type="compared_with",
|
|
|
|
|
source=EdgeEndpoint(kind="node", id=NODE.node_id),
|
|
|
|
|
target=EdgeEndpoint(kind="node", id=related_two.node_id),
|
|
|
|
|
)
|
|
|
|
|
repository.publish_files(
|
|
|
|
|
expected_head=repository.head(),
|
|
|
|
|
files={
|
|
|
|
|
f"projects/{project_key}/nodes/{node_key}.yaml": yaml.safe_dump(
|
|
|
|
|
stale_node.model_dump(mode="json")
|
|
|
|
|
).encode(),
|
|
|
|
|
f"projects/{project_key}/nodes/{problem_key}.yaml": yaml.safe_dump(
|
|
|
|
|
problem_node.model_dump(mode="json")
|
|
|
|
|
).encode(),
|
|
|
|
|
f"projects/{project_key}/nodes/{related_key}.yaml": yaml.safe_dump(
|
|
|
|
|
related.model_dump(mode="json")
|
|
|
|
|
).encode(),
|
|
|
|
|
f"projects/{project_key}/nodes/{related_two_key}.yaml": yaml.safe_dump(
|
|
|
|
|
related_two.model_dump(mode="json")
|
|
|
|
|
).encode(),
|
|
|
|
|
(
|
|
|
|
|
f"sources/{hashlib.sha256(SOURCE_REVISION.source_id.encode()).hexdigest()}/"
|
|
|
|
|
f"{source_key}.yaml"
|
|
|
|
|
): yaml.safe_dump(SOURCE_REVISION.model_dump(mode="json")).encode(),
|
|
|
|
|
(
|
|
|
|
|
f"sources/{hashlib.sha256(older_source.source_id.encode()).hexdigest()}/"
|
|
|
|
|
f"{hashlib.sha256(older_source.revision_id.encode()).hexdigest()}.yaml"
|
|
|
|
|
): yaml.safe_dump(older_source.model_dump(mode="json")).encode(),
|
|
|
|
|
f"edges/{hashlib.sha256(edge.edge_id.encode()).hexdigest()}.yaml": yaml.safe_dump(
|
|
|
|
|
edge.model_dump(mode="json")
|
|
|
|
|
).encode(),
|
|
|
|
|
(
|
|
|
|
|
f"edges/{hashlib.sha256(edge_two.edge_id.encode()).hexdigest()}.yaml"
|
|
|
|
|
): yaml.safe_dump(edge_two.model_dump(mode="json")).encode(),
|
|
|
|
|
},
|
|
|
|
|
message="Add searchable node",
|
|
|
|
|
index_directory=tmp_path / "publishing",
|
|
|
|
|
)
|
|
|
|
|
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
|
|
|
|
|
token = identities.provision(
|
|
|
|
|
credential_id="search-key",
|
|
|
|
|
principal_id="search-reader",
|
|
|
|
|
roles=("viewer",),
|
|
|
|
|
project_ids=("dome-performance",),
|
|
|
|
|
)
|
|
|
|
|
search = CanonicalSearchService(
|
|
|
|
|
index=SQLiteSearchIndex(tmp_path / "search.sqlite3", repository),
|
|
|
|
|
project_ids_provider=lambda: PROJECTS.keys(),
|
|
|
|
|
canonical_head=repository.head,
|
|
|
|
|
)
|
|
|
|
|
app = create_app(
|
|
|
|
|
identity_provider=identities,
|
|
|
|
|
node_reader=CanonicalNodeReader(repository),
|
|
|
|
|
search_service=search,
|
|
|
|
|
)
|
|
|
|
|
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:
|
|
|
|
|
diagnostic_unbuilt = await http.get("/v1/diagnostics?projects=dome-performance")
|
|
|
|
|
assert diagnostic_unbuilt.status_code == 200
|
|
|
|
|
unbuilt_state = diagnostic_unbuilt.json()
|
|
|
|
|
assert unbuilt_state["index_state"] == "not_built"
|
|
|
|
|
assert unbuilt_state["index_revision"] is None
|
2026-10-16 20:02:29 +08:00
|
|
|
assert unbuilt_state["index_schema_version"] == 5
|
2026-10-14 19:21:35 +08:00
|
|
|
result = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
2026-10-16 20:02:29 +08:00
|
|
|
"include_evidence": False,
|
2026-10-14 19:21:35 +08:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert result.status_code == 200, result.text
|
|
|
|
|
envelope = result.json()
|
|
|
|
|
assert envelope["retrieval_mode"] == "keyword"
|
|
|
|
|
assert envelope["index_revision"] == repository.head()
|
|
|
|
|
assert envelope["data"][0]["node"]["node_id"] == NODE.node_id
|
2026-10-16 20:02:29 +08:00
|
|
|
assert envelope["data"][0]["evidence_included"] is False
|
|
|
|
|
compact_claim = envelope["data"][0]["node"]["claims"][0]
|
|
|
|
|
assert "evidence" not in compact_claim
|
|
|
|
|
assert compact_claim["evidence_count"] == len(NODE.claims[0].evidence)
|
|
|
|
|
assert envelope["citations"] == []
|
|
|
|
|
full_result = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
"include_evidence": True,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert full_result.status_code == 200, full_result.text
|
|
|
|
|
full_row = full_result.json()["data"][0]
|
|
|
|
|
assert full_row["evidence_included"] is True
|
|
|
|
|
assert full_row["node"]["claims"][0]["evidence"]
|
2026-10-16 20:13:59 +08:00
|
|
|
assert (
|
|
|
|
|
full_result.json()["citations"][0]["quote"]
|
|
|
|
|
== NODE.claims[0].evidence[0].quote
|
|
|
|
|
)
|
2026-10-16 20:02:29 +08:00
|
|
|
browse = await http.post(
|
|
|
|
|
"/v1/knowledge/browse",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"limit": 2,
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert browse.status_code == 200, browse.text
|
|
|
|
|
browse_body = browse.json()
|
|
|
|
|
assert browse_body["project_node_count"] == 4
|
|
|
|
|
assert browse_body["filtered_node_count"] == 4
|
|
|
|
|
assert len(browse_body["nodes"]) == 2
|
|
|
|
|
assert browse_body["next_cursor"]
|
|
|
|
|
assert [item["title"] for item in browse_body["nodes"]] == sorted(
|
|
|
|
|
item["title"] for item in browse_body["nodes"]
|
|
|
|
|
)
|
|
|
|
|
assert browse_body["facets"]["tags"]
|
|
|
|
|
browse_page_two = await http.post(
|
|
|
|
|
"/v1/knowledge/browse",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"limit": 2,
|
|
|
|
|
"cursor": browse_body["next_cursor"],
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert browse_page_two.status_code == 200
|
|
|
|
|
assert len(browse_page_two.json()["nodes"]) == 2
|
|
|
|
|
assert browse_page_two.json()["next_cursor"] is None
|
|
|
|
|
filtered_browse = await http.post(
|
|
|
|
|
"/v1/knowledge/browse",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"node_types": ["problem"],
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert filtered_browse.status_code == 200
|
|
|
|
|
assert filtered_browse.json()["filtered_node_count"] == 1
|
|
|
|
|
assert filtered_browse.json()["nodes"][0]["node_type"] == "problem"
|
|
|
|
|
assert filtered_browse.json()["project_node_count"] == 4
|
|
|
|
|
graph = await http.post(
|
|
|
|
|
"/v1/knowledge/graph",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert graph.status_code == 200, graph.text
|
|
|
|
|
assert len(graph.json()["nodes"]) == 4
|
|
|
|
|
assert len(graph.json()["edges"]) == 2
|
|
|
|
|
assert graph.json()["truncated"] is False
|
|
|
|
|
neighborhood = await http.post(
|
|
|
|
|
"/v1/knowledge/graph",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"center_node_id": NODE.node_id,
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert neighborhood.status_code == 200, neighborhood.text
|
|
|
|
|
assert neighborhood.json()["center_node_id"] == NODE.node_id
|
|
|
|
|
assert NODE.node_id in {item["node_id"] for item in neighborhood.json()["nodes"]}
|
|
|
|
|
assert len(neighborhood.json()["nodes"]) == 3
|
|
|
|
|
center_only = await http.post(
|
|
|
|
|
"/v1/knowledge/graph",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"center_node_id": NODE.node_id,
|
|
|
|
|
"max_nodes": 1,
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert center_only.status_code == 200
|
|
|
|
|
assert [item["node_id"] for item in center_only.json()["nodes"]] == [NODE.node_id]
|
|
|
|
|
assert center_only.json()["edges"] == []
|
|
|
|
|
denied_neighborhood = await http.post(
|
|
|
|
|
"/v1/knowledge/graph",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["other-project"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"center_node_id": NODE.node_id,
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert denied_neighborhood.status_code == 404
|
|
|
|
|
capped_graph = await http.post(
|
|
|
|
|
"/v1/knowledge/graph",
|
2026-10-16 20:13:59 +08:00
|
|
|
json={
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_projects": [],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"max_nodes": 1,
|
|
|
|
|
},
|
2026-10-16 20:02:29 +08:00
|
|
|
)
|
|
|
|
|
assert capped_graph.status_code == 200
|
|
|
|
|
assert len(capped_graph.json()["nodes"]) == 1
|
|
|
|
|
assert capped_graph.json()["truncated"] is True
|
2026-10-14 19:21:35 +08:00
|
|
|
listed_sources = await http.post(
|
|
|
|
|
"/v1/sources",
|
|
|
|
|
json={
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert listed_sources.status_code == 200, listed_sources.text
|
2026-10-16 20:02:29 +08:00
|
|
|
source_nodes_resp = await http.get(
|
|
|
|
|
f"/v1/projects/dome-performance/sources/{SOURCE_REVISION.revision_id}/nodes"
|
|
|
|
|
)
|
|
|
|
|
assert source_nodes_resp.status_code == 200, source_nodes_resp.text
|
|
|
|
|
source_nodes_data = source_nodes_resp.json()
|
|
|
|
|
assert source_nodes_data["total"] >= 1
|
2026-10-16 20:13:59 +08:00
|
|
|
assert source_nodes_data["nodes"][0]["relation_basis"] in (
|
|
|
|
|
"citation",
|
|
|
|
|
"edge",
|
|
|
|
|
"both",
|
|
|
|
|
)
|
2026-10-16 20:02:29 +08:00
|
|
|
|
|
|
|
|
assert listed_sources.json()["filtered_count"] == 2
|
2026-10-14 19:21:35 +08:00
|
|
|
assert listed_sources.json()["data"][0]["source"]["revision_id"] == (
|
|
|
|
|
SOURCE_REVISION.revision_id
|
|
|
|
|
)
|
|
|
|
|
source_page_one = await http.post(
|
|
|
|
|
"/v1/sources",
|
|
|
|
|
json={
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
source_cursor = source_page_one.json()["next_cursor"]
|
|
|
|
|
assert source_cursor
|
|
|
|
|
source_page_two = await http.post(
|
|
|
|
|
"/v1/sources",
|
|
|
|
|
json={
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": source_cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert source_page_two.status_code == 200
|
|
|
|
|
assert source_page_two.json()["data"][0]["source"]["revision_id"] == (
|
|
|
|
|
older_source.revision_id
|
|
|
|
|
)
|
|
|
|
|
denied_sources = await http.post(
|
|
|
|
|
"/v1/sources",
|
|
|
|
|
json={
|
|
|
|
|
"scope": {"projects": ["interactive-video"], "include_shared": False},
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert denied_sources.status_code == 403
|
|
|
|
|
problem_filter = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "unmatched phrase",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"node_types": ["problem"],
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert problem_filter.status_code == 200, problem_filter.text
|
|
|
|
|
assert problem_filter.json()["data"] == []
|
|
|
|
|
problem_browse = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"node_types": ["problem"],
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert problem_browse.status_code == 200, problem_browse.text
|
|
|
|
|
assert [row["node"]["node_id"] for row in problem_browse.json()["data"]] == [
|
|
|
|
|
problem_node.node_id
|
|
|
|
|
]
|
|
|
|
|
unbounded_browse = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert unbounded_browse.status_code == 422
|
|
|
|
|
stale_filter = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"stale_only": True,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert stale_filter.status_code == 200, stale_filter.text
|
|
|
|
|
assert stale_filter.json()["data"][0]["node"]["node_id"] == NODE.node_id
|
|
|
|
|
facet_filter = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"tags": ["attendance"],
|
|
|
|
|
"audience": ["product"],
|
|
|
|
|
"use_for": ["comparison"],
|
|
|
|
|
"domain": ["production"],
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert facet_filter.status_code == 200, facet_filter.text
|
|
|
|
|
assert facet_filter.json()["data"][0]["node"]["node_id"] == NODE.node_id
|
|
|
|
|
unmatched_facet = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"domain": ["unrelated-domain"],
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert unmatched_facet.status_code == 200, unmatched_facet.text
|
|
|
|
|
assert unmatched_facet.json()["data"] == []
|
|
|
|
|
diagnostic_current = await http.get("/v1/diagnostics?projects=dome-performance")
|
|
|
|
|
assert diagnostic_current.status_code == 200
|
|
|
|
|
current_state = diagnostic_current.json()
|
|
|
|
|
assert current_state["index_state"] == "current"
|
|
|
|
|
assert current_state["index_revision"] == repository.head()
|
|
|
|
|
assert "proposal_submit" not in current_state["authorized_operations"]
|
|
|
|
|
projects = await http.get("/v1/projects")
|
|
|
|
|
assert projects.status_code == 200
|
|
|
|
|
assert projects.json() == ["dome-performance"]
|
|
|
|
|
page_one = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert page_one.status_code == 200
|
|
|
|
|
cursor = page_one.json()["next_cursor"]
|
|
|
|
|
assert cursor
|
|
|
|
|
page_two = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert page_two.status_code == 200
|
|
|
|
|
assert (
|
|
|
|
|
page_two.json()["data"][0]["node"]["node_id"]
|
|
|
|
|
!= page_one.json()["data"][0]["node"]["node_id"]
|
|
|
|
|
)
|
|
|
|
|
reused_cursor = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert reused_cursor.status_code == 422
|
|
|
|
|
changed_facet_cursor = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"node_types": ["problem"],
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert changed_facet_cursor.status_code == 422
|
|
|
|
|
changed_tag_cursor = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"tags": ["attendance"],
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert changed_tag_cursor.status_code == 422
|
|
|
|
|
adjacent = await http.post(
|
|
|
|
|
"/v1/neighbors",
|
|
|
|
|
json={
|
|
|
|
|
"node_id": NODE.node_id,
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert adjacent.status_code == 200, adjacent.text
|
|
|
|
|
assert adjacent.json()["data"][0]["neighbor"]["id"] == related.node_id
|
|
|
|
|
neighbor_cursor = adjacent.json()["next_cursor"]
|
|
|
|
|
assert neighbor_cursor
|
|
|
|
|
adjacent_two = await http.post(
|
|
|
|
|
"/v1/neighbors",
|
|
|
|
|
json={
|
|
|
|
|
"node_id": NODE.node_id,
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": neighbor_cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert adjacent_two.status_code == 200
|
|
|
|
|
assert adjacent_two.json()["data"][0]["neighbor"]["id"] == related_two.node_id
|
|
|
|
|
assert adjacent_two.json()["next_cursor"] is None
|
|
|
|
|
assert {citation["quote"] for citation in adjacent.json()["citations"]} == {
|
|
|
|
|
NODE.claims[0].evidence[0].quote
|
|
|
|
|
}
|
|
|
|
|
newer = NODE.model_copy(update={"node_id": "new:unrelated-revision-node"})
|
|
|
|
|
newer_key = hashlib.sha256(newer.node_id.encode()).hexdigest()
|
|
|
|
|
repository.publish_files(
|
|
|
|
|
expected_head=repository.head(),
|
|
|
|
|
files={
|
|
|
|
|
f"projects/{project_key}/nodes/{newer_key}.yaml": yaml.safe_dump(
|
|
|
|
|
newer.model_dump(mode="json")
|
|
|
|
|
).encode()
|
|
|
|
|
},
|
|
|
|
|
message="Advance canonical index for cursor-expiry check",
|
|
|
|
|
index_directory=tmp_path / "publishing",
|
|
|
|
|
)
|
|
|
|
|
diagnostic_lag = await http.get("/v1/diagnostics?projects=dome-performance")
|
|
|
|
|
assert diagnostic_lag.status_code == 200
|
|
|
|
|
lag_state = diagnostic_lag.json()
|
|
|
|
|
assert lag_state["index_state"] == "lagging"
|
|
|
|
|
assert lag_state["index_revision"] != repository.head()
|
|
|
|
|
assert lag_state["canonical_revision"] == repository.head()
|
|
|
|
|
expired_search = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert expired_search.status_code == 409
|
|
|
|
|
expired_neighbors = await http.post(
|
|
|
|
|
"/v1/neighbors",
|
|
|
|
|
json={
|
|
|
|
|
"node_id": NODE.node_id,
|
|
|
|
|
"scope": {"projects": ["dome-performance"], "include_shared": False},
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": neighbor_cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert expired_neighbors.status_code == 409
|
|
|
|
|
too_small = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"budget_tokens": 256,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert too_small.status_code == 413
|
|
|
|
|
dated_confidence_search = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"confidences": ["medium"],
|
|
|
|
|
"as_of_from": "2024-01-01",
|
|
|
|
|
"as_of_to": "2024-12-31",
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert dated_confidence_search.status_code == 200, dated_confidence_search.text
|
|
|
|
|
assert [
|
|
|
|
|
row["node"]["node_id"] for row in dated_confidence_search.json()["data"]
|
|
|
|
|
] == [NODE.node_id]
|
|
|
|
|
filtered_page = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"confidences": ["medium"],
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert filtered_page.status_code == 200
|
|
|
|
|
filter_cursor = filtered_page.json()["next_cursor"]
|
|
|
|
|
assert filter_cursor
|
|
|
|
|
changed_filter_page = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"confidences": ["low"],
|
|
|
|
|
"limit": 1,
|
|
|
|
|
"cursor": filter_cursor,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert changed_filter_page.status_code == 422
|
|
|
|
|
forbidden = await http.post(
|
|
|
|
|
"/v1/search",
|
|
|
|
|
json={
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"scope": {
|
|
|
|
|
"projects": ["interactive-video"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
},
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert forbidden.status_code == 403
|
|
|
|
|
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",
|
|
|
|
|
"list_projects",
|
|
|
|
|
"search",
|
|
|
|
|
"neighbors",
|
|
|
|
|
"list_sources",
|
|
|
|
|
}
|
|
|
|
|
listed = await mcp.call_tool("list_projects", {})
|
|
|
|
|
assert not listed.is_error
|
|
|
|
|
assert isinstance(listed.content[0], TextContent)
|
|
|
|
|
assert json.loads(listed.content[0].text) == ["dome-performance"]
|
|
|
|
|
searched = await mcp.call_tool(
|
|
|
|
|
"search",
|
|
|
|
|
{
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not searched.is_error
|
|
|
|
|
assert isinstance(searched.content[0], TextContent)
|
|
|
|
|
assert (
|
|
|
|
|
json.loads(searched.content[0].text)["data"][0]["node"]["node_id"]
|
|
|
|
|
== NODE.node_id
|
|
|
|
|
)
|
|
|
|
|
dated_confidence_tool = await mcp.call_tool(
|
|
|
|
|
"search",
|
|
|
|
|
{
|
|
|
|
|
"query": "visitors",
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
"confidences": ["medium"],
|
|
|
|
|
"as_of_from": "2024-01-01",
|
|
|
|
|
"as_of_to": "2024-12-31",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not dated_confidence_tool.is_error
|
|
|
|
|
assert [
|
|
|
|
|
row["node"]["node_id"]
|
|
|
|
|
for row in json.loads(dated_confidence_tool.content[0].text)["data"]
|
|
|
|
|
] == [NODE.node_id]
|
|
|
|
|
filtered_search = await mcp.call_tool(
|
|
|
|
|
"search",
|
|
|
|
|
{
|
|
|
|
|
"query": "unmatched phrase",
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
"node_types": ["problem"],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not filtered_search.is_error
|
|
|
|
|
assert json.loads(filtered_search.content[0].text)["data"] == []
|
|
|
|
|
browsed_problems = await mcp.call_tool(
|
|
|
|
|
"search",
|
|
|
|
|
{
|
|
|
|
|
"query": "",
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
"node_types": ["problem"],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not browsed_problems.is_error
|
|
|
|
|
assert [
|
|
|
|
|
row["node"]["node_id"]
|
|
|
|
|
for row in json.loads(browsed_problems.content[0].text)["data"]
|
|
|
|
|
] == [problem_node.node_id]
|
|
|
|
|
facet_search = await mcp.call_tool(
|
|
|
|
|
"search",
|
|
|
|
|
{
|
|
|
|
|
"query": "400 visitors",
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
"tags": ["attendance"],
|
|
|
|
|
"audience": ["product"],
|
|
|
|
|
"use_for": ["comparison"],
|
|
|
|
|
"domain": ["production"],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not facet_search.is_error
|
|
|
|
|
assert (
|
|
|
|
|
json.loads(facet_search.content[0].text)["data"][0]["node"]["node_id"]
|
|
|
|
|
== NODE.node_id
|
|
|
|
|
)
|
|
|
|
|
listed_sources_tool = await mcp.call_tool(
|
|
|
|
|
"list_sources",
|
|
|
|
|
{
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not listed_sources_tool.is_error
|
|
|
|
|
assert (
|
|
|
|
|
json.loads(listed_sources_tool.content[0].text)["data"][0]["source"][
|
|
|
|
|
"revision_id"
|
|
|
|
|
]
|
|
|
|
|
== SOURCE_REVISION.revision_id
|
|
|
|
|
)
|
|
|
|
|
adjacent_tool = await mcp.call_tool(
|
|
|
|
|
"neighbors",
|
|
|
|
|
{
|
|
|
|
|
"node_id": NODE.node_id,
|
|
|
|
|
"projects": ["dome-performance"],
|
|
|
|
|
"include_shared": False,
|
|
|
|
|
"all_projects": False,
|
|
|
|
|
"edge_types": [],
|
|
|
|
|
"limit": 10,
|
|
|
|
|
"budget_tokens": 5000,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
assert not adjacent_tool.is_error
|
|
|
|
|
assert isinstance(adjacent_tool.content[0], TextContent)
|
|
|
|
|
assert (
|
|
|
|
|
json.loads(adjacent_tool.content[0].text)["data"][0]["neighbor"]["id"]
|
|
|
|
|
== related.node_id
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
asyncio.run(exercise())
|