from __future__ import annotations
import asyncio
import json
import pytest
pytest.importorskip("net")
pytest.importorskip("net_sdk")
TOOL_NAMES = {
"net_search_capabilities",
"net_describe_capability",
"net_invoke_capability",
"net_list_pinned_capabilities",
"net_request_pin",
"net_mesh_invite",
"net_mesh_devices",
"net_mesh_revoke",
"net_a2a_submit",
"net_a2a_status",
"net_a2a_cancel",
}
def _run(coro):
return json.loads(asyncio.run(coro))
def test_register_wires_all_tools_and_hook(plugin, ctx):
plugin.register(ctx)
assert set(ctx.tools) == TOOL_NAMES
for name, entry in ctx.tools.items():
assert entry["toolset"] == "net"
assert entry["is_async"] is True
assert entry["check_fn"] is plugin.node.check_net_available
assert entry["schema"]["name"] == name
assert entry["schema"]["parameters"]["type"] == "object"
assert callable(entry["handler"])
assert "on_session_start" in ctx.hooks
assert "on_session_end" in ctx.hooks
def test_session_end_teardown_survives_a_failing_stop(plugin, monkeypatch):
stopped = []
class _Svc:
def __init__(self, label, fail=False):
self._label = label
self._fail = fail
def stop(self):
stopped.append(self._label)
if self._fail:
raise RuntimeError(f"{self._label} refuses to stop")
shutdowns = []
monkeypatch.setattr(plugin.node, "shutdown", lambda: shutdowns.append(True))
monkeypatch.setattr(plugin, "_a2a_service", _Svc("a2a", fail=True))
monkeypatch.setattr(plugin, "_federation", _Svc("federation", fail=True))
monkeypatch.setattr(plugin, "_provider", _Svc("provider"))
monkeypatch.setattr(plugin, "_promotion", _Svc("promotion"))
plugin._on_session_end()
assert stopped == ["a2a", "federation", "provider", "promotion"]
assert shutdowns == [True], "node.shutdown still ran"
assert plugin._a2a_service is None and plugin._federation is None
assert plugin._provider is None and plugin._promotion is None
plugin._on_session_end()
assert stopped == ["a2a", "federation", "provider", "promotion"]
def test_descriptions_disambiguate_from_local_tool_search(plugin, ctx):
plugin.register(ctx)
desc = ctx.tools["net_search_capabilities"]["schema"]["description"]
assert "tool_search" in desc
assert "MESH" in desc
def test_search_on_isolated_node_is_ok_and_empty(plugin, node_ready):
tools = plugin.tools
result = _run(tools.handle_net_search({"query": "anything"}))
assert result == {"status": "ok", "capabilities": []}
def test_invoke_unknown_capability_is_structured(plugin, node_ready):
tools = plugin.tools
result = _run(tools.handle_net_invoke({"cap_id": "42/nope", "arguments": {"x": 1}}))
assert result["status"] in {"transport_error", "not_found"}
def test_invoke_and_describe_require_cap_id(plugin, node_ready):
tools = plugin.tools
assert _run(tools.handle_net_invoke({}))["status"] == "error"
assert _run(tools.handle_net_describe({}))["status"] == "error"
def test_invoke_defaults_absent_arguments_to_empty_object(plugin, node_ready, monkeypatch):
tools = plugin.tools
class _RecordingGateway:
def __init__(self):
self.last_args = None
async def invoke(self, cap_id, arguments_json):
self.last_args = arguments_json
return '{"status": "ok", "content": []}'
fake = _RecordingGateway()
monkeypatch.setattr(plugin.node, "gateway", lambda: fake)
_run(tools.handle_net_invoke({"cap_id": "42/x"}))
assert fake.last_args == "{}"
_run(tools.handle_net_invoke({"cap_id": "42/x", "arguments": None}))
assert fake.last_args == "{}"
def test_request_pin_records_pending_and_list_reflects_it(plugin, node_ready):
from net_sdk import PinStore
tools = plugin.tools
cap = "42/plugin-req-test"
res = _run(tools.handle_net_request_pin({"cap_id": cap}))
assert res["status"] == "pending_approval"
assert res["cap_id"] == cap
assert res["approval_channels"] == ["telegram", "desktop", "cli_fallback"]
assert res["approval_channels"][-1] == "cli_fallback"
assert "operator surface" in res["message"].lower()
assert res["message"].lower().index("hermes") < res["message"].lower().index("cli")
assert f"net mcp pin approve {cap}" in res["message"]
listed = _run(tools.handle_net_list_pinned({}))
assert listed["status"] == "ok"
assert cap in listed["pending"]
assert cap not in listed["approved"]
PinStore(node_ready.pin_store_path()).approve(cap)
listed2 = _run(tools.handle_net_list_pinned({}))
assert cap in listed2["approved"]
assert cap not in listed2["pending"]
def test_request_pin_requires_cap_id(plugin, node_ready):
tools = plugin.tools
assert _run(tools.handle_net_request_pin({}))["status"] == "error"
def test_list_pinned_surfaces_store_errors_as_data(plugin, node_ready, monkeypatch):
tools = plugin.tools
def _boom():
raise RuntimeError("store unreadable")
monkeypatch.setattr(plugin.node, "pin_store_path", _boom)
res = _run(tools.handle_net_list_pinned({}))
assert res["status"] == "error"
assert "store unreadable" in res["error"]
def test_request_pin_on_an_already_approved_cap_says_so(plugin, node_ready):
from net_sdk import PinStore
tools = plugin.tools
cap = "42/already-approved-req-test"
PinStore(node_ready.pin_store_path()).approve(cap)
res = _run(tools.handle_net_request_pin({"cap_id": cap}))
assert res["status"] == "approved"
assert "already approved" in res["message"].lower()
assert "approval required" not in res["message"].lower()
def test_malformed_peer_entries_do_not_crash_startup(plugin, monkeypatch, tmp_path):
monkeypatch.setenv("NET_MESH_PEERS", '[42, "nope"]')
monkeypatch.setenv("NET_MESH_PIN_STORE", str(tmp_path / "pins.json"))
monkeypatch.delenv("NET_MESH_PSK", raising=False)
monkeypatch.delenv("NET_MESH_IDENTITY_SEED", raising=False)
mesh, _gateway, _pin_store, _delegation = plugin.node._build()
try:
assert mesh is not None finally:
mesh.shutdown()
def test_init_failure_after_start_rolls_back_the_mesh(plugin, monkeypatch):
import net
class _FakeMesh:
def __init__(self, *a, **kw):
self.started = False
self.shut = 0
def start(self):
self.started = True
def connect(self, *a, **kw):
pass
def shutdown(self):
self.shut += 1
made = []
def factory(*a, **kw):
m = _FakeMesh()
made.append(m)
return m
monkeypatch.setattr(net, "NetMesh", factory)
monkeypatch.setattr("net_sdk.default_pin_store_path", lambda: None)
monkeypatch.delenv("NET_MESH_PIN_STORE", raising=False)
monkeypatch.delenv("NET_MESH_PSK", raising=False)
monkeypatch.delenv("NET_MESH_IDENTITY_SEED", raising=False)
with pytest.raises(RuntimeError, match="pin-store"):
plugin.node._build()
assert made, "the fake mesh should have been constructed"
assert made[0].started, "the mesh was started before the failure"
assert made[0].shut == 1, "the started mesh must be shut down on init failure"
def test_bad_identity_seed_is_a_clear_error(plugin, monkeypatch):
monkeypatch.setenv("NET_MESH_IDENTITY_SEED", "not-valid-hex-zz")
with pytest.raises(RuntimeError, match="NET_MESH_IDENTITY_SEED"):
plugin.node._build()