| """Integration test: cross-node chat delivery (M10). |
| |
| Verifies that chat.send from node A to node B: |
| - calls chat.deliver on B's bus |
| - B stores the message |
| - chat.history on B shows the message |
| - delivery status is "delivered" (not "queued" or "direct") |
| |
| Uses in-process InMemoryTransport so no relay/HTTP needed. |
| """ |
| from __future__ import annotations |
|
|
| import pytest |
| from hearthnet.bus import InMemoryTransport |
|
|
|
|
| @pytest.fixture() |
| def two_nodes(): |
| from hearthnet.node import HearthNode |
|
|
| net = InMemoryTransport() |
| alice = HearthNode("node-alice", "Alice", "ed25519:test", transport=net) |
| alice.install_demo_services() |
| net.register(alice.bus) |
|
|
| bob = HearthNode("node-bob", "Bob", "ed25519:test", transport=net) |
| bob.install_demo_services() |
| net.register(bob.bus) |
|
|
| return alice, bob |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_chat_send_cross_node_delivers(two_nodes): |
| alice, bob = two_nodes |
|
|
| r = await alice.bus.call( |
| "chat.send", (1, 0), {"input": {"recipient": "node-bob", "body": "Hello Bob!"}} |
| ) |
| status = r.get("output", {}).get("delivered") |
| assert status == "delivered", f"Expected 'delivered', got {status!r}" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_chat_history_shows_received_message(two_nodes): |
| alice, bob = two_nodes |
|
|
| await alice.bus.call( |
| "chat.send", (1, 0), {"input": {"recipient": "node-bob", "body": "Hi from Alice"}} |
| ) |
|
|
| r = await bob.bus.call("chat.history", (1, 0), {"input": {"peer": "node-alice"}}) |
| msgs = r.get("output", {}).get("messages", []) |
| assert len(msgs) == 1, f"Expected 1 message in Bob's history, got {len(msgs)}" |
| assert msgs[0]["body"] == "Hi from Alice" |
| assert msgs[0]["from"] == "node-alice" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_self_send_returns_direct(two_nodes): |
| alice, _ = two_nodes |
| r = await alice.bus.call( |
| "chat.send", (1, 0), {"input": {"recipient": "node-alice", "body": "hi me"}} |
| ) |
| status = r.get("output", {}).get("delivered") |
| assert status == "direct", f"Expected 'direct' for self-send, got {status!r}" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_send_to_unknown_node_returns_queued(two_nodes): |
| alice, _ = two_nodes |
| r = await alice.bus.call( |
| "chat.send", (1, 0), {"input": {"recipient": "node-nobody", "body": "hello?"}} |
| ) |
| status = r.get("output", {}).get("delivered") |
| assert status == "queued", f"Expected 'queued' for unknown node, got {status!r}" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_manually_wired_chat_service_delivers(): |
| """Regression: a ChatService registered manually (as app.py / the HF Space |
| entry point does) must receive a ``bus=`` reference, otherwise |
| ``_deliver_remote`` short-circuits to ``"queued"`` before attempting |
| delivery. Mirrors the app.py wiring to guard against that regression. |
| """ |
| from hearthnet.node import HearthNode |
| from hearthnet.services.chat.service import ChatService |
|
|
| net = InMemoryTransport() |
| alice = HearthNode("node-alice", "Alice", "ed25519:test", transport=net) |
| |
| alice.bus.register_service(ChatService(alice.node_id, bus=alice.bus)) |
| net.register(alice.bus) |
|
|
| bob = HearthNode("node-bob", "Bob", "ed25519:test", transport=net) |
| bob.bus.register_service(ChatService(bob.node_id, bus=bob.bus)) |
| net.register(bob.bus) |
|
|
| r = await alice.bus.call( |
| "chat.send", (1, 0), {"input": {"recipient": "node-bob", "body": "wired"}} |
| ) |
| assert r.get("output", {}).get("delivered") == "delivered" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_chat_service_without_bus_cannot_deliver(): |
| """A ChatService constructed without a bus reference can only queue — |
| documents the failure mode the app.py wiring fix prevents. |
| """ |
| from hearthnet.services.chat.service import ChatService |
|
|
| svc = ChatService("node-alice") |
| status = await svc._deliver_remote( |
| {"to": "node-bob", "from": "node-alice", "body": "x", "event_id": "e1"} |
| ) |
| assert status == "queued" |
|
|
|
|