feat: streamline agent memory surface

This commit is contained in:
HumbleTurtle
2026-08-16 04:29:39 -03:00
parent 5574faace0
commit 9429b7039f
3 changed files with 820 additions and 107 deletions
+33 -9
View File
@@ -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"})
+153
View File
@@ -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