fix: harden MCP and auth runtime lifecycles

This commit is contained in:
HumbleTurtle
2026-08-19 23:35:13 -03:00
parent 829d54b080
commit 7ca30e690d
14 changed files with 2050 additions and 285 deletions
+59
View File
@@ -69,6 +69,7 @@ def test_failed_anonymous_refresh_uses_ssh_recovery():
mod.DEFAULT_PROJECT_ID = "project"
mod.REFRESH_TOKEN = "stale-refresh"
mod.REFRESH_EXPIRES_AT = None
mod.refresh_public_auth_state = lambda: mod.AUTH
mod.auth_public_key = lambda: "ssh-ed25519 test-public-key"
mod.sign_with_auth_key = lambda *args: "test-signature"
saved = []
@@ -106,7 +107,65 @@ def test_failed_anonymous_refresh_uses_ssh_recovery():
assert saved[0]["refresh_token"] == "recovered-refresh"
def test_failed_mixed_state_refresh_uses_ssh_recovery():
mod = load_module()
mod.AUTH = {
**anonymous_auth(mod),
"account_mode": "operator_project",
"oauth_client_id": "nap-cli",
"project_id": "project",
"ssh_private_key": "test-private-key",
"ssh_public_key": "ssh-ed25519 test-public-key",
}
mod.BASE_URL = mod.AUTH["base_url"]
mod.DEFAULT_PROJECT_ID = "project"
mod.REFRESH_TOKEN = "stale-refresh"
mod.REFRESH_EXPIRES_AT = None
mod.refresh_public_auth_state = lambda: mod.AUTH
mod.auth_public_key = lambda: "ssh-ed25519 test-public-key"
mod.sign_with_auth_key = lambda *args: "test-signature"
saved = []
mod.save_auth = lambda payload: saved.append(dict(payload))
def request_form(method, path, payload=None, token_required=False):
assert (method, path, token_required) == ("POST", "/v1/oauth/token", False)
raise RuntimeError("HTTP 401")
def request(method, path, payload=None, token_required=False):
if path == "/v1/enrollment/refresh":
raise RuntimeError("HTTP 401")
if path == "/v1/enrollment/challenges":
return {"challenge_id": "challenge", "challenge_text": "sign-me"}
if path == "/v1/enrollment/verify":
return {
"token": {
"access_token": "recovered-access",
"refresh_token": "recovered-refresh",
"expires_at": "2099-01-01T00:00:00Z",
"refresh_expires_at": "2099-12-31T00:00:00Z",
},
"worker": {
"id": "worker",
"agent_id": "agent-id",
"name": "agent",
"device_fingerprint": "device",
"root_path": "/repo",
"project_id": "project",
},
}
raise AssertionError(path)
mod.request_form_json = request_form
mod.request_json = request
result = mod.renew_auth()
assert result["method"] == "anonymous_ssh_recovery"
assert result["project_id"] == "project"
assert saved[0]["refresh_token"] == "recovered-refresh"
if __name__ == "__main__":
test_initial_enrollment_persists_refresh_credential()
test_failed_anonymous_refresh_uses_ssh_recovery()
test_failed_mixed_state_refresh_uses_ssh_recovery()
print("ok: anonymous auth recovery tests passed")
+173 -1
View File
@@ -2,6 +2,8 @@ import importlib.util
import hashlib
import json
import pathlib
import time
import pytest
SCRIPT_PATH = (
@@ -114,6 +116,18 @@ def test_update_does_not_overwrite_repository_source(monkeypatch, capsys):
assert json.loads(capsys.readouterr().out)["status"] == "updated"
def test_update_and_expanded_auth_tail_fail_before_side_effects(monkeypatch):
module = load_installer()
monkeypatch.setattr(module, "install_assets", fail_if_called)
monkeypatch.setattr(module, "run_wrapper", fail_if_called)
with pytest.raises(RuntimeError, match="unexpected argument 'extra' for `nap update`"):
module.main(["nap", "update", "extra"])
with pytest.raises(RuntimeError, match="Bash expands `!nap`"):
module.main(["nap", "auth", "login", "project", "attach", "update"])
def test_doctor_reports_presence_without_exposing_values(tmp_path, monkeypatch):
module = load_installer()
state_dir = tmp_path / ".napseer"
@@ -136,7 +150,18 @@ def test_doctor_reports_presence_without_exposing_values(tmp_path, monkeypatch):
(install_dir / "napseer_mcp_supervisor.py").write_text("# supervisor\n", encoding="utf-8")
monkeypatch.setattr(module, "INSTALL_DIR", install_dir)
monkeypatch.setattr(module, "runtime_assets_missing", lambda: [])
monkeypatch.setattr(module, "mcp_runtime_probe", lambda: {"status": "ok", "transport": True, "read": True, "tool_count": 56})
monkeypatch.setattr(
module,
"mcp_runtime_probe",
lambda: {
"status": "local_ready",
"transport": True,
"contract": True,
"local_state": True,
"read": True,
"tool_count": 56,
},
)
monkeypatch.setattr(
module,
"state_dir_status",
@@ -174,6 +199,153 @@ def test_doctor_does_not_create_state_directory(tmp_path, monkeypatch):
assert not state_dir.exists()
def test_doctor_treats_missing_project_as_setup_not_runtime_repair(tmp_path, monkeypatch):
module = load_installer()
install_dir = tmp_path / "install"
install_dir.mkdir()
(install_dir / "napseer_mcp_server.py").write_text("# worker\n", encoding="utf-8")
(install_dir / "napseer_mcp_supervisor.py").write_text("# supervisor\n", encoding="utf-8")
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(module, "INSTALL_DIR", install_dir)
monkeypatch.setattr(module, "runtime_assets_missing", lambda: [])
monkeypatch.setattr(
module,
"mcp_runtime_probe",
lambda: {
"status": "local_ready",
"transport": True,
"contract": True,
"local_state": True,
"authentication": {"configured": False, "verified": False},
"project": {"configured": False, "verified": False},
},
)
result = module.doctor_status()
assert result["status"] == "setup_available"
assert result["issues"] == []
assert result["setup"][0]["code"] == "project_not_initialized"
assert result["lifecycle"]["transport"] == "ready"
assert not (tmp_path / ".napseer").exists()
def test_mcp_status_separates_runtime_health_from_project_readiness(tmp_path, monkeypatch):
module = load_installer()
install_dir = tmp_path / "install"
install_dir.mkdir()
(install_dir / "napseer_mcp_server.py").write_text("# worker\n", encoding="utf-8")
(install_dir / "napseer_mcp_supervisor.py").write_text("# supervisor\n", encoding="utf-8")
monkeypatch.setattr(module, "INSTALL_DIR", install_dir)
monkeypatch.setattr(module, "runtime_assets_missing", lambda: [])
monkeypatch.setattr(
module,
"mcp_runtime_probe",
lambda: {
"status": "local_ready",
"transport": True,
"contract": True,
"local_state": True,
"tool_count": 56,
"authentication": {"configured": False, "verified": False},
"project": {"configured": False, "verified": False},
},
)
result = module.mcp_status()
assert result["status"] == "probe_ok"
assert result["lifecycle"]["runtime"] == "installed"
assert result["lifecycle"]["transport"] == "ready"
assert result["lifecycle"]["authentication"] == "unconfigured"
assert result["lifecycle"]["project"] == "unconfigured"
assert "not tested" in result["message"]
def write_probe_server(path, trace_path, delay=0):
path.write_text(
f"""\
import json
import pathlib
import sys
import time
TRACE = pathlib.Path({str(trace_path)!r})
DELAY = {delay!r}
for line in sys.stdin:
message = json.loads(line)
params = message.get("params") or {{}}
record = {{"method": message.get("method")}}
if message.get("method") == "initialize":
record["protocol_version"] = params.get("protocolVersion")
if message.get("method") == "tools/call":
record["tool"] = params.get("name")
with TRACE.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(record) + "\\n")
if "id" not in message:
continue
time.sleep(DELAY)
if message.get("method") == "initialize":
result = {{"protocolVersion": "2025-11-25", "capabilities": {{}}, "serverInfo": {{"name": "fake", "version": "1"}}}}
elif message.get("method") == "tools/list":
result = {{"tools": [{{"name": "nap_whoami"}}]}}
else:
payload = {{"project_configured": False, "has_token": False}}
result = {{"structuredContent": payload, "content": [{{"type": "text", "text": json.dumps(payload)}}], "isError": False}}
print(json.dumps({{"jsonrpc": "2.0", "id": message["id"], "result": result}}), flush=True)
""",
encoding="utf-8",
)
def test_mcp_runtime_probe_uses_local_status_without_project_bootstrap(tmp_path, monkeypatch):
module = load_installer()
supervisor = tmp_path / "probe.py"
worker = tmp_path / "worker.py"
trace = tmp_path / "trace.jsonl"
write_probe_server(supervisor, trace)
worker.write_text("# exists\n", encoding="utf-8")
paths = {
"napseer_mcp_supervisor.py": supervisor,
"napseer_mcp_server.py": worker,
}
monkeypatch.setattr(module, "runtime_asset_path", lambda name: paths[name])
result = module.mcp_runtime_probe(timeout_seconds=2)
records = [json.loads(line) for line in trace.read_text(encoding="utf-8").splitlines()]
assert result["status"] == "local_ready"
assert result["transport"] is True
assert result["local_state"] is True
assert records[0]["protocol_version"] == "2025-11-25"
assert [record.get("tool") for record in records if record.get("tool")] == ["nap_whoami"]
assert any(record["method"] == "notifications/initialized" for record in records)
def test_mcp_runtime_probe_timeout_is_one_total_deadline(tmp_path, monkeypatch):
module = load_installer()
supervisor = tmp_path / "slow-probe.py"
worker = tmp_path / "worker.py"
trace = tmp_path / "trace.jsonl"
write_probe_server(supervisor, trace, delay=0.15)
worker.write_text("# exists\n", encoding="utf-8")
paths = {
"napseer_mcp_supervisor.py": supervisor,
"napseer_mcp_server.py": worker,
}
monkeypatch.setattr(module, "runtime_asset_path", lambda name: paths[name])
started = time.monotonic()
result = module.mcp_runtime_probe(timeout_seconds=0.2)
assert result["status"] == "failed"
assert result["error_class"] == "TimeoutError"
assert result["transport"] is True
assert result["contract"] is False
assert time.monotonic() - started < 0.35
def test_wrapper_dispatch_never_installs_missing_runtime(monkeypatch):
module = load_installer()
monkeypatch.setattr(module, "runtime_assets_missing", lambda: ["napseer_mcp_server.py"])
+13
View File
@@ -36,6 +36,18 @@ def expect_unknown(mod, argv, expected_fragment):
raise AssertionError(f"expected RuntimeError for {argv}")
def test_auth_login_rejects_history_expansion_tail_before_browser():
mod = load_module()
mod.operator_account_login = lambda _args: (_ for _ in ()).throw(
AssertionError("malformed auth login must fail before browser login")
)
expect_unknown(
mod,
["napseer_mcp_server.py", "auth", "login", "project", "attach", "update"],
"Bash expands `!nap`",
)
def test_chat_secret_aliases_rejected():
mod = load_module()
for args in [
@@ -214,6 +226,7 @@ def test_project_encryption_status_aliases_are_read_only_compatibility():
if __name__ == "__main__":
test_auth_login_rejects_history_expansion_tail_before_browser()
test_chat_secret_aliases_rejected()
test_lineage_aliases_rejected()
test_plan_aliases_rejected()
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""Regression tests for bounded DNS address fallback in the local wrapper."""
import importlib.util
import pathlib
import socket
import sys
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_address_fallback_test", script_path
)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
class FakeSocket:
def __init__(self, family, attempts, failing_addresses):
self.family = family
self.attempts = attempts
self.failing_addresses = failing_addresses
self.timeouts = []
self.closed = False
def settimeout(self, value):
self.timeouts.append(value)
def bind(self, _address):
return None
def connect(self, address):
self.attempts.append((self.family, address[0]))
if address[0] in self.failing_addresses:
raise TimeoutError("unreachable test address")
def close(self):
self.closed = True
def test_ipv4_is_preferred_and_failed_addresses_are_bounded():
mod = load_module()
attempts = []
sockets = []
def resolver(_host, port, _family, _socktype):
return [
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::1", port, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", port)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", port)),
]
def factory(family, _socktype, _proto):
created = FakeSocket(family, attempts, {"192.0.2.1"})
sockets.append(created)
return created
connected = mod.connect_with_address_fallback(
("api.example.test", 443),
timeout=30,
resolver=resolver,
socket_factory=factory,
)
assert connected is sockets[1]
assert attempts == [
(socket.AF_INET, "192.0.2.1"),
(socket.AF_INET, "192.0.2.2"),
]
assert sockets[0].timeouts == [mod.API_CONNECT_ATTEMPT_TIMEOUT_SECONDS]
assert sockets[0].closed
assert sockets[1].timeouts == [mod.API_CONNECT_ATTEMPT_TIMEOUT_SECONDS, 30.0]
def test_ipv4_only_never_attempts_ipv6():
mod = load_module()
attempts = []
resolver_families = []
def resolver(_host, port, family, _socktype):
resolver_families.append(family)
return [
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::1", port, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", port)),
]
def factory(family, _socktype, _proto):
return FakeSocket(family, attempts, {"192.0.2.1"})
try:
mod.connect_with_address_fallback(
("api.example.test", 443),
timeout=30,
resolver=resolver,
socket_factory=factory,
ipv4_only=True,
)
except TimeoutError:
pass
else:
raise AssertionError("IPv4-only connection unexpectedly succeeded")
assert resolver_families == [socket.AF_INET]
assert attempts == [(socket.AF_INET, "192.0.2.1")]
if __name__ == "__main__":
test_ipv4_is_preferred_and_failed_addresses_are_bounded()
test_ipv4_only_never_attempts_ipv6()
print("ok: connection address fallback tests passed")
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Regression tests for bounded DNS address fallback in the updater transport."""
import importlib.util
import pathlib
import socket
def load_module():
script_path = (
pathlib.Path(__file__).resolve().parents[1]
/ "resources"
/ "scripts"
/ "nap_install.py"
)
spec = importlib.util.spec_from_file_location(
"nap_install_address_fallback_test", script_path
)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
class FakeSocket:
def __init__(self, family, attempts, failing_addresses):
self.family = family
self.attempts = attempts
self.failing_addresses = failing_addresses
self.timeouts = []
self.closed = False
def settimeout(self, value):
self.timeouts.append(value)
def bind(self, _address):
return None
def connect(self, address):
self.attempts.append((self.family, address[0]))
if address[0] in self.failing_addresses:
raise TimeoutError("unreachable test address")
def close(self):
self.closed = True
def test_updater_prefers_ipv4_and_bounds_failed_addresses():
mod = load_module()
attempts = []
sockets = []
def resolver(_host, port, _family, _socktype):
return [
(socket.AF_INET6, socket.SOCK_STREAM, 6, "", ("2001:db8::1", port, 0, 0)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.1", port)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("192.0.2.2", port)),
]
def factory(family, _socktype, _proto):
created = FakeSocket(family, attempts, {"192.0.2.1"})
sockets.append(created)
return created
connected = mod.connect_with_address_fallback(
("api.example.test", 443),
timeout=30,
resolver=resolver,
socket_factory=factory,
)
assert connected is sockets[1]
assert attempts == [
(socket.AF_INET, "192.0.2.1"),
(socket.AF_INET, "192.0.2.2"),
]
assert sockets[0].timeouts == [mod.API_CONNECT_ATTEMPT_TIMEOUT_SECONDS]
assert sockets[0].closed
assert sockets[1].timeouts == [mod.API_CONNECT_ATTEMPT_TIMEOUT_SECONDS, 30.0]
if __name__ == "__main__":
test_updater_prefers_ipv4_and_bounds_failed_addresses()
print("ok: updater connection address fallback tests passed")
+34 -3
View File
@@ -55,13 +55,44 @@ def run():
current = {}
mod.DEFAULT_PROJECT_ID = "stale-project"
bootstrap_calls = []
def bootstrap_project(args):
bootstrap_calls.append(dict(args))
mod.DEFAULT_PROJECT_ID = "anonymous-project"
return {"status": "created", "project_id": "anonymous-project"}
mod.bootstrap_project = bootstrap_project
assert mod.resolve_project_id({}) == "anonymous-project"
assert bootstrap_calls == [
{
"slug": mod.default_project_slug(),
"name": mod.default_project_name(mod.default_project_slug()),
"description": "Created automatically by the local Napseer MCP runtime.",
"encryption": "standard",
}
]
current = {}
mod.DEFAULT_PROJECT_ID = None
def limited_project(_args):
raise mod.SafeToolError(
"anonymous_space_limit_reached",
"Anonymous space limit reached. Run `nap project claim` to authenticate and continue.",
status=403,
service_code="anonymous_space_limit_reached",
)
mod.bootstrap_project = limited_project
try:
mod.resolve_project_id({})
except mod.SafeToolError as exc:
assert exc.code == "project_not_configured"
assert "nap project attach" in exc.safe_message
assert exc.code == "anonymous_space_limit_reached"
assert exc.service_code == "anonymous_space_limit_reached"
assert "nap project claim" in exc.safe_message
else:
raise AssertionError("missing project selection must return an actionable safe error")
raise AssertionError("anonymous space limit must remain actionable")
def open_success(request, timeout=None):
requests.append(request)
+76
View File
@@ -0,0 +1,76 @@
import importlib.util
import pathlib
import sys
import time
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"}}
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]
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
+182 -11
View File
@@ -10,6 +10,7 @@ import time
ROOT = pathlib.Path(__file__).resolve().parents[1]
SUPERVISOR = ROOT / "resources" / "scripts" / "napseer_mcp_supervisor.py"
REAL_WORKER = ROOT / "resources" / "scripts" / "napseer_mcp_server.py"
def write_worker(path: pathlib.Path, version: int) -> None:
@@ -18,31 +19,64 @@ def write_worker(path: pathlib.Path, version: int) -> None:
import json
import os
import sys
import threading
import time
VERSION = {version}
WRITE_LOCK = threading.Lock()
CANCELLED = set()
for line in sys.stdin:
message = json.loads(line)
if message.get("method") == "notifications/initialized":
continue
if message.get("method") == "tools/call":
def emit(payload):
with WRITE_LOCK:
print(json.dumps(payload), flush=True)
def handle(message):
method = message.get("method")
if method == "notifications/initialized":
return
if method == "notifications/cancelled":
CANCELLED.add((message.get("params") or {{}}).get("requestId"))
return
if method == "notifications/noisy":
emit({{"jsonrpc": "2.0", "id": None, "error": {{"code": -32601, "message": "noise"}}}})
return
if "id" not in message:
return
if method == "tools/call":
name = (message.get("params") or {{}}).get("name")
if name == "slow":
time.sleep(1)
if name == "crash":
time.sleep(0.1)
os._exit(17)
result = {{"pid": os.getpid(), "version": VERSION, "name": name}}
else:
result = {{"version": VERSION}}
print(json.dumps({{"jsonrpc": "2.0", "id": message.get("id"), "result": result}}), flush=True)
if message.get("id") in CANCELLED:
return
emit({{"jsonrpc": "2.0", "id": message.get("id"), "result": result}})
for line in sys.stdin:
message = json.loads(line)
if "id" not in message:
handle(message)
continue
threading.Thread(target=handle, args=(message,), daemon=True).start()
""",
encoding="utf-8",
)
class SupervisorClient:
def __init__(self, worker_path: pathlib.Path):
def __init__(self, worker_path: pathlib.Path, project_root: pathlib.Path | None = None):
environment = os.environ.copy()
environment["NAPSEER_MCP_WORKER_PATH"] = str(worker_path)
environment["NAPSEER_MCP_RESPONSE_TIMEOUT_SECONDS"] = "5"
environment["NAPSEER_TELEMETRY"] = "0"
if project_root is not None:
environment["NAPSEER_PROJECT_ROOT"] = str(project_root)
self.process = subprocess.Popen(
[sys.executable, str(SUPERVISOR)],
stdin=subprocess.PIPE,
@@ -52,21 +86,40 @@ class SupervisorClient:
)
def request(self, request_id: int, method: str, params=None):
self.send(request_id, method, params)
return self.receive()
def send(self, request_id: int, method: str, params=None):
assert self.process.stdin is not None
assert self.process.stdout is not None
message = {"jsonrpc": "2.0", "id": request_id, "method": method}
if params is not None:
message["params"] = params
self.process.stdin.write(json.dumps(message).encode("utf-8") + b"\n")
self.process.stdin.flush()
readable, _, _ = select.select([self.process.stdout], [], [], 8)
def notify(self, method: str, params=None):
assert self.process.stdin is not None
message = {"jsonrpc": "2.0", "method": method}
if params is not None:
message["params"] = params
self.process.stdin.write(json.dumps(message).encode("utf-8") + b"\n")
self.process.stdin.flush()
def receive(self, timeout=8):
assert self.process.stdout is not None
readable, _, _ = select.select([self.process.stdout], [], [], timeout)
assert readable, "supervisor did not return a response"
return json.loads(self.process.stdout.readline())
def close(self):
if self.process.poll() is None:
self.process.terminate()
self.process.wait(timeout=5)
assert self.process.stdin is not None
self.process.stdin.close()
try:
self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.terminate()
self.process.wait(timeout=5)
def test_restarts_worker_killed_between_requests(tmp_path):
@@ -86,6 +139,124 @@ def test_restarts_worker_killed_between_requests(tmp_path):
client.close()
def test_concurrent_requests_are_correlated_without_head_of_line_blocking(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
client.send(1, "tools/call", {"name": "slow", "arguments": {}})
time.sleep(0.05)
client.send(2, "tools/call", {"name": "fast", "arguments": {}})
first = client.receive(timeout=0.75)
second = client.receive(timeout=2)
assert first["id"] == 2
assert first["result"]["name"] == "fast"
assert second["id"] == 1
assert second["result"]["name"] == "slow"
finally:
client.close()
def test_notification_response_cannot_poison_the_next_request(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
client.notify("notifications/noisy")
time.sleep(0.05)
response = client.request(7, "tools/list")
assert response["id"] == 7
assert response["result"]["version"] == 1
finally:
client.close()
def test_cancellation_drops_the_cancelled_and_late_worker_response(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
client.send(1, "tools/call", {"name": "slow", "arguments": {}})
time.sleep(0.05)
client.notify(
"notifications/cancelled",
{"requestId": 1, "reason": "test cancellation"},
)
client.send(2, "tools/call", {"name": "fast", "arguments": {}})
response = client.receive(timeout=0.75)
assert response["id"] == 2
time.sleep(1.05)
follow_up = client.request(3, "tools/list")
assert follow_up["id"] == 3
finally:
client.close()
def test_worker_crash_fails_every_in_flight_request_once_then_recovers(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
client.send(1, "tools/call", {"name": "slow", "arguments": {}})
client.send(2, "tools/call", {"name": "crash", "arguments": {}})
failed = [client.receive(timeout=2), client.receive(timeout=2)]
assert {item["id"] for item in failed} == {1, 2}
assert all(item["error"]["code"] == -32098 for item in failed)
recovered = client.request(3, "tools/list")
assert recovered["id"] == 3
assert recovered["result"]["version"] == 1
assert client.process.poll() is None
finally:
client.close()
def test_real_supervisor_worker_protocol_lifecycle(tmp_path):
client = SupervisorClient(REAL_WORKER, project_root=tmp_path)
try:
initialized = client.request(
1,
"initialize",
{
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": {"name": "protocol-test", "version": "1"},
},
)
client.notify("notifications/initialized", {})
client.send(2, "tools/list")
client.send(3, "tools/call", {"name": "nap_whoami", "arguments": {}})
responses = {item["id"]: item for item in (client.receive(), client.receive())}
assert initialized["result"]["protocolVersion"] == "2025-11-25"
tool_names = {tool["name"] for tool in responses[2]["result"]["tools"]}
assert "nap_whoami" in tool_names
assert responses[3]["result"]["isError"] is False
client.notify("notifications/unknown", {"proof": "one-way"})
follow_up = client.request(
4,
"tools/call",
{"name": "nap_whoami", "arguments": {}},
)
assert follow_up["id"] == 4
assert follow_up["result"]["isError"] is False
finally:
client.close()
assert client.process.returncode == 0
assert not (tmp_path / ".napseer").exists()
def test_reloads_replaced_worker_between_requests(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
+244
View File
@@ -0,0 +1,244 @@
import importlib.util
import pathlib
import sys
import threading
import time
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_lifecycle_test", script_path
)
module = importlib.util.module_from_spec(spec)
assert spec and spec.loader
spec.loader.exec_module(module)
return module
def wait_until(predicate, timeout=2):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return
time.sleep(0.01)
raise AssertionError("condition was not reached before timeout")
def test_worker_runtime_completes_fast_read_before_slow_read():
mod = load_module()
emitted = []
emitted_lock = threading.Lock()
def handler(message):
if message["id"] == 1:
time.sleep(0.4)
return mod.rpc_result(message["id"], {"done": True})
def emit(response):
with emitted_lock:
emitted.append(response)
runtime = mod.McpRequestRuntime(handler=handler, sender=emit, max_workers=4)
try:
runtime.dispatch({"jsonrpc": "2.0", "id": 1, "method": "tools/call"})
runtime.dispatch({"jsonrpc": "2.0", "id": 2, "method": "tools/call"})
wait_until(lambda: len(emitted) == 2)
assert [response["id"] for response in emitted] == [2, 1]
finally:
runtime.close()
def test_worker_runtime_cancellation_suppresses_unused_result():
mod = load_module()
emitted = []
def handler(message):
if message["id"] == 1:
time.sleep(0.3)
return mod.rpc_result(message["id"], {"done": True})
runtime = mod.McpRequestRuntime(handler=handler, sender=emitted.append, max_workers=2)
try:
runtime.dispatch({"jsonrpc": "2.0", "id": 1, "method": "tools/call"})
runtime.dispatch(
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {"requestId": 1, "reason": "unused"},
}
)
runtime.dispatch({"jsonrpc": "2.0", "id": 2, "method": "tools/call"})
wait_until(lambda: any(response["id"] == 2 for response in emitted))
time.sleep(0.35)
assert [response["id"] for response in emitted] == [2]
finally:
runtime.close()
def test_initialize_cannot_be_cancelled():
mod = load_module()
emitted = []
def handler(message):
time.sleep(0.1)
return mod.rpc_result(message["id"], {"initialized": True})
runtime = mod.McpRequestRuntime(handler=handler, sender=emitted.append, max_workers=2)
try:
runtime.dispatch({"jsonrpc": "2.0", "id": 1, "method": "initialize"})
runtime.dispatch(
{
"jsonrpc": "2.0",
"method": "notifications/cancelled",
"params": {"requestId": 1},
}
)
wait_until(lambda: len(emitted) == 1)
assert emitted[0]["id"] == 1
finally:
runtime.close()
def test_unknown_notification_never_returns_a_response():
mod = load_module()
response = mod.handle(
{"jsonrpc": "2.0", "method": "notifications/unknown", "params": {}}
)
assert response is None
def test_mutating_tools_are_serialized_while_reads_can_overlap(monkeypatch):
mod = load_module()
active = 0
max_active = 0
state_lock = threading.Lock()
def fake_call(_name, _args):
nonlocal active, max_active
with state_lock:
active += 1
max_active = max(max_active, active)
time.sleep(0.1)
with state_lock:
active -= 1
return {"ok": True}
monkeypatch.setattr(mod, "call_tool_impl", fake_call)
read_threads = [
threading.Thread(target=mod.call_tool, args=("nap_whoami", {}))
for _ in range(2)
]
for thread in read_threads:
thread.start()
for thread in read_threads:
thread.join()
assert max_active == 2
max_active = 0
write_threads = [
threading.Thread(target=mod.call_tool, args=("nap_create_node", {}))
for _ in range(2)
]
for thread in write_threads:
thread.start()
for thread in write_threads:
thread.join()
assert max_active == 1
def test_concurrent_first_reads_share_one_project_bootstrap(tmp_path, monkeypatch):
mod = load_module()
mod.AUTH_PATH = tmp_path / "missing-auth.json"
mod.DEFAULT_PROJECT_ID = None
monkeypatch.setattr(mod, "refresh_public_auth_state", lambda: None)
calls = []
def bootstrap(_args):
calls.append("bootstrap")
time.sleep(0.1)
mod.DEFAULT_PROJECT_ID = "project-1"
return {"project_id": "project-1"}
monkeypatch.setattr(mod, "bootstrap_project", bootstrap)
results = []
threads = [
threading.Thread(target=lambda: results.append(mod.resolve_project_id({})))
for _ in range(3)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
assert results == ["project-1"] * 3
assert calls == ["bootstrap"]
def test_concurrent_auth_updates_are_atomic_and_merge_fields(tmp_path):
mod = load_module()
mod.AUTH_DIR = tmp_path / ".napseer"
mod.AUTH_PATH = mod.AUTH_DIR / "auth.json"
mod.VAULT_PATH = mod.AUTH_DIR / "vault.json"
mod.AUTH_DIR.mkdir()
mod.write_public_auth({"base_url": "https://api.example.test"})
barrier = threading.Barrier(3)
def update(values):
barrier.wait()
mod.save_auth(values)
threads = [
threading.Thread(target=update, args=({"project_slug": "demo"},)),
threading.Thread(target=update, args=({"project_name": "Demo"},)),
]
for thread in threads:
thread.start()
barrier.wait()
for thread in threads:
thread.join()
saved = mod.load_public_auth_file()
assert saved["project_slug"] == "demo"
assert saved["project_name"] == "Demo"
assert not list(mod.AUTH_DIR.glob(".auth.json.*.tmp"))
def test_auth_renewal_reuses_token_refreshed_by_another_request(monkeypatch):
mod = load_module()
mod.TOKEN = "fresh-access"
mod.TOKEN_EXPIRES_AT = "future"
mod.REFRESH_EXPIRES_AT = "later"
mod.DEFAULT_PROJECT_ID = "project-1"
monkeypatch.setattr(mod, "refresh_public_auth_state", lambda: None)
monkeypatch.setattr(
mod,
"_renew_auth_locked",
lambda: (_ for _ in ()).throw(AssertionError("must not rotate again")),
)
result = mod.renew_auth(stale_token="stale-access")
assert result == {
"status": "already_renewed",
"method": "concurrent_refresh",
"token_expires_at": "future",
"refresh_expires_at": "later",
"project_id": "project-1",
}
+32
View File
@@ -2,6 +2,7 @@
"""Regression tests for account-login -> project-create authorization."""
import importlib.util
import json
import pathlib
import sys
@@ -125,9 +126,40 @@ def test_server_scope_denial_is_actionable():
raise AssertionError("scope denial should return an actionable error")
def test_anonymous_limits_are_actionable_without_exposing_response_details():
mod = load_module()
class HttpError:
code = 403
for code, label in (
("anonymous_space_limit_reached", "space"),
("anonymous_node_limit_reached", "node"),
):
error = mod.safe_http_error(
HttpError(),
operation="POST request",
body_text=json.dumps(
{
"error": {
"code": code,
"message": "server detail must not be forwarded",
}
}
),
)
assert error.code == code
assert error.status == 403
assert error.service_code == code
assert label in error.safe_message
assert "nap project claim" in error.safe_message
assert "server detail" not in error.safe_message
if __name__ == "__main__":
test_account_login_requests_create_and_runtime_scopes()
test_legacy_account_scope_fails_before_http_with_recovery()
test_successful_create_transitions_local_state_to_project_mode()
test_server_scope_denial_is_actionable()
test_anonymous_limits_are_actionable_without_exposing_response_details()
print("ok: project create OAuth lifecycle passed")