Release resilient atomic Napseer CLI runtime

This commit is contained in:
HumbleTurtle
2026-07-30 00:21:50 -03:00
parent a88930ce69
commit a70cb7c46c
17 changed files with 3734 additions and 1466 deletions
+21 -23
View File
@@ -35,33 +35,31 @@ def test_initial_enrollment_persists_refresh_credential():
mod.AUTH = anonymous_auth(mod)
mod.BASE_URL = mod.AUTH["base_url"]
mod.DEFAULT_PROJECT_ID = None
with tempfile.TemporaryDirectory() as directory:
key_path = pathlib.Path(directory) / "test-key"
pathlib.Path(str(key_path) + ".pub").write_text(
"ssh-ed25519 test-public-key", encoding="utf-8"
)
mod.ensure_local_files = lambda: key_path
mod.sign_text = lambda *args: "test-signature"
saved = []
mod.save_auth = lambda payload: saved.append(dict(payload))
responses = iter([
{"challenge_id": "challenge", "challenge_text": "sign-me"},
{
"token": {
"access_token": "access",
"refresh_token": "refresh",
"expires_at": "2099-01-01T00:00:00Z",
"refresh_expires_at": "",
},
"worker": {"id": "worker", "agent_id": "agent-id"},
},
])
mod.request_json = lambda *args, **kwargs: next(responses)
saved = []
requests = []
mod.save_auth = lambda payload: saved.append(dict(payload))
mod.ensure_enrolled({"slug": "example"})
def request(method, path, payload=None, token_required=False):
requests.append((method, path, payload, token_required))
return {
"token": {
"access_token": "access",
"refresh_token": "refresh",
"expires_at": "2099-01-01T00:00:00Z",
"refresh_expires_at": "",
},
"worker": {"id": "worker", "agent_id": "agent-id"},
}
mod.request_json = request
mod.ensure_enrolled({"slug": "example"})
assert saved[0]["refresh_token"] == "refresh"
assert "refresh_expires_at" in saved[0]
assert [(method, path) for method, path, _, _ in requests] == [
("POST", "/v1/enrollment/token")
]
assert requests[0][3] is False
def test_failed_anonymous_refresh_uses_ssh_recovery():
+282
View File
@@ -0,0 +1,282 @@
import importlib.util
import hashlib
import json
import pathlib
SCRIPT_PATH = (
pathlib.Path(__file__).resolve().parents[1]
/ "resources"
/ "scripts"
/ "nap_install.py"
)
def load_installer():
spec = importlib.util.spec_from_file_location("nap_install_command_system_test", SCRIPT_PATH)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def fail_if_called(*_args, **_kwargs):
raise AssertionError("help dispatched a side-effecting handler")
def test_root_help_exposes_only_canonical_commands(capsys):
module = load_installer()
assert module.main(["nap"]) == 0
output = capsys.readouterr().out
visible = [name for name, metadata in module.COMMAND_METADATA.items() if metadata["visible"]]
assert visible == [name for name, _summary in module.CANONICAL_COMMANDS]
assert len(visible) == 10
for name in visible:
assert f" {name}" in output
for hidden in ("chat", "plan", "lineage", "agent", "feedback", "where", "install"):
assert f" {hidden}" not in output
def test_help_is_resolved_before_mutating_dispatch(monkeypatch, capsys):
module = load_installer()
monkeypatch.setattr(module, "install_assets", fail_if_called)
monkeypatch.setattr(module, "run_wrapper", fail_if_called)
monkeypatch.setattr(module, "handle_gateway", fail_if_called)
assert module.main(["nap", "update", "help"]) == 0
assert module.main(["nap", "install", "--help"]) == 0
assert module.main(["nap", "auth", "--help"]) == 0
assert module.main(["nap", "project", "encryption", "--help"]) == 0
assert module.main(["nap", "gateway", "vault", "help"]) == 0
output = capsys.readouterr().out
assert "Help never performs an update" in output
assert "Use `nap mcp install`" in output
assert "Usage: nap auth" in output
assert "Usage: nap project" in output
assert "Usage: nap gateway" in output
def test_help_topic_uses_same_canonical_text(capsys):
module = load_installer()
assert module.main(["nap", "help", "mcp"]) == 0
assert "Usage: nap mcp [status|install|update|serve]" in capsys.readouterr().out
def test_init_and_auth_repair_normalize_to_existing_handlers(monkeypatch):
module = load_installer()
calls = []
monkeypatch.setattr(
module,
"run_wrapper",
lambda command, args: calls.append((command, args)) or 0,
)
assert module.main(["nap", "init", "--slug", "demo"]) == 0
assert module.main(["nap", "auth", "repair"]) == 0
assert calls == [
("project", ["init", "--slug", "demo"]),
("auth", ["refresh"]),
]
def test_update_does_not_overwrite_repository_source(monkeypatch, capsys):
module = load_installer()
calls = []
monkeypatch.setattr(
module,
"install_assets",
lambda: calls.append("update") or {"status": "installed"},
)
assert module.main(["nap", "update"]) == 0
assert calls == ["update"]
assert json.loads(capsys.readouterr().out)["status"] == "updated"
def test_doctor_reports_presence_without_exposing_values(tmp_path, monkeypatch):
module = load_installer()
state_dir = tmp_path / ".napseer"
state_dir.mkdir()
auth_path = state_dir / "auth.json"
auth_path.write_text(
json.dumps(
{
"account_mode": "anonymous",
"project_id": "test-project",
"token": "test-access-value",
"refresh_token": "test-refresh-value",
}
),
encoding="utf-8",
)
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,
"state_dir_status",
lambda: {
"active_auth_path": str(auth_path),
"state_dir_exists": True,
},
)
result = module.doctor_status()
serialized = json.dumps(result)
assert result["status"] == "ok"
assert result["checks"]["token_present"] is True
assert result["checks"]["refresh_present"] is True
assert "test-access-value" not in serialized
assert "test-refresh-value" not in serialized
def test_doctor_does_not_create_state_directory(tmp_path, monkeypatch):
module = load_installer()
state_dir = tmp_path / ".napseer"
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: [])
result = module.doctor_status()
assert result["status"] == "repair_required"
assert not state_dir.exists()
def test_wrapper_dispatch_never_installs_missing_runtime(monkeypatch):
module = load_installer()
monkeypatch.setattr(module, "runtime_assets_missing", lambda: ["napseer_mcp_server.py"])
monkeypatch.setattr(module, "install_assets", fail_if_called)
try:
module.run_wrapper("configure", [])
except RuntimeError as exc:
assert "nap mcp install" in str(exc)
else:
raise AssertionError("missing runtime should require an explicit repair command")
def fake_bundle(module, revision="a" * 40, broken_name=None):
contents = {
name: f"#!/usr/bin/env python3\nVALUE = {index!r}\n"
for index, name in enumerate(module.SCRIPT_NAMES)
}
items = []
payloads = {}
for name in module.SCRIPT_NAMES:
content = contents[name]
digest = hashlib.sha256(content.encode("utf-8")).hexdigest()
item = {
"name": name,
"version": module.CLI_RELEASE_VERSION,
"bytes": len(content.encode("utf-8")),
"sha256": digest,
"source_repo": "https://github.com/napseer/nap-cli",
"source_revision": revision,
"source_revision_status": "resolved",
"contract_version": module.CLI_DISTRIBUTION_CONTRACT_VERSION,
"minimum_contract_version": module.CLI_MINIMUM_CONTRACT_VERSION,
"install_path": module.INSTALL_PATHS[name],
"mode": "0755",
}
items.append(item)
payloads[name] = ({**item, "content": content}, content)
if broken_name:
payload, content = payloads[broken_name]
payloads[broken_name] = (payload, content + "not valid python !!!")
manifest = {
"schema_version": module.CLI_BUNDLE_SCHEMA_VERSION,
"bundle_id": f"nap-cli:{module.CLI_RELEASE_VERSION}:{revision}",
"release_version": module.CLI_RELEASE_VERSION,
"published_at": "2026-07-29T00:00:00Z",
"contract": {
"current": module.CLI_DISTRIBUTION_CONTRACT_VERSION,
"minimum_supported": module.CLI_MINIMUM_CONTRACT_VERSION,
},
"source": {
"repo": "https://github.com/napseer/nap-cli",
"revision": revision,
"revision_status": "resolved",
},
"items": items,
}
return manifest, payloads
def test_bundle_install_stages_validates_and_activates_atomically(tmp_path, monkeypatch):
module = load_installer()
install_dir = tmp_path / "install"
bin_dir = tmp_path / "bin"
monkeypatch.setattr(module, "INSTALL_DIR", install_dir)
monkeypatch.setattr(module, "BIN_DIR", bin_dir)
manifest, payloads = fake_bundle(module)
monkeypatch.setattr(module, "bundle_manifest", lambda: manifest)
monkeypatch.setattr(module, "fetch_script", lambda name: payloads[name])
result = module.install_assets()
assert result["cli_distribution"]["release_version"] == "0.2.0"
assert (install_dir / "current").is_symlink()
assert (install_dir / "current" / "bundle-manifest.json").is_file()
assert (install_dir / "current" / "napseer_mcp_server.py").is_file()
assert (install_dir / "current" / "terminal" / "protocol.py").is_file()
assert (install_dir / "napseer_mcp_supervisor.py").is_symlink()
assert (bin_dir / "nap").is_symlink()
assert module.version_status()["source_revision"] == "a" * 40
def test_failed_bundle_validation_keeps_previous_release_active(tmp_path, monkeypatch):
module = load_installer()
monkeypatch.setattr(module, "INSTALL_DIR", tmp_path / "install")
monkeypatch.setattr(module, "BIN_DIR", tmp_path / "bin")
first_manifest, first_payloads = fake_bundle(module, revision="a" * 40)
monkeypatch.setattr(module, "bundle_manifest", lambda: first_manifest)
monkeypatch.setattr(module, "fetch_script", lambda name: first_payloads[name])
module.install_assets()
active_before = (module.INSTALL_DIR / "current").resolve()
broken_manifest, broken_payloads = fake_bundle(
module,
revision="b" * 40,
broken_name="napseer_mcp_server.py",
)
monkeypatch.setattr(module, "bundle_manifest", lambda: broken_manifest)
monkeypatch.setattr(module, "fetch_script", lambda name: broken_payloads[name])
try:
module.install_assets()
except RuntimeError as exc:
assert "does not match bundle manifest" in str(exc)
else:
raise AssertionError("invalid bundle must not activate")
assert (module.INSTALL_DIR / "current").resolve() == active_before
assert not list((module.INSTALL_DIR / "releases").glob(".staging-*"))
def test_manifest_rejects_path_traversal():
module = load_installer()
manifest, _payloads = fake_bundle(module)
target = next(item for item in manifest["items"] if item["name"] == "nap_install.py")
target["install_path"] = "../nap_install.py"
try:
module.validated_manifest_items(manifest)
except RuntimeError as exc:
assert "unsafe install_path" in str(exc)
else:
raise AssertionError("unsafe manifest path must be rejected")
+32 -18
View File
@@ -6,11 +6,12 @@ Verifies that:
are accepted and route to the right function.
- dropped aliases (ls, new, send, rm, set-status, admin, global-list, etc.)
now raise RuntimeError with a clear pointer to the canonical verb.
- `nap project encryption set` requires --state; positional-state shortcut is gone.
- `nap project encryption plaintext` (action-as-state) now errors.
- project encryption compatibility forms normalize to one transition handler.
"""
import importlib.util
import contextlib
import io
import pathlib
import sys
@@ -155,35 +156,48 @@ def test_gateway_rotate_passphrase_aliases_rejected():
expect_unknown(mod, args, "rotate-passphrase")
def test_project_encryption_state_flag_required():
"""nap project encryption set requires --state; positional state is rejected."""
def dispatch_encryption(mod, argv):
calls = []
mod.project_encryption_transition = lambda args: (
calls.append(dict(args)) or {"status": "updated", "state": args["state"]}
)
with contextlib.redirect_stdout(io.StringIO()):
mod.cli_main(argv)
return calls
def test_project_encryption_set_compatibility_form():
"""The explicit set form routes to the canonical transition handler."""
mod = load_module()
# Without --state, the dispatch should raise
expect_unknown(
calls = dispatch_encryption(
mod,
["napseer_mcp_server.py", "project", "encryption", "set", "plaintext"],
"--state plaintext|encrypted",
)
assert calls == [{"state": "plaintext", "passphrase": None}]
def test_project_encryption_action_as_state_rejected():
"""`nap project encryption plaintext` (the old shortcut) must error."""
def test_project_encryption_action_as_state_is_hidden_compatibility():
mod = load_module()
expect_unknown(
calls = dispatch_encryption(
mod,
["napseer_mcp_server.py", "project", "encryption", "plaintext"],
"project encryption",
)
assert calls == [{"state": "plaintext", "passphrase": None}]
def test_project_encryption_show_alias_rejected():
def test_project_encryption_status_aliases_are_read_only_compatibility():
mod = load_module()
for args in [
calls = []
mod.project_encryption_status = lambda args: (
calls.append(dict(args)) or {"status": "ok", "state": "plaintext"}
)
for argv in [
["napseer_mcp_server.py", "project", "encryption", "state"],
["napseer_mcp_server.py", "project", "encryption", "show"],
["napseer_mcp_server.py", "project", "encryption", "encrypted"],
]:
expect_unknown(mod, args, "project encryption")
with contextlib.redirect_stdout(io.StringIO()):
mod.cli_main(argv)
assert calls == [{}, {}]
if __name__ == "__main__":
@@ -198,7 +212,7 @@ if __name__ == "__main__":
test_gateway_service_run_rejected()
test_gateway_vault_aliases_rejected()
test_gateway_rotate_passphrase_aliases_rejected()
test_project_encryption_state_flag_required()
test_project_encryption_action_as_state_rejected()
test_project_encryption_show_alias_rejected()
test_project_encryption_set_compatibility_form()
test_project_encryption_action_as_state_is_hidden_compatibility()
test_project_encryption_status_aliases_are_read_only_compatibility()
print("ok: cli dispatch consolidation tests passed")
+43 -116
View File
@@ -246,141 +246,62 @@ 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) == len(mod.tools())
get_tool = next(item for item in mod.tools() if item["name"] == "nap_node_get")
assert get_tool["inputSchema"].get("required") is None
assert "node_id" in get_tool["inputSchema"]["properties"]
assert "uri" in get_tool["inputSchema"]["properties"]
assert "path" not in get_tool["inputSchema"]["properties"]
by_id = next(item for item in mod.tools() if item["name"] == "nap_node_by_id")
assert by_id["inputSchema"]["required"] == ["node_id"]
assert "nap:// URI" in by_id["description"]
by_uri = next(item for item in mod.tools() if item["name"] == "nap_node_by_uri")
assert by_uri["inputSchema"]["required"] == ["uri"]
assert "nap://" in by_uri["description"]
tool = next(item for item in mod.tools() if item["name"] == "nap_node_by_path")
assert tool["inputSchema"]["required"] == ["path"]
assert "preview_chars" not in tool["inputSchema"]["properties"]
assert "does not read full node content" in tool["description"]
assert all(item["name"] != "nap_cat" for item in mod.tools())
for hidden_name in [
"nap_cat",
"nap_node_by_id",
"nap_node_by_uri",
"nap_tee",
"nap_patch",
"nap_find",
"nap_index_sync",
]:
assert hidden_name not in tool_by_name
for name in ["nap_discover", "nap_find", "nap_context", "nap_find_related", "nap_recent"]:
for name in ["nap_discover", "nap_context", "nap_find_related", "nap_recent"]:
schema = next(item for item in mod.tools() if item["name"] == name)["inputSchema"]
view = schema["properties"].get("view")
if view:
assert "full" not in view["enum"]
for name in ["nap_patch", "nap_node_patch", "nap_rm"]:
for name in ["nap_node_patch", "nap_rm"]:
schema = next(item for item in mod.tools() if item["name"] == name)["inputSchema"]
assert "node_id" in schema["properties"]
assert "uri" in schema["properties"]
assert "path" not in schema["properties"]
tee_schema = next(item for item in mod.tools() if item["name"] == "nap_tee")["inputSchema"]
assert tee_schema.get("required") is None
assert "path" in tee_schema["properties"]
assert "node_id" in tee_schema["properties"]
assert "uri" in tee_schema["properties"]
bulk_node_schema = next(item for item in mod.tools() if item["name"] == "nap_bulk")["inputSchema"]["properties"]["nodes"]["items"]
assert bulk_node_schema.get("required") is None
assert "path" in bulk_node_schema["properties"]
assert "node_id" in bulk_node_schema["properties"]
assert "uri" in bulk_node_schema["properties"]
for name in [
"nap_kanban_start",
"nap_kanban_send_review",
"nap_kanban_complete",
"nap_kanban_block",
"nap_kanban_unblock",
"nap_kanban_update",
"nap_kanban_archive",
for required_name in [
"nap_library_ls",
"nap_library_attach",
"nap_library_detach",
"nap_library_nodes",
"nap_library_node_get",
"nap_auth_refresh",
"nap_node_restore",
"nap_plan_transition",
"nap_index_status",
]:
schema = next(item for item in mod.tools() if item["name"] == name)["inputSchema"]
assert "node_id" in schema["properties"]
assert "uri" in schema["properties"]
assert "path" not in schema["properties"]
mv_schema = next(item for item in mod.tools() if item["name"] == "nap_mv")["inputSchema"]
assert mv_schema["required"] == ["new_path"]
assert "node_id" in mv_schema["properties"]
assert "uri" in mv_schema["properties"]
assert "path" not in mv_schema["properties"]
ln_schema = next(item for item in mod.tools() if item["name"] == "nap_ln")["inputSchema"]
assert "source_node_id" in ln_schema["properties"]
assert "source_uri" in ln_schema["properties"]
assert "target_node_id" in ln_schema["properties"]
assert "target_uri" in ln_schema["properties"]
assert "source_path" not in ln_schema["properties"]
assert "target_path" not in ln_schema["properties"]
backlinks_schema = next(item for item in mod.tools() if item["name"] == "nap_backlinks")["inputSchema"]
assert "node_id" in backlinks_schema["properties"]
assert "uri" in backlinks_schema["properties"]
assert "path" not in backlinks_schema["properties"]
context_schema = next(item for item in mod.tools() if item["name"] == "nap_context")["inputSchema"]
assert "node_id" in context_schema["properties"]
assert "node_ids" in context_schema["properties"]
assert "uri" in context_schema["properties"]
assert "uris" in context_schema["properties"]
assert "path" in context_schema["properties"]
assert "paths" in context_schema["properties"]
related_schema = next(item for item in mod.tools() if item["name"] == "nap_find_related")["inputSchema"]
assert "node_id" in related_schema["properties"]
assert "uri" in related_schema["properties"]
assert "path" not in related_schema["properties"]
complete_schema = next(item for item in mod.tools() if item["name"] == "nap_plan_complete")["inputSchema"]
assert "plan_node_id" in complete_schema["properties"]
assert "plan_uri" in complete_schema["properties"]
assert "outcome_node_id" in complete_schema["properties"]
assert "outcome_uri" in complete_schema["properties"]
assert "path" not in complete_schema["properties"]
assert "outcome_path" not in complete_schema["properties"]
supersede_schema = next(item for item in mod.tools() if item["name"] == "nap_plan_supersede")["inputSchema"]
assert "required" not in supersede_schema
assert "plan_node_id" in supersede_schema["properties"]
assert "replacement_node_id" in supersede_schema["properties"]
assert "path" not in supersede_schema["properties"]
assert "replacement_path" not in supersede_schema["properties"]
cancel_schema = next(item for item in mod.tools() if item["name"] == "nap_plan_cancel")["inputSchema"]
assert "required" not in cancel_schema
assert "plan_node_id" in cancel_schema["properties"]
assert "path" not in cancel_schema["properties"]
plan_to_kanban_schema = next(item for item in mod.tools() if item["name"] == "nap_plan_to_kanban")["inputSchema"]
assert "plan_node_id" in plan_to_kanban_schema["properties"]
assert "plan_uri" in plan_to_kanban_schema["properties"]
assert "plan_path" not in plan_to_kanban_schema["properties"]
for name in ["nap_agent_cat", "nap_agent_patch", "nap_agent_rm"]:
schema = next(item for item in mod.tools() if item["name"] == name)["inputSchema"]
assert schema["required"] == ["agent_slug"]
assert "node_id" in schema["properties"]
assert "uri" in schema["properties"]
assert "path" not in schema["properties"]
agent_tee_schema = next(item for item in mod.tools() if item["name"] == "nap_agent_tee")["inputSchema"]
assert agent_tee_schema["required"] == ["agent_slug"]
assert "path" in agent_tee_schema["properties"]
assert "node_id" in agent_tee_schema["properties"]
assert "uri" in agent_tee_schema["properties"]
agent_ln_schema = next(item for item in mod.tools() if item["name"] == "nap_agent_ln")["inputSchema"]
assert agent_ln_schema["required"] == ["agent_slug"]
assert "source_node_id" in agent_ln_schema["properties"]
assert "target_node_id" in agent_ln_schema["properties"]
assert "source_path" not in agent_ln_schema["properties"]
assert "target_path" not in agent_ln_schema["properties"]
assert required_name in tool_by_name
def test_mutation_tools_reject_path_and_patch_by_node_id(mod):
@@ -508,15 +429,21 @@ def test_tee_and_bulk_create_by_path_update_by_identity(mod):
assert writes[-1][0] == "PATCH"
assert writes[-1][1] == "/v1/projects/project-1/nodes/node-123"
with pytest.raises(RuntimeError, match="path already resolves"):
mod.upsert_node({"path": "/notes/current", "content_text": "bad"})
compatibility_update = mod.upsert_node(
{"path": "/notes/current", "content_text": "compatibility update"}
)
assert compatibility_update["updated"] is True
assert writes[-1][0] == "PATCH"
bulk = mod.bulk_upsert_nodes({"nodes": [{"node_id": "node-123", "content_text": "bulk update"}]})
assert bulk["items"][0]["created"] is False
assert writes[-1][1] == "/v1/projects/project-1/nodes/node-123"
with pytest.raises(RuntimeError, match="path already resolves"):
mod.bulk_upsert_nodes({"nodes": [{"path": "/notes/current", "content_text": "bad"}]})
compatibility_bulk_update = mod.bulk_upsert_nodes(
{"nodes": [{"path": "/notes/current", "content_text": "compatibility update"}]}
)
assert compatibility_bulk_update["items"][0]["created"] is False
assert writes[-1][0] == "PATCH"
def test_agent_tools_create_by_route_and_update_by_identity(mod):
@@ -867,12 +794,12 @@ def test_index_sync_fetches_incremental_changes_and_removes_archived_nodes(mod):
assert mod.search_local_index({"q": "existing"})["items"] == []
def test_tools_include_index_sync_descriptor(mod):
tool = next(item for item in mod.tools() if item["name"] == "nap_index_sync")
assert "Incrementally synchronize" in tool["description"]
assert tool["inputSchema"]["properties"]["lookback_seconds"]["default"] == 5
contract = mod.tool_contract_metadata("nap_index_sync")
assert contract["side_effect"] == "local-file"
def test_tools_include_read_only_index_status_descriptor(mod):
tool = next(item for item in mod.tools() if item["name"] == "nap_index_status")
assert "local search index" in tool["description"]
assert tool["inputSchema"]["properties"] == {}
contract = mod.tool_contract_metadata("nap_index_status")
assert contract["side_effect"] == "none"
assert contract["auth_mode"] == "local-only"
+5 -1
View File
@@ -266,7 +266,11 @@ def run():
assert compact["view"] == "paths"
assert "content_text" not in compact["items"][0]
assert compact["items"][0]["full_path"] == "/renamed/alpha"
full = mod.list_project_nodes({"limit": 10, "view": "full"})
full = mod.list_project_nodes({
"limit": 10,
"view": "full",
"_allow_full": True,
})
assert full["items"][0]["content_text"] == "secret needle"
plan_base = {
+116
View File
@@ -0,0 +1,116 @@
import json
import os
import pathlib
import select
import signal
import subprocess
import sys
import time
ROOT = pathlib.Path(__file__).resolve().parents[1]
SUPERVISOR = ROOT / "resources" / "scripts" / "napseer_mcp_supervisor.py"
def write_worker(path: pathlib.Path, version: int) -> None:
path.write_text(
f"""\
import json
import os
import sys
VERSION = {version}
for line in sys.stdin:
message = json.loads(line)
if message.get("method") == "notifications/initialized":
continue
if message.get("method") == "tools/call":
name = (message.get("params") or {{}}).get("name")
if name == "crash":
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)
""",
encoding="utf-8",
)
class SupervisorClient:
def __init__(self, worker_path: pathlib.Path):
environment = os.environ.copy()
environment["NAPSEER_MCP_WORKER_PATH"] = str(worker_path)
environment["NAPSEER_MCP_RESPONSE_TIMEOUT_SECONDS"] = "5"
self.process = subprocess.Popen(
[sys.executable, str(SUPERVISOR)],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=environment,
)
def request(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)
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)
def test_restarts_worker_killed_between_requests(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
first = client.request(1, "tools/call", {"name": "pid", "arguments": {}})
os.kill(first["result"]["pid"], signal.SIGTERM)
time.sleep(0.1)
second = client.request(2, "tools/list")
assert second["result"]["version"] == 1
assert client.process.poll() is None
finally:
client.close()
def test_reloads_replaced_worker_between_requests(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
assert client.request(1, "initialize")["result"]["version"] == 1
time.sleep(0.01)
write_worker(worker_path, 2)
os.utime(worker_path, None)
assert client.request(2, "tools/list")["result"]["version"] == 2
finally:
client.close()
def test_worker_crash_returns_error_without_closing_transport(tmp_path):
worker_path = tmp_path / "worker.py"
write_worker(worker_path, 1)
client = SupervisorClient(worker_path)
try:
failed = client.request(1, "tools/call", {"name": "crash", "arguments": {}})
recovered = client.request(2, "tools/list")
assert failed["error"]["code"] == -32098
assert recovered["result"]["version"] == 1
assert client.process.poll() is None
finally:
client.close()
+31 -7
View File
@@ -163,13 +163,36 @@ def test_cli_main_dispatches_init():
print("ok: cli_main routes `project init` to cli_project_init passed")
def test_cli_main_rejects_old_aliases():
"""Verify `auth login` / `project bootstrap-existing` are no longer accepted."""
mod = load_module()
def test_cli_main_accepts_canonical_login_and_hidden_compatibility_alias():
"""Login aliases must route to one handler without performing browser or network I/O."""
import contextlib
import io
mod = load_module()
calls = []
mod.operator_account_login = lambda args: (
calls.append(dict(args)) or {"status": "authenticated"}
)
for subcommand in ["login", "login-local", "operator-login"]:
with contextlib.redirect_stdout(io.StringIO()):
mod.cli_main([
"napseer_mcp_server.py",
"auth",
subcommand,
"--no-browser",
"--timeout",
"1",
])
assert len(calls) == 3
assert all(call["open_browser"] is False for call in calls)
assert all(call["timeout_seconds"] == 1 for call in calls)
def test_cli_main_rejects_project_login_aliases():
mod = load_module()
cases = [
(["napseer_mcp_server.py", "auth", "login"], "unknown auth subcommand: login"),
(["napseer_mcp_server.py", "auth", "operator-login"], "unknown auth subcommand: operator-login"),
(["napseer_mcp_server.py", "project", "login"], "unknown project subcommand: login"),
(["napseer_mcp_server.py", "project", "bootstrap-existing"], "unknown project subcommand: bootstrap-existing"),
]
@@ -180,7 +203,7 @@ def test_cli_main_rejects_old_aliases():
assert expected_substring in str(exc), (args, str(exc))
else:
raise AssertionError(f"expected RuntimeError for {args}")
print("ok: cli_main rejects old auth/project aliases with clear errors passed")
print("ok: cli_main rejects old project aliases with clear errors passed")
if __name__ == "__main__":
@@ -188,5 +211,6 @@ if __name__ == "__main__":
test_init_already_initialized()
test_init_token_only_creates_project()
test_cli_main_dispatches_init()
test_cli_main_rejects_old_aliases()
test_cli_main_accepts_canonical_login_and_hidden_compatibility_alias()
test_cli_main_rejects_project_login_aliases()
print("ok: project init wizard smoke tests passed")