`nap gateway vault setup` unwraps the project memory secret once (prompt on a terminal, or the existing argument/env paths) and stores the secret, never the passphrase, in memory-secrets.vault.json under the existing 0600 local vault key. The CLI and MCP workers load it automatically and MCP never prompts, per /decisions/local-gateway-no-interactive-unlock and /decisions/2026-09-29-memory-secret-local-vault. Older versions stay readable; a rotated active version reports memory_secret_rotated with the setup next action. `vault forget` revokes local access, including for running workers. Env passphrases still work, are persisted into the vault, and are reported as deprecated. Search warns e2e_index_not_provisioned instead of e2e_index_locked. Local key creation is 0600 from the start and never replaces a published key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
269 lines
9.5 KiB
Python
Executable File
269 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Smoke-test the consolidated CLI dispatch surface.
|
|
|
|
Verifies that:
|
|
- canonical verbs (status, list, create, update, delete, run, preregister, activate)
|
|
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.
|
|
- project encryption compatibility forms normalize to one transition handler.
|
|
"""
|
|
|
|
import importlib.util
|
|
import contextlib
|
|
import io
|
|
import pathlib
|
|
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_dispatch_test", script_path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
assert spec and spec.loader
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
def expect_unknown(mod, argv, expected_fragment):
|
|
"""Run cli_main(argv) and assert it raises RuntimeError with the fragment."""
|
|
try:
|
|
mod.cli_main(argv)
|
|
except RuntimeError as exc:
|
|
assert expected_fragment in str(exc), (argv, str(exc))
|
|
else:
|
|
raise AssertionError(f"expected RuntimeError for {argv}")
|
|
|
|
|
|
def test_reindex_cli_rebuilds_with_bounded_page_size():
|
|
mod = load_module()
|
|
calls = []
|
|
mod.reindex_project = lambda args: calls.append(dict(args)) or {
|
|
"indexed": 7,
|
|
"graph_complete": True,
|
|
}
|
|
|
|
output = io.StringIO()
|
|
with contextlib.redirect_stdout(output):
|
|
mod.cli_main(["napseer_mcp_server.py", "reindex", "--limit", "50"])
|
|
|
|
assert calls == [{"limit": 50}]
|
|
assert '"indexed": 7' in output.getvalue()
|
|
|
|
|
|
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 [
|
|
["napseer_mcp_server.py", "chat", "secret", "init"],
|
|
["napseer_mcp_server.py", "chat", "secret", "configure"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown chat secret action")
|
|
|
|
|
|
def test_lineage_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "lineage", "list"],
|
|
["napseer_mcp_server.py", "lineage", "ls"],
|
|
["napseer_mcp_server.py", "lineage", "check"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown lineage subcommand")
|
|
|
|
|
|
def test_plan_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "plan", "ls"],
|
|
["napseer_mcp_server.py", "plan", "active"],
|
|
["napseer_mcp_server.py", "plan", "card-from"],
|
|
["napseer_mcp_server.py", "plan", "create-card"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown plan subcommand")
|
|
|
|
|
|
def test_feedback_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "feedback", "ls"],
|
|
["napseer_mcp_server.py", "feedback", "admin"],
|
|
["napseer_mcp_server.py", "feedback", "admin-list"],
|
|
["napseer_mcp_server.py", "feedback", "global-list"],
|
|
["napseer_mcp_server.py", "feedback", "status", "abc", "open"],
|
|
["napseer_mcp_server.py", "feedback", "set-status", "abc", "open"],
|
|
["napseer_mcp_server.py", "feedback", "update", "abc", "open"],
|
|
["napseer_mcp_server.py", "feedback", "resolved", "abc"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown feedback subcommand")
|
|
|
|
|
|
def test_agent_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "agent", "ls"],
|
|
["napseer_mcp_server.py", "agent", "registered"],
|
|
["napseer_mcp_server.py", "agent", "workspace-list"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown agent subcommand")
|
|
|
|
|
|
def test_gateway_status_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "gateway", "ls"],
|
|
["napseer_mcp_server.py", "gateway", "list"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown gateway subcommand")
|
|
|
|
|
|
def test_gateway_terminal_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "gateway", "terminals", "list"],
|
|
["napseer_mcp_server.py", "gateway", "terminal", "new"],
|
|
["napseer_mcp_server.py", "gateway", "terminal", "rm", "x"],
|
|
["napseer_mcp_server.py", "gateway", "terminal", "send", "x"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown gateway")
|
|
|
|
|
|
def test_gateway_schedule_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "gateway", "schedules", "list"],
|
|
["napseer_mcp_server.py", "gateway", "cron", "list"],
|
|
["napseer_mcp_server.py", "gateway", "crons", "list"],
|
|
["napseer_mcp_server.py", "gateway", "schedule", "ls"],
|
|
["napseer_mcp_server.py", "gateway", "schedule", "new"],
|
|
["napseer_mcp_server.py", "gateway", "schedule", "edit", "x"],
|
|
["napseer_mcp_server.py", "gateway", "schedule", "rm", "x"],
|
|
["napseer_mcp_server.py", "gateway", "schedule", "run-now", "x"],
|
|
]:
|
|
expect_unknown(mod, args, "unknown gateway")
|
|
|
|
|
|
def test_gateway_schedule_list_uses_canonical_route():
|
|
mod = load_module()
|
|
calls = []
|
|
mod.cli_gateway_require_unlocked = lambda args, operation: None
|
|
mod.cli_gateway_service_call = lambda route, args: calls.append((route, dict(args))) or {
|
|
"status": "listed",
|
|
"schedules": [],
|
|
}
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
mod.cli_main(["napseer_mcp_server.py", "gateway", "schedule", "list"])
|
|
assert calls == [("/local-api/gateway/schedule/list", {})]
|
|
|
|
|
|
def test_gateway_service_run_rejected():
|
|
"""Lifecycle verbs (run, start, etc.) moved to `nap service`."""
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "gateway", "service", "run"],
|
|
["napseer_mcp_server.py", "gateway", "service", "start"],
|
|
["napseer_mcp_server.py", "gateway", "service", "register"],
|
|
["napseer_mcp_server.py", "gateway", "service", "bootstrap"],
|
|
]:
|
|
expect_unknown(mod, args, "nap service start")
|
|
|
|
|
|
def test_gateway_vault_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "gateway", "vault", "list"],
|
|
["napseer_mcp_server.py", "gateway", "vault", "ls"],
|
|
]:
|
|
expect_unknown(mod, args, "gateway vault action: list")
|
|
|
|
|
|
def test_gateway_vault_unlock_verbs_point_to_setup_and_forget():
|
|
"""The memory secret is provisioned once, not unlocked per session."""
|
|
mod = load_module()
|
|
for action in ("unlock", "lock", "provision", "remove", "reset"):
|
|
expect_unknown(mod, ["napseer_mcp_server.py", "gateway", "vault", action], "nap gateway vault setup")
|
|
|
|
|
|
def test_gateway_rotate_passphrase_aliases_rejected():
|
|
mod = load_module()
|
|
for args in [
|
|
["napseer_mcp_server.py", "gateway", "passwd"],
|
|
["napseer_mcp_server.py", "gateway", "change-passphrase"],
|
|
]:
|
|
expect_unknown(mod, args, "rotate-passphrase")
|
|
|
|
|
|
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()
|
|
calls = dispatch_encryption(
|
|
mod,
|
|
["napseer_mcp_server.py", "project", "encryption", "set", "plaintext"],
|
|
)
|
|
assert calls == [{"state": "plaintext", "passphrase": None}]
|
|
|
|
|
|
def test_project_encryption_action_as_state_is_hidden_compatibility():
|
|
mod = load_module()
|
|
calls = dispatch_encryption(
|
|
mod,
|
|
["napseer_mcp_server.py", "project", "encryption", "plaintext"],
|
|
)
|
|
assert calls == [{"state": "plaintext", "passphrase": None}]
|
|
|
|
|
|
def test_project_encryption_status_aliases_are_read_only_compatibility():
|
|
mod = load_module()
|
|
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"],
|
|
]:
|
|
with contextlib.redirect_stdout(io.StringIO()):
|
|
mod.cli_main(argv)
|
|
assert calls == [{}, {}]
|
|
|
|
|
|
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()
|
|
test_feedback_aliases_rejected()
|
|
test_agent_aliases_rejected()
|
|
test_gateway_status_aliases_rejected()
|
|
test_gateway_terminal_aliases_rejected()
|
|
test_gateway_schedule_aliases_rejected()
|
|
test_gateway_schedule_list_uses_canonical_route()
|
|
test_gateway_service_run_rejected()
|
|
test_gateway_vault_aliases_rejected()
|
|
test_gateway_rotate_passphrase_aliases_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")
|