feat: streamline agent memory surface
This commit is contained in:
@@ -23,6 +23,9 @@ def load_module():
|
||||
module.AUTH_PATH = pathlib.Path(module._test_state.name) / "auth.json"
|
||||
module.AUTH = {"account_id": "acct-1", "token": "token", "token_expires_at": "later"}
|
||||
module.DEFAULT_PROJECT_ID = "project-1"
|
||||
# Keep broad workflow coverage in this legacy suite. Dedicated profile
|
||||
# tests exercise the production core-only default.
|
||||
module.CONFIGURED_TOOL_PROFILES = {"all"}
|
||||
module.project_memory_encryption_active = lambda project_id: False
|
||||
return module
|
||||
|
||||
@@ -214,7 +217,7 @@ def test_node_get_accepts_node_id_without_path(mod):
|
||||
return found
|
||||
|
||||
mod.request_json = fake_request
|
||||
result = mod.node_get({"node_id": "node-123", "view": "render"})
|
||||
result = mod.node_get({"node_id": "node-123", "view": "content"})
|
||||
assert result["id"] == "node-123"
|
||||
assert result["full_path"] == "/documentation/product/prd"
|
||||
|
||||
@@ -243,7 +246,7 @@ def test_node_get_edit_view_uses_id_route_not_by_path(mod):
|
||||
|
||||
def test_node_get_rejects_path_reads(mod):
|
||||
try:
|
||||
mod.node_get({"path": "/documentation/product/prd", "view": "render"})
|
||||
mod.node_get({"path": "/documentation/product/prd", "view": "content"})
|
||||
except RuntimeError as exc:
|
||||
assert "path is an index/route only" in str(exc)
|
||||
else:
|
||||
@@ -252,7 +255,7 @@ def test_node_get_rejects_path_reads(mod):
|
||||
|
||||
def test_tools_include_node_by_path_descriptor(mod):
|
||||
tool_by_name = {item["name"]: item for item in mod.tools()}
|
||||
assert len(tool_by_name) == 56
|
||||
assert len(tool_by_name) == 51
|
||||
assert len(tool_by_name) == len(mod.tools())
|
||||
|
||||
get_tool = next(item for item in mod.tools() if item["name"] == "nap_node_get")
|
||||
@@ -274,10 +277,15 @@ def test_tools_include_node_by_path_descriptor(mod):
|
||||
"nap_patch",
|
||||
"nap_find",
|
||||
"nap_index_sync",
|
||||
"nap_recent",
|
||||
"nap_find_related",
|
||||
"nap_backlinks",
|
||||
"nap_ls_folders",
|
||||
"nap_ls_tags",
|
||||
]:
|
||||
assert hidden_name not in tool_by_name
|
||||
|
||||
for name in ["nap_discover", "nap_context", "nap_find_related", "nap_recent"]:
|
||||
for name in ["nap_discover", "nap_context"]:
|
||||
schema = next(item for item in mod.tools() if item["name"] == name)["inputSchema"]
|
||||
view = schema["properties"].get("view")
|
||||
if view:
|
||||
@@ -541,6 +549,21 @@ def test_context_and_related_use_identity_inputs(mod):
|
||||
|
||||
def fake_request(method, path, payload=None, **kwargs):
|
||||
assert method == "GET"
|
||||
if "/nodes/root-123/neighborhood?" in path:
|
||||
return {
|
||||
"nodes": [
|
||||
{"id": "root-123", "full_path": "/notes/root", "title": "root", "depth": 0},
|
||||
{"id": "linked-123", "full_path": "/notes/linked", "title": "linked", "depth": 1},
|
||||
{"id": "backlink-123", "full_path": "/notes/backlink", "title": "backlink", "depth": 1},
|
||||
],
|
||||
"edges": [
|
||||
{"source": "root-123", "target": "linked-123", "relation": "references"},
|
||||
{"source": "backlink-123", "target": "root-123", "relation": "references"},
|
||||
],
|
||||
"levels": [{"depth": 0, "node_ids": ["root-123"]}, {"depth": 1, "node_ids": ["linked-123", "backlink-123"]}],
|
||||
"frontier": [],
|
||||
"truncated": False,
|
||||
}
|
||||
if path == "/v1/projects/project-1/nodes/root-123/backlinks":
|
||||
return {"items": [backlink]}
|
||||
if path == "/v1/projects/project-1/nodes/linked-123/backlinks":
|
||||
@@ -550,19 +573,20 @@ def test_context_and_related_use_identity_inputs(mod):
|
||||
mod.get_node_by_id = fake_get_node_by_id
|
||||
mod.read_node_by_path = fake_read_by_path
|
||||
mod.request_json = fake_request
|
||||
mod.node_by_path = lambda args: {"identity": {"node_id": "root-123"}}
|
||||
mod.list_project_nodes = lambda args: {"items": [root, linked]}
|
||||
|
||||
context = mod.get_memory_context({"node_id": "root-123", "depth": 1, "view": "paths"})
|
||||
context = mod.get_memory_context({"node_id": "root-123", "depth": 1, "mode": "server"})
|
||||
assert [item["full_path"] for item in context["items"]] == ["/notes/root", "/notes/linked", "/notes/backlink"]
|
||||
assert {"source": "/notes/root", "target": "/notes/linked", "relation": "references"} in context["edges"]
|
||||
assert {"source": "/notes/backlink", "target": "/notes/root", "relation": "backlink"} in context["edges"]
|
||||
assert {"source": "root-123", "target": "linked-123", "relation": "references"} in context["edges"]
|
||||
assert {"source": "backlink-123", "target": "root-123", "relation": "references"} in context["edges"]
|
||||
|
||||
related = mod.find_related_nodes({"node_id": "root-123", "view": "paths"})
|
||||
assert related["target"]["full_path"] == "/notes/root"
|
||||
assert {item["full_path"] for item in related["items"]} >= {"/notes/backlink", "/notes/linked"}
|
||||
|
||||
path_context = mod.get_memory_context({"path": "/notes/root", "depth": 0, "view": "paths"})
|
||||
assert [item["full_path"] for item in path_context["items"]] == ["/notes/root"]
|
||||
path_context = mod.get_memory_context({"path": "/notes/root", "depth": 0, "mode": "server"})
|
||||
assert path_context["items"][0]["full_path"] == "/notes/root"
|
||||
with pytest.raises(RuntimeError, match="path is an index/route only"):
|
||||
mod.find_related_nodes({"path": "/notes/root"})
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Contract tests for the reduced, bounded agent-memory surface."""
|
||||
|
||||
import importlib.util
|
||||
import pathlib
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def load_module():
|
||||
script_path = pathlib.Path(__file__).resolve().parents[1] / "resources" / "scripts" / "napseer_mcp_server.py"
|
||||
sys.path.insert(0, str(script_path.parent))
|
||||
spec = importlib.util.spec_from_file_location("napseer_mcp_server_surface_test", script_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
module.DEFAULT_PROJECT_ID = "project-1"
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mod(monkeypatch):
|
||||
monkeypatch.delenv("NAPSEER_TOOL_PROFILES", raising=False)
|
||||
module = load_module()
|
||||
module.CONFIGURED_TOOL_PROFILES = None
|
||||
return module
|
||||
|
||||
|
||||
def test_default_surface_is_exactly_the_core_fourteen(mod):
|
||||
assert [tool["name"] for tool in mod.tools()] == [
|
||||
"nap_apropos", "nap_man", "nap_doctor", "nap_whoami",
|
||||
"nap_discover", "nap_context", "nap_node_by_path", "nap_node_get",
|
||||
"nap_create_node", "nap_node_patch", "nap_bulk", "nap_ln",
|
||||
"nap_mv", "nap_rm",
|
||||
]
|
||||
|
||||
|
||||
def test_profiles_expand_surface_without_restoring_pruned_tools(mod, monkeypatch):
|
||||
monkeypatch.setenv("NAPSEER_TOOL_PROFILES", "workflow,maintenance")
|
||||
names = {tool["name"] for tool in mod.tools()}
|
||||
assert {"nap_plan_list_active", "nap_kanban_complete", "nap_index_status"} <= names
|
||||
assert not ({"nap_recent", "nap_find_related", "nap_backlinks", "nap_ls_folders", "nap_ls_tags"} & names)
|
||||
|
||||
|
||||
def test_apropos_can_describe_inactive_profile_tools(mod):
|
||||
result = mod.explore_tools({"q": "kanban complete"})
|
||||
card = result["groups"]["kanban"][0]
|
||||
assert card["name"] == "nap_kanban_complete"
|
||||
assert card["enabled"] is False
|
||||
assert card["profile"] == "workflow"
|
||||
|
||||
|
||||
def test_pruned_and_profile_gated_calls_are_actionable(mod):
|
||||
with pytest.raises(mod.SafeToolError) as removed:
|
||||
mod.call_tool_impl("nap_recent", {})
|
||||
assert removed.value.code == "tool_removed"
|
||||
assert "nap_discover" in removed.value.safe_message
|
||||
|
||||
with pytest.raises(mod.SafeToolError) as gated:
|
||||
mod.call_tool_impl("nap_kanban_list", {})
|
||||
assert gated.value.code == "tool_profile_required"
|
||||
assert "workflow" in gated.value.safe_message
|
||||
|
||||
|
||||
def test_context_and_node_read_schemas_are_bounded(mod):
|
||||
by_name = {tool["name"]: tool for tool in mod.all_tools()}
|
||||
context = by_name["nap_context"]["inputSchema"]["properties"]
|
||||
assert context["depth"]["maximum"] == 3
|
||||
assert context["direction"]["enum"] == ["outgoing", "incoming", "both"]
|
||||
assert context["mode"]["enum"] == ["hybrid", "local", "server"]
|
||||
assert "relations" in context
|
||||
assert "max_edges" in context
|
||||
assert "max_per_level" in context
|
||||
|
||||
node_get = by_name["nap_node_get"]["inputSchema"]["properties"]
|
||||
assert node_get["view"]["enum"] == ["content", "outline", "edit"]
|
||||
assert node_get["view"]["default"] == "content"
|
||||
|
||||
|
||||
def test_tool_text_does_not_duplicate_large_structured_payload(mod):
|
||||
payload = {"ok": True, "items": [{"content_text": "x" * 20_000}]}
|
||||
result = mod.tool_result(payload)
|
||||
assert result["structuredContent"] == payload
|
||||
assert len(result["content"][0]["text"]) < 200
|
||||
assert "20000" not in result["content"][0]["text"]
|
||||
|
||||
|
||||
def test_local_context_returns_title_radius_without_node_bodies(mod):
|
||||
state = tempfile.TemporaryDirectory()
|
||||
mod._surface_state = state
|
||||
mod.AUTH_DIR = pathlib.Path(state.name)
|
||||
mod.INDEX_PATH = mod.AUTH_DIR / "index.sqlite"
|
||||
mod.INDEX_LOCK_PATH = mod.AUTH_DIR / "index.sqlite.lock"
|
||||
root = {
|
||||
"id": "root", "project_id": "project-1", "full_path": "/docs/root",
|
||||
"folder_path": "/docs", "name": "root", "type": "note",
|
||||
"metadata": {"title": "Root", "status": "active"}, "tags": [],
|
||||
"links": [{"path": "/docs/child", "relation": "explains"}],
|
||||
"content_text": "secret body", "updated_at": "2026-08-16T12:00:00Z",
|
||||
}
|
||||
child = {
|
||||
"id": "child", "project_id": "project-1", "full_path": "/docs/child",
|
||||
"folder_path": "/docs", "name": "child", "type": "note",
|
||||
"metadata": {"title": "Child", "status": "active"}, "tags": [],
|
||||
"links": [], "content_text": "another body", "updated_at": "2026-08-16T12:01:00Z",
|
||||
}
|
||||
mod.index_node(root)
|
||||
mod.index_node(child)
|
||||
|
||||
result = mod.get_memory_context({
|
||||
"node_id": "root", "mode": "local", "depth": 2,
|
||||
"direction": "both", "relations": ["explains"],
|
||||
})
|
||||
assert [(item["title"], item["depth"]) for item in result["nodes"]] == [("Root", 0), ("Child", 1)]
|
||||
assert result["edges"] == [{"source": "root", "target": "child", "relation": "explains"}]
|
||||
assert result["levels"] == [
|
||||
{"depth": 0, "node_ids": ["root"]},
|
||||
{"depth": 1, "node_ids": ["child"]},
|
||||
]
|
||||
assert "content_text" not in result["nodes"][0]
|
||||
assert result["source"] == "local_index"
|
||||
|
||||
|
||||
def test_discovery_sort_and_returned_item_facets(mod):
|
||||
mod.list_project_nodes = lambda args: {
|
||||
"items": [
|
||||
{"name": "Beta", "folder_path": "/b", "tags": ["x"], "type": "note", "status": "active", "updated_at": "2026-01-01T00:00:00Z"},
|
||||
{"name": "Alpha", "folder_path": "/a", "tags": ["x", "y"], "type": "plan", "status": "planned", "updated_at": "2026-02-01T00:00:00Z"},
|
||||
]
|
||||
}
|
||||
result = mod.discover_memory({"sort": "title_asc", "facets": ["folder", "tag", "status"]})
|
||||
assert [item["name"] for item in result["items"]] == ["Alpha", "Beta"]
|
||||
assert result["facets"]["scope"] == "returned_items"
|
||||
assert result["facets"]["counts"]["tag"][0] == {"value": "x", "count": 2}
|
||||
|
||||
|
||||
def test_node_get_content_and_outline_hide_mutation_envelopes(mod):
|
||||
node = {
|
||||
"id": "node-1", "project_id": "project-1", "full_path": "/docs/one",
|
||||
"name": "one", "type": "note", "metadata": {"title": "One", "status": "active"},
|
||||
"links": [], "content_text": "# First\nBody\n## Second\nMore",
|
||||
"encrypted_content_envelope": {"ciphertext": "not-for-read-view"},
|
||||
"updated_at": "2026-08-16T12:00:00Z",
|
||||
}
|
||||
mod.get_node_by_id = lambda args: node
|
||||
content = mod.node_get({"node_id": "node-1"})
|
||||
outline = mod.node_get({"node_id": "node-1", "view": "outline"})
|
||||
assert content["content_text"].startswith("# First")
|
||||
assert "encrypted_content_envelope" not in content
|
||||
assert [heading["title"] for heading in outline["headings"]] == ["First", "Second"]
|
||||
assert "content_text" not in outline
|
||||
Reference in New Issue
Block a user