import importlib.util import pathlib import sys import threading import time from concurrent.futures import ThreadPoolExecutor 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_search_latency_test", script_path ) module = importlib.util.module_from_spec(spec) assert spec and spec.loader spec.loader.exec_module(module) return module def test_natural_language_discovery_uses_one_backend_search_request(tmp_path): mod = load_module() calls = [] mod.INDEX_PATH = tmp_path / "missing-index.sqlite" mod.resolve_project_id = lambda _args: "project-1" mod.project_encryption_state = lambda _project_id: "standard" def request(method, path, payload=None, **_kwargs): calls.append((method, path, payload)) return { "items": [], "next_cursor": None, "query_analysis": {"mode": "terms"}, "search_strategy": "identity_fts", "search_coverage": "identity_only", "search_projection": "postgres_identity_v1", } mod.request_json = request result = mod.discover_memory( { "q": "SIARA signer facturacion certificate API XML signature", "limit": 20, "view": "summary", } ) assert result["ok"] is True assert len(calls) == 1 assert calls[0][0] == "GET" assert "/nodes?" in calls[0][1] assert result["search_strategy"] == "identity_fts" assert result["search_coverage"] == "identity_only" assert result["search_projection"] == "postgres_identity_v1" def test_sparse_index_search_latency_does_not_scale_with_query_variants(tmp_path): mod = load_module() calls = [] mod.INDEX_PATH = tmp_path / "missing-index.sqlite" mod.resolve_project_id = lambda _args: "project-1" mod.project_encryption_state = lambda _project_id: "standard" def request(_method, _path, _payload=None, **_kwargs): calls.append(time.monotonic()) time.sleep(0.03) return {"items": [], "next_cursor": None} mod.request_json = request started = time.monotonic() mod.search_memory( { "q": "one two three four five six seven eight", "limit": 20, "view": "summary", } ) elapsed = time.monotonic() - started assert len(calls) == 1 assert elapsed < 0.15 def test_complete_local_search_skips_remote_encryption_preflight_and_search(tmp_path): mod = load_module() mod.INDEX_PATH = tmp_path / "index.sqlite" mod.INDEX_PATH.touch() mod.resolve_project_id = lambda _args: "project-1" mod.project_encryption_state = lambda _project_id: "unknown" items = [ { "id": f"node-{index}", "project_id": "project-1", "full_path": f"/notes/result-{index}", "name": f"result-{index}", "type": "note", "tags": [], "updated_at": "2026-08-22T00:00:00Z", } for index in range(5) ] mod.local_index_diagnostics = lambda _project_id: { "exists": True, "graph_complete": True, } mod.search_local_index = lambda _args: { "items": items, "query": {"terms": ["result"]}, "search_strategies": [], "index": {"exists": True, "graph_complete": True, "used": True}, } def unexpected_request(*_args, **_kwargs): raise AssertionError("complete local search must not make an HTTP request") mod.request_json = unexpected_request result = mod.search_memory({"q": "result", "limit": 5, "view": "summary"}) assert result["sources"] == ["local"] assert result["search_strategy"] == "local_fts" assert result["search_coverage"] == "identity_and_local_body" assert result["search_projection"] == "sqlite_fts5_current" assert len(result["items"]) == 5 assert all(item["node_id"].startswith("node-") for item in result["items"]) def indexed_node(path, *, node_type="note", tags=None, status="active", content=""): name = path.rsplit("/", 1)[-1] return { "id": f"node-{path.strip('/').replace('/', '-')}", "project_id": "project-1", "full_path": path, "folder_path": path.rsplit("/", 1)[0] or None, "name": name, "title": name, "type": node_type, "tags": tags or [], "aliases": [], "links": [], "metadata": {"status": status}, "content_text": content, "encryption_state": "legacy_plaintext", "updated_at": "2026-08-22T00:00:00Z", } def configure_local_index(mod, tmp_path): mod.AUTH_DIR = tmp_path mod.INDEX_PATH = tmp_path / "index.sqlite" mod.INDEX_LOCK_PATH = tmp_path / "index.lock" mod.resolve_project_id = lambda _args: "project-1" def test_single_pass_ranking_prefers_exact_canonical_identity(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) mod.index_node( indexed_node( "/rules/memory-source-policy", node_type="rule", tags=["memory", "policy"], content="Canonical memory source rule.", ) ) for index in range(12): mod.index_node( indexed_node( f"/work/current/incidental-{index}", content="memory source policy " * (index + 2), ) ) result = mod.search_local_index({"q": "memory source policy", "limit": 5}) assert result["items"][0]["full_path"] == "/rules/memory-source-policy" assert "exact_identity" in result["items"][0]["match_reason"] assert "full_path" in result["items"][0]["matched_fields"] assert result["search_strategies"] == [ { "strategy": "single_pass_weighted_candidates", "matched": 13, "returned": 5, "query_count": 1, "candidate_limit": 30, } ] def test_complete_local_compact_browse_uses_keyset_cursor_without_http(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) for index in range(7): item = indexed_node( f"/plans/item-{index}", node_type="plan", tags=["performance"], status="active", ) item["updated_at"] = f"2026-08-2{index}T00:00:00Z" mod.index_node(item) mod.set_local_graph_index_complete("project-1", True) def unexpected_request(*_args, **_kwargs): raise AssertionError("complete local compact browse must not make an HTTP request") mod.request_json = unexpected_request first = mod.discover_memory( { "folder_path": "/plans", "tag": "performance", "limit": 3, "view": "paths", } ) second = mod.discover_memory( { "folder_path": "/plans", "tag": "performance", "limit": 3, "view": "paths", "cursor": first["next_cursor"], } ) assert first["source"] == "local_index" assert first["search_strategy"] == "local_snapshot_browse" assert first["search_projection"] == "sqlite_compact_identity_v1" assert first["freshness"]["graph_complete"] is True assert first["next_cursor"].startswith(mod.LOCAL_BROWSE_CURSOR_PREFIX) assert [item["updated_at"] for item in first["items"]] == sorted( (item["updated_at"] for item in first["items"]), reverse=True ) assert not ({item["node_id"] for item in first["items"]} & {item["node_id"] for item in second["items"]}) assert all(item["node_id"] for item in first["items"] + second["items"]) def test_local_compact_browse_cursor_is_bound_to_filters(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) for index in range(3): mod.index_node(indexed_node(f"/plans/item-{index}", node_type="plan")) mod.set_local_graph_index_complete("project-1", True) first = mod.discover_memory({"folder_path": "/plans", "limit": 1, "view": "summary"}) with pytest.raises(RuntimeError, match="cursor filters do not match"): mod.discover_memory( { "folder_path": "/decisions", "limit": 1, "view": "summary", "cursor": first["next_cursor"], } ) def test_local_compact_browse_title_order_is_global_across_pages(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) for title in ["Zulu", "alpha", "Mike", "bravo"]: item = indexed_node(f"/notes/{title}") item["title"] = title mod.index_node(item) mod.set_local_graph_index_complete("project-1", True) first = mod.discover_memory({"limit": 2, "view": "titles", "sort": "title_asc"}) second = mod.discover_memory( {"limit": 2, "view": "titles", "sort": "title_asc", "cursor": first["next_cursor"]} ) assert [item["title"] for item in first["items"] + second["items"]] == [ "alpha", "bravo", "Mike", "Zulu", ] def test_compact_browse_falls_back_once_when_local_contract_is_incomplete(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) mod.index_node(indexed_node("/plans/local", node_type="plan")) calls = [] def request(method, path, payload=None, **_kwargs): calls.append((method, path, payload)) return {"items": [], "next_cursor": None} mod.request_json = request result = mod.discover_memory({"folder_path": "/plans", "limit": 5, "view": "paths"}) assert result["ok"] is True assert len(calls) == 1 assert calls[0][0] == "GET" assert "/nodes?" in calls[0][1] with pytest.raises(RuntimeError, match="complete active-node index"): mod.discover_memory( {"folder_path": "/plans", "limit": 5, "view": "paths", "mode": "local"} ) def test_local_search_reads_overlap_without_writer_file_lock(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) for index in range(4): mod.index_node(indexed_node(f"/notes/result-{index}", content="shared needle")) mod.set_local_graph_index_complete("project-1", True) def unexpected_writer_lock(*_args, **_kwargs): raise AssertionError("read-only local search must not acquire the writer file lock") mod.index_file_lock = unexpected_writer_lock original_rank = mod.rank_search_items rendezvous = threading.Barrier(2) def overlapping_rank(items, analysis, limit=None): rendezvous.wait(timeout=1) return original_rank(items, analysis, limit=limit) mod.rank_search_items = overlapping_rank with ThreadPoolExecutor(max_workers=2) as executor: futures = [ executor.submit(mod.search_local_index, {"q": "shared needle", "limit": 2}) for _ in range(2) ] results = [future.result(timeout=2) for future in futures] assert all(len(result["items"]) == 2 for result in results) def test_index_schema_initializes_once_and_revalidates_replacement(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) original_initialize = mod.initialize_index_schema calls = [] def counted_initialize(conn): calls.append(str(mod.INDEX_PATH)) return original_initialize(conn) mod.initialize_index_schema = counted_initialize with mod.index_connect(): pass with mod.index_connect(): pass assert len(calls) == 1 replacement = tmp_path / "replacement.sqlite" replacement.touch() replacement.replace(mod.INDEX_PATH) with mod.index_connect(): pass assert len(calls) == 2 def test_discover_contract_advertises_local_routing_and_pagination(): mod = load_module() tool = next(item for item in mod.tools() if item["name"] == "nap_discover") properties = tool["inputSchema"]["properties"] assert "cursor" in properties assert properties["mode"]["enum"] == ["hybrid", "server", "local"] def test_query_planner_normalizes_plurals_and_reports_intent(): mod = load_module() analysis = mod.analyze_search_query( "where did we decide how search queries and policies should work" ) assert "query" in analysis["terms"] assert "querie" not in analysis["terms"] assert analysis["inferred_intent"] == "decision" assert len(mod.fts_query_variants(analysis["raw"])[1]) == 1 def test_local_search_pushes_structured_filters_into_candidate_query(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) nodes = [ indexed_node( "/decisions/search-governance", node_type="decision", tags=["search"], content="search governance", ), indexed_node( "/decisions/old-search-governance", node_type="decision", tags=["search"], status="superseded", content="search governance", ), indexed_node( "/plans/search-governance", node_type="plan", tags=["search"], content="search governance", ), indexed_node( "/decisions/other-search", node_type="decision", tags=["other"], content="search governance", ), ] for node in nodes: mod.index_node(node) result = mod.search_local_index( { "q": "search governance", "folder_path": "/decisions", "tag": "search", "type": "decision", "status": "active", "limit": 10, } ) assert [item["full_path"] for item in result["items"]] == [ "/decisions/search-governance" ] assert result["index"]["query_count"] == 1 assert set(result["index"]["filters_applied"]) >= { "folder_path", "tag", "type", "status", } def test_explicit_intent_boosts_without_filtering(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) mod.index_node( indexed_node( "/decisions/gateway-relay-authentication", node_type="decision", content="gateway relay authentication", ) ) mod.index_node( indexed_node( "/work/current/gateway-relay-authentication", content="gateway relay authentication", ) ) result = mod.search_local_index( {"q": "gateway relay authentication", "intent": "decision", "limit": 10} ) assert result["items"][0]["full_path"].startswith("/decisions/") assert len(result["items"]) == 2 def test_complete_filtered_local_search_stays_off_network(tmp_path): mod = load_module() configure_local_index(mod, tmp_path) mod.index_node( indexed_node( "/documentation/facturacion-electronica", tags=["billing"], content="facturación electrónica", ) ) mod.index_node( indexed_node( "/work/current/facturacion-migration", tags=["migration"], content="facturación electrónica", ) ) mod.set_local_graph_index_complete("project-1", True) def unexpected_request(*_args, **_kwargs): raise AssertionError("a complete filtered local result must not use the network") mod.request_json = unexpected_request result = mod.search_memory( { "q": "facturación", "tag": "billing", "limit": 1, "view": "summary", } ) assert result["sources"] == ["local"] assert [item["full_path"] for item in result["items"]] == [ "/documentation/facturacion-electronica" ] def test_discovery_schema_documents_dynamic_sort_and_intent(): mod = load_module() tool = next(item for item in mod.tools() if item["name"] == "nap_discover") properties = tool["inputSchema"]["properties"] assert properties["intent"]["enum"] == [ "any", "decision", "rule", "plan", "task", "implementation", "documentation", ] assert "default" not in properties["sort"] assert "relevance" in properties["sort"]["description"]