// Round-trip tests for the generated TS FlatBuffers bindings under // `src/proto/galaxy/fbs/lobby/`. These guard against codegen drift — // if the wire schema and the bindings disagree, the round-trip fails // instead of letting a broken binding ship silently. import { Builder, ByteBuffer } from "flatbuffers"; import { describe, expect, test } from "vitest"; import { ApplicationSubmitRequest, ApplicationSubmitResponse, ApplicationSummary, ErrorBody, ErrorResponse, GameCreateRequest, GameCreateResponse, GameSummary, InviteDeclineRequest, InviteDeclineResponse, InviteRedeemRequest, InviteRedeemResponse, InviteSummary, MyApplicationsListRequest, MyApplicationsListResponse, MyGamesListRequest, MyGamesListResponse, MyInvitesListRequest, MyInvitesListResponse, OpenEnrollmentRequest, OpenEnrollmentResponse, PublicGamesListRequest, PublicGamesListResponse, } from "../src/proto/galaxy/fbs/lobby"; interface GameSummaryFixture { gameId: string; gameName: string; gameType: string; status: string; ownerUserId: string; minPlayers: number; maxPlayers: number; enrollmentEndsAtMs: bigint; createdAtMs: bigint; updatedAtMs: bigint; } const PRIVATE_GAME: GameSummaryFixture = { gameId: "game-private-7c8f", gameName: "First Contact", gameType: "private", status: "draft", ownerUserId: "user-9912", minPlayers: 2, maxPlayers: 8, enrollmentEndsAtMs: 1_780_000_000_000n, createdAtMs: 1_770_000_000_000n, updatedAtMs: 1_770_000_300_000n, }; const PUBLIC_GAME: GameSummaryFixture = { gameId: "game-public-aabb", gameName: "Open Lobby", gameType: "public", status: "enrollment_open", ownerUserId: "", minPlayers: 4, maxPlayers: 12, enrollmentEndsAtMs: 1_780_500_000_000n, createdAtMs: 1_770_500_000_000n, updatedAtMs: 1_770_600_000_000n, }; function encodeGameSummary(builder: Builder, value: GameSummaryFixture): number { const gameId = builder.createString(value.gameId); const gameName = builder.createString(value.gameName); const gameType = builder.createString(value.gameType); const status = builder.createString(value.status); const ownerUserId = builder.createString(value.ownerUserId); GameSummary.startGameSummary(builder); GameSummary.addGameId(builder, gameId); GameSummary.addGameName(builder, gameName); GameSummary.addGameType(builder, gameType); GameSummary.addStatus(builder, status); GameSummary.addOwnerUserId(builder, ownerUserId); GameSummary.addMinPlayers(builder, value.minPlayers); GameSummary.addMaxPlayers(builder, value.maxPlayers); GameSummary.addEnrollmentEndsAtMs(builder, value.enrollmentEndsAtMs); GameSummary.addCreatedAtMs(builder, value.createdAtMs); GameSummary.addUpdatedAtMs(builder, value.updatedAtMs); return GameSummary.endGameSummary(builder); } function expectGameSummary(actual: GameSummary | null, want: GameSummaryFixture): void { expect(actual).not.toBeNull(); const got = actual!; expect(got.gameId()).toBe(want.gameId); expect(got.gameName()).toBe(want.gameName); expect(got.gameType()).toBe(want.gameType); expect(got.status()).toBe(want.status); expect(got.ownerUserId()).toBe(want.ownerUserId); expect(got.minPlayers()).toBe(want.minPlayers); expect(got.maxPlayers()).toBe(want.maxPlayers); expect(got.enrollmentEndsAtMs()).toBe(want.enrollmentEndsAtMs); expect(got.createdAtMs()).toBe(want.createdAtMs); expect(got.updatedAtMs()).toBe(want.updatedAtMs); } describe("lobby FlatBuffers TS bindings", () => { test("MyGamesListRequest round-trips an empty body", () => { const builder = new Builder(32); MyGamesListRequest.startMyGamesListRequest(builder); builder.finish(MyGamesListRequest.endMyGamesListRequest(builder)); const bytes = builder.asUint8Array(); const decoded = MyGamesListRequest.getRootAsMyGamesListRequest(new ByteBuffer(bytes)); expect(decoded).toBeDefined(); }); test("MyGamesListResponse encodes and decodes multiple summaries", () => { const builder = new Builder(512); const item0 = encodeGameSummary(builder, PRIVATE_GAME); const item1 = encodeGameSummary(builder, PUBLIC_GAME); const items = MyGamesListResponse.createItemsVector(builder, [item0, item1]); MyGamesListResponse.startMyGamesListResponse(builder); MyGamesListResponse.addItems(builder, items); builder.finish(MyGamesListResponse.endMyGamesListResponse(builder)); const bytes = builder.asUint8Array(); const decoded = MyGamesListResponse.getRootAsMyGamesListResponse(new ByteBuffer(bytes)); expect(decoded.itemsLength()).toBe(2); expectGameSummary(decoded.items(0), PRIVATE_GAME); expectGameSummary(decoded.items(1), PUBLIC_GAME); }); test("PublicGamesListResponse preserves pagination metadata", () => { const builder = new Builder(256); const item = encodeGameSummary(builder, PUBLIC_GAME); const items = PublicGamesListResponse.createItemsVector(builder, [item]); PublicGamesListResponse.startPublicGamesListResponse(builder); PublicGamesListResponse.addItems(builder, items); PublicGamesListResponse.addPage(builder, 3); PublicGamesListResponse.addPageSize(builder, 25); PublicGamesListResponse.addTotal(builder, 51); builder.finish(PublicGamesListResponse.endPublicGamesListResponse(builder)); const bytes = builder.asUint8Array(); const decoded = PublicGamesListResponse.getRootAsPublicGamesListResponse( new ByteBuffer(bytes), ); expect(decoded.itemsLength()).toBe(1); expectGameSummary(decoded.items(0), PUBLIC_GAME); expect(decoded.page()).toBe(3); expect(decoded.pageSize()).toBe(25); expect(decoded.total()).toBe(51); }); test("PublicGamesListRequest round-trips page numbers", () => { const builder = new Builder(32); PublicGamesListRequest.startPublicGamesListRequest(builder); PublicGamesListRequest.addPage(builder, 2); PublicGamesListRequest.addPageSize(builder, 10); builder.finish(PublicGamesListRequest.endPublicGamesListRequest(builder)); const decoded = PublicGamesListRequest.getRootAsPublicGamesListRequest( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.page()).toBe(2); expect(decoded.pageSize()).toBe(10); }); test("ApplicationSummary preserves pending and decided records", () => { const builder = new Builder(256); const pendingId = builder.createString("app-1"); const pendingGameId = builder.createString("public-1"); const pendingApplicant = builder.createString("user-1"); const pendingRace = builder.createString("Vegan Federation"); const pendingStatus = builder.createString("pending"); ApplicationSummary.startApplicationSummary(builder); ApplicationSummary.addApplicationId(builder, pendingId); ApplicationSummary.addGameId(builder, pendingGameId); ApplicationSummary.addApplicantUserId(builder, pendingApplicant); ApplicationSummary.addRaceName(builder, pendingRace); ApplicationSummary.addStatus(builder, pendingStatus); ApplicationSummary.addCreatedAtMs(builder, 1_770_000_000_000n); ApplicationSummary.addDecidedAtMs(builder, 0n); const pending = ApplicationSummary.endApplicationSummary(builder); const approvedId = builder.createString("app-2"); const approvedGameId = builder.createString("public-2"); const approvedApplicant = builder.createString("user-1"); const approvedRace = builder.createString("Lithic Compact"); const approvedStatus = builder.createString("approved"); ApplicationSummary.startApplicationSummary(builder); ApplicationSummary.addApplicationId(builder, approvedId); ApplicationSummary.addGameId(builder, approvedGameId); ApplicationSummary.addApplicantUserId(builder, approvedApplicant); ApplicationSummary.addRaceName(builder, approvedRace); ApplicationSummary.addStatus(builder, approvedStatus); ApplicationSummary.addCreatedAtMs(builder, 1_770_000_000_000n); ApplicationSummary.addDecidedAtMs(builder, 1_770_010_000_000n); const approved = ApplicationSummary.endApplicationSummary(builder); const items = MyApplicationsListResponse.createItemsVector(builder, [pending, approved]); MyApplicationsListResponse.startMyApplicationsListResponse(builder); MyApplicationsListResponse.addItems(builder, items); builder.finish(MyApplicationsListResponse.endMyApplicationsListResponse(builder)); const decoded = MyApplicationsListResponse.getRootAsMyApplicationsListResponse( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.itemsLength()).toBe(2); const first = decoded.items(0)!; expect(first.status()).toBe("pending"); expect(first.decidedAtMs()).toBe(0n); const second = decoded.items(1)!; expect(second.status()).toBe("approved"); expect(second.decidedAtMs()).toBe(1_770_010_000_000n); }); test("MyApplicationsListRequest round-trips an empty body", () => { const builder = new Builder(32); MyApplicationsListRequest.startMyApplicationsListRequest(builder); builder.finish(MyApplicationsListRequest.endMyApplicationsListRequest(builder)); const decoded = MyApplicationsListRequest.getRootAsMyApplicationsListRequest( new ByteBuffer(builder.asUint8Array()), ); expect(decoded).toBeDefined(); }); test("InviteSummary preserves invited_user_id and code fields", () => { const builder = new Builder(256); const userBoundId = builder.createString("invite-user-bound"); const userBoundGame = builder.createString("private-1"); const userBoundInviter = builder.createString("user-host"); const userBoundInvited = builder.createString("user-1"); const userBoundCode = builder.createString(""); const userBoundRace = builder.createString("Vegan Federation"); const userBoundStatus = builder.createString("pending"); InviteSummary.startInviteSummary(builder); InviteSummary.addInviteId(builder, userBoundId); InviteSummary.addGameId(builder, userBoundGame); InviteSummary.addInviterUserId(builder, userBoundInviter); InviteSummary.addInvitedUserId(builder, userBoundInvited); InviteSummary.addCode(builder, userBoundCode); InviteSummary.addRaceName(builder, userBoundRace); InviteSummary.addStatus(builder, userBoundStatus); InviteSummary.addCreatedAtMs(builder, 1_770_000_000_000n); InviteSummary.addExpiresAtMs(builder, 1_780_000_000_000n); InviteSummary.addDecidedAtMs(builder, 0n); const userBound = InviteSummary.endInviteSummary(builder); const codeBasedId = builder.createString("invite-code-based"); const codeBasedGame = builder.createString("private-2"); const codeBasedInviter = builder.createString("user-host"); const codeBasedInvited = builder.createString(""); const codeBasedCode = builder.createString("ABCDEF12"); const codeBasedRace = builder.createString("Lithic Compact"); const codeBasedStatus = builder.createString("pending"); InviteSummary.startInviteSummary(builder); InviteSummary.addInviteId(builder, codeBasedId); InviteSummary.addGameId(builder, codeBasedGame); InviteSummary.addInviterUserId(builder, codeBasedInviter); InviteSummary.addInvitedUserId(builder, codeBasedInvited); InviteSummary.addCode(builder, codeBasedCode); InviteSummary.addRaceName(builder, codeBasedRace); InviteSummary.addStatus(builder, codeBasedStatus); InviteSummary.addCreatedAtMs(builder, 1_770_000_000_000n); InviteSummary.addExpiresAtMs(builder, 1_780_000_000_000n); InviteSummary.addDecidedAtMs(builder, 0n); const codeBased = InviteSummary.endInviteSummary(builder); const items = MyInvitesListResponse.createItemsVector(builder, [userBound, codeBased]); MyInvitesListResponse.startMyInvitesListResponse(builder); MyInvitesListResponse.addItems(builder, items); builder.finish(MyInvitesListResponse.endMyInvitesListResponse(builder)); const decoded = MyInvitesListResponse.getRootAsMyInvitesListResponse( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.itemsLength()).toBe(2); const first = decoded.items(0)!; expect(first.invitedUserId()).toBe("user-1"); expect(first.code()).toBe(""); const second = decoded.items(1)!; expect(second.invitedUserId()).toBe(""); expect(second.code()).toBe("ABCDEF12"); }); test("MyInvitesListRequest round-trips an empty body", () => { const builder = new Builder(32); MyInvitesListRequest.startMyInvitesListRequest(builder); builder.finish(MyInvitesListRequest.endMyInvitesListRequest(builder)); const decoded = MyInvitesListRequest.getRootAsMyInvitesListRequest( new ByteBuffer(builder.asUint8Array()), ); expect(decoded).toBeDefined(); }); test("OpenEnrollmentRequest and Response round-trip", () => { const builder = new Builder(64); const gameId = builder.createString("game-private-7c8f"); OpenEnrollmentRequest.startOpenEnrollmentRequest(builder); OpenEnrollmentRequest.addGameId(builder, gameId); builder.finish(OpenEnrollmentRequest.endOpenEnrollmentRequest(builder)); const reqDecoded = OpenEnrollmentRequest.getRootAsOpenEnrollmentRequest( new ByteBuffer(builder.asUint8Array()), ); expect(reqDecoded.gameId()).toBe("game-private-7c8f"); const respBuilder = new Builder(64); const respGameId = respBuilder.createString("game-private-7c8f"); const status = respBuilder.createString("enrollment_open"); OpenEnrollmentResponse.startOpenEnrollmentResponse(respBuilder); OpenEnrollmentResponse.addGameId(respBuilder, respGameId); OpenEnrollmentResponse.addStatus(respBuilder, status); respBuilder.finish(OpenEnrollmentResponse.endOpenEnrollmentResponse(respBuilder)); const respDecoded = OpenEnrollmentResponse.getRootAsOpenEnrollmentResponse( new ByteBuffer(respBuilder.asUint8Array()), ); expect(respDecoded.gameId()).toBe("game-private-7c8f"); expect(respDecoded.status()).toBe("enrollment_open"); }); test("GameCreateRequest and Response round-trip", () => { const builder = new Builder(256); const name = builder.createString("First Contact"); const description = builder.createString(""); const turnSchedule = builder.createString("0 0 * * *"); const targetVersion = builder.createString("v1"); GameCreateRequest.startGameCreateRequest(builder); GameCreateRequest.addGameName(builder, name); GameCreateRequest.addDescription(builder, description); GameCreateRequest.addMinPlayers(builder, 2); GameCreateRequest.addMaxPlayers(builder, 8); GameCreateRequest.addStartGapHours(builder, 24); GameCreateRequest.addStartGapPlayers(builder, 2); GameCreateRequest.addEnrollmentEndsAtMs(builder, 1_780_000_000_000n); GameCreateRequest.addTurnSchedule(builder, turnSchedule); GameCreateRequest.addTargetEngineVersion(builder, targetVersion); builder.finish(GameCreateRequest.endGameCreateRequest(builder)); const reqDecoded = GameCreateRequest.getRootAsGameCreateRequest( new ByteBuffer(builder.asUint8Array()), ); expect(reqDecoded.gameName()).toBe("First Contact"); expect(reqDecoded.minPlayers()).toBe(2); expect(reqDecoded.maxPlayers()).toBe(8); expect(reqDecoded.turnSchedule()).toBe("0 0 * * *"); expect(reqDecoded.targetEngineVersion()).toBe("v1"); expect(reqDecoded.enrollmentEndsAtMs()).toBe(1_780_000_000_000n); const respBuilder = new Builder(256); const game = encodeGameSummary(respBuilder, PRIVATE_GAME); GameCreateResponse.startGameCreateResponse(respBuilder); GameCreateResponse.addGame(respBuilder, game); respBuilder.finish(GameCreateResponse.endGameCreateResponse(respBuilder)); const respDecoded = GameCreateResponse.getRootAsGameCreateResponse( new ByteBuffer(respBuilder.asUint8Array()), ); expectGameSummary(respDecoded.game(), PRIVATE_GAME); }); test("ApplicationSubmitRequest and Response round-trip", () => { const builder = new Builder(128); const gameId = builder.createString("public-1"); const raceName = builder.createString("Vegan Federation"); ApplicationSubmitRequest.startApplicationSubmitRequest(builder); ApplicationSubmitRequest.addGameId(builder, gameId); ApplicationSubmitRequest.addRaceName(builder, raceName); builder.finish(ApplicationSubmitRequest.endApplicationSubmitRequest(builder)); const reqDecoded = ApplicationSubmitRequest.getRootAsApplicationSubmitRequest( new ByteBuffer(builder.asUint8Array()), ); expect(reqDecoded.gameId()).toBe("public-1"); expect(reqDecoded.raceName()).toBe("Vegan Federation"); const respBuilder = new Builder(128); const appId = respBuilder.createString("app-3"); const appGameId = respBuilder.createString("public-1"); const applicant = respBuilder.createString("user-1"); const race = respBuilder.createString("Vegan Federation"); const status = respBuilder.createString("pending"); ApplicationSummary.startApplicationSummary(respBuilder); ApplicationSummary.addApplicationId(respBuilder, appId); ApplicationSummary.addGameId(respBuilder, appGameId); ApplicationSummary.addApplicantUserId(respBuilder, applicant); ApplicationSummary.addRaceName(respBuilder, race); ApplicationSummary.addStatus(respBuilder, status); ApplicationSummary.addCreatedAtMs(respBuilder, 1_770_000_000_000n); ApplicationSummary.addDecidedAtMs(respBuilder, 0n); const app = ApplicationSummary.endApplicationSummary(respBuilder); ApplicationSubmitResponse.startApplicationSubmitResponse(respBuilder); ApplicationSubmitResponse.addApplication(respBuilder, app); respBuilder.finish(ApplicationSubmitResponse.endApplicationSubmitResponse(respBuilder)); const respDecoded = ApplicationSubmitResponse.getRootAsApplicationSubmitResponse( new ByteBuffer(respBuilder.asUint8Array()), ); const application = respDecoded.application(); expect(application).not.toBeNull(); expect(application!.applicationId()).toBe("app-3"); expect(application!.status()).toBe("pending"); }); test("InviteRedeem and InviteDecline requests round-trip", () => { for (const ctor of [InviteRedeemRequest, InviteDeclineRequest] as const) { const builder = new Builder(128); const gameId = builder.createString("private-1"); const inviteId = builder.createString("invite-1"); if (ctor === InviteRedeemRequest) { InviteRedeemRequest.startInviteRedeemRequest(builder); InviteRedeemRequest.addGameId(builder, gameId); InviteRedeemRequest.addInviteId(builder, inviteId); builder.finish(InviteRedeemRequest.endInviteRedeemRequest(builder)); const decoded = InviteRedeemRequest.getRootAsInviteRedeemRequest( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.gameId()).toBe("private-1"); expect(decoded.inviteId()).toBe("invite-1"); } else { InviteDeclineRequest.startInviteDeclineRequest(builder); InviteDeclineRequest.addGameId(builder, gameId); InviteDeclineRequest.addInviteId(builder, inviteId); builder.finish(InviteDeclineRequest.endInviteDeclineRequest(builder)); const decoded = InviteDeclineRequest.getRootAsInviteDeclineRequest( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.gameId()).toBe("private-1"); expect(decoded.inviteId()).toBe("invite-1"); } } }); test("InviteRedeemResponse and InviteDeclineResponse carry an InviteSummary", () => { for (const status of ["accepted", "declined"]) { const builder = new Builder(128); const inviteId = builder.createString("invite-1"); const gameId = builder.createString("private-1"); const inviter = builder.createString("user-host"); const invited = builder.createString("user-1"); const code = builder.createString(""); const race = builder.createString("Vegan Federation"); const statusStr = builder.createString(status); InviteSummary.startInviteSummary(builder); InviteSummary.addInviteId(builder, inviteId); InviteSummary.addGameId(builder, gameId); InviteSummary.addInviterUserId(builder, inviter); InviteSummary.addInvitedUserId(builder, invited); InviteSummary.addCode(builder, code); InviteSummary.addRaceName(builder, race); InviteSummary.addStatus(builder, statusStr); InviteSummary.addCreatedAtMs(builder, 1_770_000_000_000n); InviteSummary.addExpiresAtMs(builder, 1_780_000_000_000n); InviteSummary.addDecidedAtMs(builder, 1_770_010_000_000n); const summary = InviteSummary.endInviteSummary(builder); if (status === "accepted") { InviteRedeemResponse.startInviteRedeemResponse(builder); InviteRedeemResponse.addInvite(builder, summary); builder.finish(InviteRedeemResponse.endInviteRedeemResponse(builder)); const decoded = InviteRedeemResponse.getRootAsInviteRedeemResponse( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.invite()?.status()).toBe("accepted"); } else { InviteDeclineResponse.startInviteDeclineResponse(builder); InviteDeclineResponse.addInvite(builder, summary); builder.finish(InviteDeclineResponse.endInviteDeclineResponse(builder)); const decoded = InviteDeclineResponse.getRootAsInviteDeclineResponse( new ByteBuffer(builder.asUint8Array()), ); expect(decoded.invite()?.status()).toBe("declined"); } } }); test("ErrorResponse round-trips a code/message pair", () => { const builder = new Builder(128); const code = builder.createString("conflict"); const message = builder.createString("request conflicts with current state"); ErrorBody.startErrorBody(builder); ErrorBody.addCode(builder, code); ErrorBody.addMessage(builder, message); const errorOff = ErrorBody.endErrorBody(builder); ErrorResponse.startErrorResponse(builder); ErrorResponse.addError(builder, errorOff); builder.finish(ErrorResponse.endErrorResponse(builder)); const decoded = ErrorResponse.getRootAsErrorResponse( new ByteBuffer(builder.asUint8Array()), ); const error = decoded.error(); expect(error).not.toBeNull(); expect(error!.code()).toBe("conflict"); expect(error!.message()).toBe("request conflicts with current state"); }); });