Some checks failed
Deploy to Production / test (push) Failing after 35s
Backend: - Fix session never being attached to ws.data at upgrade time - Add GameServer class: connection registry, per-connection room tracking, automatic room cleanup on disconnect via ws.data.rooms - Replace ws-handler.ts with typed event-driven architecture using mitt - Remove redundant subscription tracking from RoomManager - Add JOIN_RESULT with player/spectator lists replacing error-as-control-flow - Add SESSION_REPLACED for multi-tab same-account detection - Add FILL_ROOM command for admin solo testing (fills empty slots with host) - Fix dual-schema routing; remove game types from WsMessageSchema - Per-player personalized views sent directly after each action Chess plugin: - Allow same-player (solo) mode: skip color/turn ownership checks - Fix forfeit and disconnect handling in solo mode (winner: null) Frontend: - Click-to-move with legal move dots and last-move highlight - Auto-scroll move history, forfeit confirmation, turn-reactive board border - JOIN_RESULT initialises player/spectator lists immediately on join - Contextual connecting state, player slot cards in waiting room - Copy-invite button with Copied! flash, Back to Lobby CTA on finish - Session-replaced warning banner with Rejoin here action - Lobby passes preferAs intent through route state - Admin waiting room shows Start Solo Test button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
6.0 KiB
TypeScript
148 lines
6.0 KiB
TypeScript
import { describe, it, expect, beforeEach } from "bun:test";
|
|
import { RoomManager } from "./RoomManager";
|
|
import { gameRegistry } from "@shared/games/registry";
|
|
import { chessPlugin } from "@shared/games/chess/plugin";
|
|
|
|
// Register chess plugin for tests
|
|
if (!gameRegistry.get("chess")) {
|
|
gameRegistry.register(chessPlugin);
|
|
}
|
|
|
|
describe("RoomManager", () => {
|
|
let manager: RoomManager;
|
|
|
|
beforeEach(() => {
|
|
manager = new RoomManager();
|
|
});
|
|
|
|
describe("createRoom", () => {
|
|
it("should create a room and return its id", () => {
|
|
const result = manager.createRoom("chess", "player1");
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.roomId).toBeDefined();
|
|
expect(typeof result.roomId).toBe("string");
|
|
}
|
|
});
|
|
|
|
it("should reject unknown game type", () => {
|
|
const result = manager.createRoom("unknown-game", "player1");
|
|
expect(result.ok).toBe(false);
|
|
});
|
|
|
|
it("should add creator as first player", () => {
|
|
const result = manager.createRoom("chess", "player1");
|
|
if (result.ok) {
|
|
const room = manager.getRoom(result.roomId);
|
|
expect(room?.players).toContain("player1");
|
|
expect(room?.host).toBe("player1");
|
|
expect(room?.status).toBe("waiting");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("joinRoom", () => {
|
|
it("should add a player to a waiting room", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
const join = manager.joinRoom(create.roomId, "player2", "player");
|
|
expect(join.ok).toBe(true);
|
|
if (join.ok) {
|
|
expect(join.joinedAs).toBe("player");
|
|
}
|
|
});
|
|
|
|
it("should auto-start when room reaches maxPlayers", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.joinRoom(create.roomId, "player2", "player");
|
|
const room = manager.getRoom(create.roomId);
|
|
expect(room?.status).toBe("playing");
|
|
expect(room?.state).toBeDefined();
|
|
});
|
|
|
|
it("should allow joining as spectator when game is playing", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.joinRoom(create.roomId, "player2", "player");
|
|
const spec = manager.joinRoom(create.roomId, "spectator1", "spectator");
|
|
expect(spec.ok).toBe(true);
|
|
});
|
|
|
|
it("should downgrade to spectator when joining full room as player", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.joinRoom(create.roomId, "player2", "player");
|
|
const result = manager.joinRoom(create.roomId, "player3", "player");
|
|
expect(result.ok).toBe(true);
|
|
if (result.ok) {
|
|
expect(result.joinedAs).toBe("spectator");
|
|
}
|
|
});
|
|
|
|
it("should reject joining nonexistent room", () => {
|
|
const result = manager.joinRoom("fake-id", "player1", "player");
|
|
expect(result.ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("handleAction", () => {
|
|
it("should apply a valid game action", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.joinRoom(create.roomId, "player2", "player");
|
|
const result = manager.handleAction(create.roomId, "player1", { type: "move", from: "e2", to: "e4" });
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it("should reject action from spectator", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.joinRoom(create.roomId, "player2", "player");
|
|
manager.joinRoom(create.roomId, "spectator1", "spectator");
|
|
const result = manager.handleAction(create.roomId, "spectator1", { type: "move", from: "e2", to: "e4" });
|
|
expect(result.ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("leaveRoom", () => {
|
|
it("should remove a player from the room", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.leaveRoom(create.roomId, "player1");
|
|
// Room is deleted when last player leaves a waiting room
|
|
const room = manager.getRoom(create.roomId);
|
|
expect(room).toBeUndefined();
|
|
});
|
|
|
|
it("should remove a spectator from the room", () => {
|
|
const create = manager.createRoom("chess", "player1");
|
|
if (!create.ok) throw new Error("Failed to create room");
|
|
manager.joinRoom(create.roomId, "player2", "player");
|
|
manager.joinRoom(create.roomId, "spec1", "spectator");
|
|
manager.leaveRoom(create.roomId, "spec1");
|
|
const room = manager.getRoom(create.roomId);
|
|
expect(room?.spectators.has("spec1")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("listRooms", () => {
|
|
it("should return summaries of all rooms", () => {
|
|
manager.createRoom("chess", "player1");
|
|
manager.createRoom("chess", "player2");
|
|
const rooms = manager.listRooms();
|
|
expect(rooms.length).toBe(2);
|
|
expect(rooms[0].gameSlug).toBe("chess");
|
|
expect(rooms[0].status).toBe("waiting");
|
|
});
|
|
|
|
it("should filter by game type", () => {
|
|
manager.createRoom("chess", "player1");
|
|
const rooms = manager.listRooms("chess");
|
|
expect(rooms.length).toBe(1);
|
|
const empty = manager.listRooms("blackjack");
|
|
expect(empty.length).toBe(0);
|
|
});
|
|
});
|
|
});
|