diff --git a/nexus/multi_user_bridge.py b/nexus/multi_user_bridge.py index 20f9d41b..535a4262 100644 --- a/nexus/multi_user_bridge.py +++ b/nexus/multi_user_bridge.py @@ -532,7 +532,7 @@ class MultiUserBridge: other_session.room_events.append(broadcast) return f'You say: \"{speech}\"' - if msg_lower.startswith("go ") or msg_lower.startswith("move "): + if msg_lower.startswith("go ") or msg_lower.startswith("move ") or msg_lower == "go" or msg_lower == "move": # Move to a new room (HTTP equivalent of WS move) parts = message.split(None, 1) if len(parts) < 2 or not parts[1].strip(): diff --git a/tests/test_multi_user_bridge.py b/tests/test_multi_user_bridge.py index 09ce0bd2..8cacfd74 100644 --- a/tests/test_multi_user_bridge.py +++ b/tests/test_multi_user_bridge.py @@ -480,3 +480,184 @@ class TestRateLimitingHTTP: "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"])