Files
the-nexus/tests/test_multi_user_bridge.py
Alexander Whitestone 6c0f7017a1 test: add 8 new tests for whisper and inventory commands
- whisper: delivery, privacy (no third-party leak), cross-room, nonexistent target, self-rejection, missing message
- inventory: stub output, short alias 'i'
2026-04-13 16:18:46 -04:00

808 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for the multi-user AI bridge — session isolation, crisis detection, HTTP endpoints."""
import asyncio
import json
import time
import pytest
from nexus.multi_user_bridge import (
CRISIS_988_MESSAGE,
CrisisState,
MultiUserBridge,
SessionManager,
UserSession,
)
# ── Session Isolation ─────────────────────────────────────────
class TestSessionIsolation:
def test_separate_users_have_independent_history(self):
mgr = SessionManager()
s1 = mgr.get_or_create("alice", "Alice", "Tower")
s2 = mgr.get_or_create("bob", "Bob", "Tower")
s1.add_message("user", "hello from alice")
s2.add_message("user", "hello from bob")
assert len(s1.message_history) == 1
assert len(s2.message_history) == 1
assert s1.message_history[0]["content"] == "hello from alice"
assert s2.message_history[0]["content"] == "hello from bob"
def test_same_user_reuses_session(self):
mgr = SessionManager()
s1 = mgr.get_or_create("alice", "Alice", "Tower")
s1.add_message("user", "msg1")
s2 = mgr.get_or_create("alice", "Alice", "Tower")
s2.add_message("user", "msg2")
assert s1 is s2
assert len(s1.message_history) == 2
def test_room_transitions_track_occupants(self):
mgr = SessionManager()
mgr.get_or_create("alice", "Alice", "Tower")
mgr.get_or_create("bob", "Bob", "Tower")
assert set(mgr.get_room_occupants("Tower")) == {"alice", "bob"}
# Alice moves
mgr.get_or_create("alice", "Alice", "Chapel")
assert mgr.get_room_occupants("Tower") == ["bob"]
assert mgr.get_room_occupants("Chapel") == ["alice"]
def test_max_sessions_evicts_oldest(self):
mgr = SessionManager(max_sessions=2)
mgr.get_or_create("a", "A", "Tower")
time.sleep(0.01)
mgr.get_or_create("b", "B", "Tower")
time.sleep(0.01)
mgr.get_or_create("c", "C", "Tower")
assert mgr.get("a") is None # evicted
assert mgr.get("b") is not None
assert mgr.get("c") is not None
assert mgr.active_count == 2
def test_history_window(self):
s = UserSession(user_id="test", username="Test")
for i in range(30):
s.add_message("user", f"msg{i}")
assert len(s.message_history) == 30
recent = s.get_history(window=5)
assert len(recent) == 5
assert recent[-1]["content"] == "msg29"
def test_session_to_dict(self):
s = UserSession(user_id="alice", username="Alice", room="Chapel")
s.add_message("user", "hello")
d = s.to_dict()
assert d["user_id"] == "alice"
assert d["username"] == "Alice"
assert d["room"] == "Chapel"
assert d["message_count"] == 1
assert d["command_count"] == 1
# ── Crisis Detection ──────────────────────────────────────────
class TestCrisisDetection:
def test_no_crisis_on_normal_messages(self):
cs = CrisisState()
assert cs.check("hello world") is False
assert cs.check("how are you") is False
def test_crisis_triggers_after_3_turns(self):
cs = CrisisState()
assert cs.check("I want to die") is False # turn 1
assert cs.check("I want to die") is False # turn 2
assert cs.check("I want to die") is True # turn 3 -> deliver 988
def test_crisis_resets_on_normal_message(self):
cs = CrisisState()
cs.check("I want to die") # turn 1
cs.check("actually never mind") # resets
assert cs.turn_count == 0
assert cs.check("I want to die") is False # turn 1 again
def test_crisis_delivers_once_per_window(self):
cs = CrisisState()
cs.check("I want to die")
cs.check("I want to die")
assert cs.check("I want to die") is True # delivered
assert cs.check("I want to die") is False # already delivered
def test_crisis_pattern_variations(self):
cs = CrisisState()
assert cs.check("I want to kill myself") is False # flagged, turn 1
assert cs.check("I want to kill myself") is False # turn 2
assert cs.check("I want to kill myself") is True # turn 3
def test_crisis_expired_window_redelivers(self):
cs = CrisisState()
cs.CRISIS_WINDOW_SECONDS = 0.1
cs.check("I want to die")
cs.check("I want to die")
assert cs.check("I want to die") is True
time.sleep(0.15)
# New window — should redeliver after 1 turn since window expired
assert cs.check("I want to die") is True
def test_self_harm_pattern(self):
cs = CrisisState()
# Note: "self-harming" doesn't match (has trailing "ing"), "self-harm" does
assert cs.check("I've been doing self-harm") is False # turn 1
assert cs.check("self harm is getting worse") is False # turn 2
assert cs.check("I can't stop self-harm") is True # turn 3
# ── HTTP Endpoint Tests (requires aiohttp test client) ────────
@pytest.fixture
async def bridge_app():
bridge = MultiUserBridge()
app = bridge.create_app()
yield app, bridge
@pytest.fixture
async def client(bridge_app):
from aiohttp.test_utils import TestClient, TestServer
app, bridge = bridge_app
async with TestClient(TestServer(app)) as client:
yield client, bridge
class TestHTTPEndpoints:
@pytest.mark.asyncio
async def test_health_endpoint(self, client):
c, bridge = client
resp = await c.get("/bridge/health")
data = await resp.json()
assert data["status"] == "ok"
assert data["active_sessions"] == 0
@pytest.mark.asyncio
async def test_chat_creates_session(self, client):
c, bridge = client
resp = await c.post("/bridge/chat", json={
"user_id": "alice",
"username": "Alice",
"message": "hello",
"room": "Tower",
})
data = await resp.json()
assert "response" in data
assert data["user_id"] == "alice"
assert data["room"] == "Tower"
assert data["session_messages"] == 2 # user + assistant
@pytest.mark.asyncio
async def test_chat_missing_user_id(self, client):
c, _ = client
resp = await c.post("/bridge/chat", json={"message": "hello"})
assert resp.status == 400
@pytest.mark.asyncio
async def test_chat_missing_message(self, client):
c, _ = client
resp = await c.post("/bridge/chat", json={"user_id": "alice"})
assert resp.status == 400
@pytest.mark.asyncio
async def test_sessions_list(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hey", "room": "Chapel"
})
resp = await c.get("/bridge/sessions")
data = await resp.json()
assert data["total"] == 2
user_ids = {s["user_id"] for s in data["sessions"]}
assert user_ids == {"alice", "bob"}
@pytest.mark.asyncio
async def test_look_command_returns_occupants(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hey", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "look", "room": "Tower"
})
data = await resp.json()
assert "bob" in data["response"].lower() or "bob" in str(data.get("room_occupants", []))
@pytest.mark.asyncio
async def test_room_occupants_tracked(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hey", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "look", "room": "Tower"
})
data = await resp.json()
assert set(data["room_occupants"]) == {"alice", "bob"}
@pytest.mark.asyncio
async def test_crisis_detection_returns_flag(self, client):
c, _ = client
for i in range(3):
resp = await c.post("/bridge/chat", json={
"user_id": "user1",
"message": "I want to die",
})
data = await resp.json()
assert data["crisis_detected"] is True
assert "988" in data["response"]
@pytest.mark.asyncio
async def test_concurrent_users_independent_responses(self, client):
c, _ = client
r1 = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "I love cats"
})
r2 = await c.post("/bridge/chat", json={
"user_id": "bob", "message": "I love dogs"
})
d1 = await r1.json()
d2 = await r2.json()
# Each user's response references their own message
assert "cats" in d1["response"].lower() or d1["user_id"] == "alice"
assert "dogs" in d2["response"].lower() or d2["user_id"] == "bob"
assert d1["user_id"] != d2["user_id"]
# ── Room Broadcast Tests ─────────────────────────────────────
class TestRoomBroadcast:
@pytest.mark.asyncio
async def test_say_broadcasts_to_room_occupants(self, client):
c, _ = client
# Position both users in the same room
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
# Alice says something
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "say Hello everyone!", "room": "Tower"
})
# Bob should have a pending room event
resp = await c.get("/bridge/room_events/bob")
data = await resp.json()
assert data["count"] >= 1
assert any("Alice" in e.get("message", "") for e in data["events"])
@pytest.mark.asyncio
async def test_say_does_not_echo_to_speaker(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "message": 'say Hello!', "room": "Tower"
})
# Alice should NOT have room events from herself
resp = await c.get("/bridge/room_events/alice")
data = await resp.json()
alice_events = [e for e in data["events"] if e.get("from_user") == "alice"]
assert len(alice_events) == 0
@pytest.mark.asyncio
async def test_say_no_broadcast_to_different_room(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hi", "room": "Chapel"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "message": 'say Hello!', "room": "Tower"
})
# Bob is in Chapel, shouldn't get Tower broadcasts
resp = await c.get("/bridge/room_events/bob")
data = await resp.json()
assert data["count"] == 0
@pytest.mark.asyncio
async def test_room_events_drain_after_read(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "message": 'say First!', "room": "Tower"
})
# First read drains
resp = await c.get("/bridge/room_events/bob")
data = await resp.json()
assert data["count"] >= 1
# Second read is empty
resp2 = await c.get("/bridge/room_events/bob")
data2 = await resp2.json()
assert data2["count"] == 0
@pytest.mark.asyncio
async def test_room_events_404_for_unknown_user(self, client):
c, _ = client
resp = await c.get("/bridge/room_events/nonexistent")
assert resp.status == 404
@pytest.mark.asyncio
async def test_rooms_lists_all_rooms_with_occupants(self, client):
c, bridge = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "carol", "username": "Carol", "message": "hi", "room": "Library"
})
resp = await c.get("/bridge/rooms")
assert resp.status == 200
data = await resp.json()
assert data["total_rooms"] == 2
assert data["total_users"] == 3
assert "Tower" in data["rooms"]
assert "Library" in data["rooms"]
assert data["rooms"]["Tower"]["count"] == 2
assert data["rooms"]["Library"]["count"] == 1
tower_users = {o["user_id"] for o in data["rooms"]["Tower"]["occupants"]}
assert tower_users == {"alice", "bob"}
@pytest.mark.asyncio
async def test_rooms_empty_when_no_sessions(self, client):
c, _ = client
resp = await c.get("/bridge/rooms")
data = await resp.json()
assert data["total_rooms"] == 0
assert data["total_users"] == 0
assert data["rooms"] == {}
# ── Rate Limiting Tests ──────────────────────────────────────
@pytest.fixture
async def rate_limited_client():
"""Bridge with very low rate limit for testing."""
from aiohttp.test_utils import TestClient, TestServer
bridge = MultiUserBridge(rate_limit=3, rate_window=60.0)
app = bridge.create_app()
async with TestClient(TestServer(app)) as client:
yield client, bridge
class TestRateLimitingHTTP:
@pytest.mark.asyncio
async def test_allowed_within_limit(self, rate_limited_client):
c, _ = rate_limited_client
for i in range(3):
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": f"msg {i}",
})
assert resp.status == 200
@pytest.mark.asyncio
async def test_returns_429_on_exceed(self, rate_limited_client):
c, _ = rate_limited_client
for i in range(3):
await c.post("/bridge/chat", json={
"user_id": "alice", "message": f"msg {i}",
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "one too many",
})
assert resp.status == 429
data = await resp.json()
assert "rate limit" in data["error"].lower()
@pytest.mark.asyncio
async def test_rate_limit_headers_on_success(self, rate_limited_client):
c, _ = rate_limited_client
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hello",
})
assert resp.status == 200
assert "X-RateLimit-Limit" in resp.headers
assert "X-RateLimit-Remaining" in resp.headers
assert resp.headers["X-RateLimit-Limit"] == "3"
assert resp.headers["X-RateLimit-Remaining"] == "2"
@pytest.mark.asyncio
async def test_rate_limit_headers_on_reject(self, rate_limited_client):
c, _ = rate_limited_client
for _ in range(3):
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "msg",
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "excess",
})
assert resp.status == 429
assert resp.headers.get("Retry-After") == "1"
assert resp.headers.get("X-RateLimit-Remaining") == "0"
@pytest.mark.asyncio
async def test_rate_limit_is_per_user(self, rate_limited_client):
c, _ = rate_limited_client
# Exhaust alice
for _ in range(3):
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "msg",
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "blocked",
})
assert resp.status == 429
# Bob should still work
resp2 = await c.post("/bridge/chat", json={
"user_id": "bob", "message": "im fine",
})
assert resp2.status == 200
# ── Stats Endpoint Tests ─────────────────────────────────────
class TestStatsEndpoint:
@pytest.mark.asyncio
async def test_stats_empty_bridge(self, client):
c, _ = client
resp = await c.get("/bridge/stats")
assert resp.status == 200
data = await resp.json()
assert data["active_sessions"] == 0
assert data["total_messages"] == 0
assert data["total_commands"] == 0
assert data["room_count"] == 0
assert data["ws_connections"] == 0
assert "uptime_seconds" in data
@pytest.mark.asyncio
async def test_stats_after_activity(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hey", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "look", "room": "Tower"
})
resp = await c.get("/bridge/stats")
data = await resp.json()
assert data["active_sessions"] == 2
assert data["total_messages"] == 6 # 3 chats × 2 (user + assistant) = 6
assert data["room_count"] == 1
assert "Tower" in data["rooms"]
# ── Go Command Tests ─────────────────────────────────────────
class TestGoCommand:
@pytest.mark.asyncio
async def test_go_changes_room(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "go Chapel", "room": "Tower"
})
data = await resp.json()
assert "Chapel" in data["response"]
assert data["room"] == "Chapel"
@pytest.mark.asyncio
async def test_go_updates_room_occupants(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hi", "room": "Tower"
})
# Alice moves to Chapel
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "go Chapel", "room": "Tower"
})
# Tower should only have bob
resp = await c.get("/bridge/rooms")
data = await resp.json()
tower_users = {o["user_id"] for o in data["rooms"]["Tower"]["occupants"]}
assert tower_users == {"bob"}
@pytest.mark.asyncio
async def test_go_notifies_old_room(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
# Alice leaves Tower
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "go Chapel", "room": "Tower"
})
# Bob should get a room event about Alice leaving
resp = await c.get("/bridge/room_events/bob")
data = await resp.json()
assert data["count"] >= 1
assert any("Alice" in e.get("message", "") and "Chapel" in e.get("message", "") for e in data["events"])
@pytest.mark.asyncio
async def test_go_same_room_rejected(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "go Tower", "room": "Tower"
})
data = await resp.json()
assert "already" in data["response"].lower()
@pytest.mark.asyncio
async def test_go_no_room_given(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "go", "room": "Tower"
})
data = await resp.json()
assert "usage" in data["response"].lower()
# ── Emote Command Tests ──────────────────────────────────────
class TestEmoteCommand:
@pytest.mark.asyncio
async def test_emote_broadcasts_to_room(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "emote waves hello", "room": "Tower"
})
resp = await c.get("/bridge/room_events/bob")
data = await resp.json()
assert data["count"] >= 1
assert any("Alice waves hello" in e.get("message", "") for e in data["events"])
@pytest.mark.asyncio
async def test_emote_returns_first_person(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "emote dances wildly", "room": "Tower"
})
data = await resp.json()
assert "dances wildly" in data["response"]
assert "Alice" not in data["response"] # first person, no username
@pytest.mark.asyncio
async def test_emote_no_echo_to_self(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "emote sits down", "room": "Tower"
})
resp = await c.get("/bridge/room_events/alice")
data = await resp.json()
assert data["count"] == 0
@pytest.mark.asyncio
async def test_slash_me_alias(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "/me stretches", "room": "Tower"
})
resp = await c.get("/bridge/room_events/bob")
data = await resp.json()
assert any("Alice stretches" in e.get("message", "") for e in data["events"])
# ── Whisper Command Tests ──────────────────────────────────────
class TestWhisperCommand:
@pytest.mark.asyncio
async def test_whisper_delivers_to_target(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
# Alice whispers to Bob
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice",
"message": "whisper bob secret meeting at midnight",
"room": "Tower"
})
data = await resp.json()
assert "Bob" in data["response"]
assert "secret meeting" in data["response"]
# Bob should see the whisper
resp2 = await c.get("/bridge/room_events/bob")
data2 = await resp2.json()
assert data2["count"] >= 1
whisper_events = [e for e in data2["events"] if e.get("type") == "whisper"]
assert len(whisper_events) >= 1
assert "Alice" in whisper_events[0]["message"]
assert "secret meeting" in whisper_events[0]["message"]
@pytest.mark.asyncio
async def test_whisper_not_visible_to_third_party(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "carol", "username": "Carol", "message": "hi", "room": "Tower"
})
# Alice whispers to Bob
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "whisper bob secret", "room": "Tower"
})
# Carol should NOT see the whisper
resp = await c.get("/bridge/room_events/carol")
data = await resp.json()
whisper_events = [e for e in data["events"] if e.get("type") == "whisper"]
assert len(whisper_events) == 0
@pytest.mark.asyncio
async def test_whisper_cross_room(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "username": "Alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "username": "Bob", "message": "hi", "room": "Chapel"
})
# Alice in Tower whispers to Bob in Chapel (cross-room works!)
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "whisper bob come to the tower", "room": "Tower"
})
data = await resp.json()
assert "Bob" in data["response"]
resp2 = await c.get("/bridge/room_events/bob")
data2 = await resp2.json()
whisper_events = [e for e in data2["events"] if e.get("type") == "whisper"]
assert len(whisper_events) >= 1
@pytest.mark.asyncio
async def test_whisper_to_nonexistent_user(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "whisper nobody hello", "room": "Tower"
})
data = await resp.json()
assert "not online" in data["response"].lower()
@pytest.mark.asyncio
async def test_whisper_to_self_rejected(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "whisper alice hello me", "room": "Tower"
})
data = await resp.json()
assert "yourself" in data["response"].lower()
@pytest.mark.asyncio
async def test_whisper_missing_message(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
await c.post("/bridge/chat", json={
"user_id": "bob", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "whisper bob", "room": "Tower"
})
data = await resp.json()
assert "usage" in data["response"].lower()
# ── Inventory Command Tests ────────────────────────────────────
class TestInventoryCommand:
@pytest.mark.asyncio
async def test_inventory_returns_stub(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "inventory", "room": "Tower"
})
data = await resp.json()
assert "pockets" in data["response"].lower() or "inventory" in data["response"].lower()
@pytest.mark.asyncio
async def test_inventory_short_alias(self, client):
c, _ = client
await c.post("/bridge/chat", json={
"user_id": "alice", "message": "hi", "room": "Tower"
})
resp = await c.post("/bridge/chat", json={
"user_id": "alice", "message": "i", "room": "Tower"
})
data = await resp.json()
assert "pockets" in data["response"].lower() or "inventory" in data["response"].lower()