HearthNet / tests /test_chat_cross_node.py
GitHub Actions
fix(chat): wire ChatService with bus in app.py so cross-node sends deliver
fa5b4a9
Raw
History Blame Contribute Delete
4.05 kB
"""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)
# Wire chat exactly like app.py: explicit bus= argument is required.
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") # no bus=
status = await svc._deliver_remote(
{"to": "node-bob", "from": "node-alice", "body": "x", "event_id": "e1"}
)
assert status == "queued"