| """Integration test: two real nodes peer over HTTP and route capability calls. |
| |
| Proves the end-to-end wiring that lets a local node talk to the HF Space node: |
| 1. Node A runs a real FastAPI HttpServer exposing /bus/v1/call + /manifest. |
| 2. Node B (no llm/rag locally) calls discovery.peer.add with A's URL. |
| 3. B fetches A's manifest and registers A's capabilities as remote entries. |
| 4. B routes llm.chat / rag.query -> HttpBusTransport POSTs to A over HTTP. |
| |
| No mocks: a genuine uvicorn server and httpx client carry the calls. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import asyncio |
| import contextlib |
| import socket |
|
|
| import pytest |
|
|
| pytest.importorskip("fastapi") |
| pytest.importorskip("uvicorn") |
| pytest.importorskip("httpx") |
|
|
| from hearthnet.node import HearthNode |
| from hearthnet.transport.server import HttpServer |
|
|
|
|
| def _free_port() -> int: |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: |
| s.bind(("127.0.0.1", 0)) |
| return s.getsockname()[1] |
|
|
|
|
| async def _wait_ready(port: int, timeout: float = 5.0) -> None: |
| import httpx |
|
|
| deadline = asyncio.get_event_loop().time() + timeout |
| async with httpx.AsyncClient(timeout=1.0) as client: |
| while asyncio.get_event_loop().time() < deadline: |
| with contextlib.suppress(Exception): |
| r = await client.get(f"http://127.0.0.1:{port}/health") |
| if r.status_code == 200: |
| return |
| await asyncio.sleep(0.1) |
| raise TimeoutError(f"server on {port} never became ready") |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_two_nodes_peer_and_route_over_http(): |
| port_a = _free_port() |
|
|
| |
| node_a = HearthNode("ed25519:nodeA", "Alice", "ed25519:community") |
| node_a.install_demo_services(corpus="alpha") |
| server_a = HttpServer( |
| bus=node_a.bus, |
| node_manifest_fn=lambda: node_a.manifest().as_dict(), |
| host="127.0.0.1", |
| port=port_a, |
| ) |
| server_a.build_app() |
| await server_a.start() |
| try: |
| await _wait_ready(port_a) |
|
|
| |
| node_b = HearthNode("ed25519:nodeB", "Bob", "ed25519:community") |
| local_caps = {e.descriptor.name for e in node_b.bus.registry.all_local()} |
| assert "llm.chat" not in local_caps |
|
|
| |
| add_result = await node_b.bus.call( |
| "discovery.peer.add", |
| (1, 0), |
| {"input": {"endpoint": f"http://127.0.0.1:{port_a}"}}, |
| ) |
| added = add_result["output"]["capabilities"] |
| assert "llm.chat" in added |
| assert "rag.query" in added |
|
|
| |
| chat = await node_b.bus.call( |
| "llm.chat", |
| (1, 0), |
| {"input": {"messages": [{"role": "user", "content": "hello mesh"}]}}, |
| ) |
| content = chat["output"]["message"]["content"] |
| assert "hello mesh" in content |
|
|
| |
| remote_entries = {e.node_id for e in node_b.bus.registry.all_remote()} |
| assert "ed25519:nodeA" in remote_entries |
| finally: |
| await server_a.shutdown() |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_peer_add_unreachable_endpoint_errors_cleanly(): |
| node = HearthNode("ed25519:solo", "Solo", "ed25519:community") |
| result = await node.bus.call( |
| "discovery.peer.add", |
| (1, 0), |
| {"input": {"endpoint": "http://127.0.0.1:1"}}, |
| ) |
| assert result.get("error") == "partition" |
|
|