// Vitest unit coverage for `sync/submit.ts`. Drives the submit // pipeline against a stub `GalaxyClient` whose `executeCommand` // hand-builds FBS responses, so the parser is exercised against // payloads identical to what the real gateway returns. import { Builder } from "flatbuffers"; import { describe, expect, test, vi } from "vitest"; import type { GalaxyClient } from "../src/api/galaxy-client"; import { uuidToHiLo } from "../src/api/game-state"; import { UUID } from "../src/proto/galaxy/fbs/common"; import { CommandItem, CommandPlanetProduce, CommandPlanetRename, CommandPlanetRouteRemove, CommandPlanetRouteSet, CommandPayload, PlanetProduction, PlanetRouteLoadType, UserGamesOrder, UserGamesOrderResponse, } from "../src/proto/galaxy/fbs/order"; import { submitOrder } from "../src/sync/submit"; import type { CargoLoadType, OrderCommand, ProductionType, } from "../src/sync/order-types"; const GAME_ID = "11111111-2222-3333-4444-555555555555"; function mockClient( executeCommand: ( messageType: string, payload: Uint8Array, ) => Promise<{ resultCode: string; payloadBytes: Uint8Array }>, ): GalaxyClient { return { executeCommand } as unknown as GalaxyClient; } function buildResponse( commands: { id: string; applied: boolean | null; errorCode: number | null }[], updatedAt: number, ): Uint8Array { const builder = new Builder(256); const itemOffsets = commands.map((c) => { const cmdIdOffset = builder.createString(c.id); const nameOffset = builder.createString("ignored"); const payloadOffset = CommandPlanetRename.createCommandPlanetRename( builder, BigInt(0), nameOffset, ); CommandItem.startCommandItem(builder); CommandItem.addCmdId(builder, cmdIdOffset); if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied); if (c.errorCode !== null) { CommandItem.addCmdErrorCode(builder, BigInt(c.errorCode)); } CommandItem.addPayloadType(builder, CommandPayload.CommandPlanetRename); CommandItem.addPayload(builder, payloadOffset); return CommandItem.endCommandItem(builder); }); const commandsVec = UserGamesOrderResponse.createCommandsVector(builder, itemOffsets); const [hi, lo] = uuidToHiLo(GAME_ID); const gameIdOffset = UUID.createUUID(builder, hi, lo); UserGamesOrderResponse.startUserGamesOrderResponse(builder); UserGamesOrderResponse.addGameId(builder, gameIdOffset); UserGamesOrderResponse.addUpdatedAt(builder, BigInt(updatedAt)); UserGamesOrderResponse.addCommands(builder, commandsVec); const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder); builder.finish(offset); return builder.asUint8Array(); } const sampleRename: OrderCommand = { kind: "planetRename", id: "00000000-0000-0000-0000-00000000aaaa", planetNumber: 7, name: "Earth", }; describe("submitOrder", () => { test("decodes per-command results from a populated response", async () => { const responsePayload = buildResponse( [{ id: sampleRename.id, applied: true, errorCode: null }], 99, ); const exec = vi.fn(async () => ({ resultCode: "ok", payloadBytes: responsePayload, })); const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]); expect(exec).toHaveBeenCalledOnce(); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.results.get(sampleRename.id)).toBe("applied"); expect(result.errorCodes.get(sampleRename.id)).toBeNull(); expect(result.updatedAt).toBe(99); }); test("falls back to batch-level applied when commands array is empty", async () => { // Hand-craft an envelope without `commands` to mimic the legacy // gateway behaviour (or a 204 wrapped via the fallback path). const builder = new Builder(64); UserGamesOrderResponse.startUserGamesOrderResponse(builder); const offset = UserGamesOrderResponse.endUserGamesOrderResponse(builder); builder.finish(offset); const exec = vi.fn(async () => ({ resultCode: "ok", payloadBytes: builder.asUint8Array(), })); const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.results.get(sampleRename.id)).toBe("applied"); expect(result.errorCodes.get(sampleRename.id)).toBeNull(); }); test("surfaces mixed applied / rejected entries by cmd id", async () => { const second: OrderCommand = { kind: "planetRename", id: "00000000-0000-0000-0000-00000000bbbb", planetNumber: 8, name: "Mars", }; const responsePayload = buildResponse( [ { id: sampleRename.id, applied: true, errorCode: null }, { id: second.id, applied: false, errorCode: 42 }, ], 120, ); const exec = vi.fn(async () => ({ resultCode: "ok", payloadBytes: responsePayload, })); const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename, second]); expect(result.ok).toBe(true); if (!result.ok) return; expect(result.results.get(sampleRename.id)).toBe("applied"); expect(result.errorCodes.get(sampleRename.id)).toBeNull(); expect(result.results.get(second.id)).toBe("rejected"); expect(result.errorCodes.get(second.id)).toBe(42); }); test("returns SubmitFailure on non-ok resultCode without throwing", async () => { const exec = vi.fn(async () => ({ resultCode: "invalid_request", payloadBytes: new TextEncoder().encode( JSON.stringify({ code: "validation_failed", message: "bad name" }), ), })); const result = await submitOrder(mockClient(exec), GAME_ID, [sampleRename]); expect(result.ok).toBe(false); if (result.ok) return; expect(result.resultCode).toBe("invalid_request"); expect(result.code).toBe("validation_failed"); expect(result.message).toBe("bad name"); }); test("posts a well-formed UserGamesOrder payload", async () => { let captured: Uint8Array | null = null; const exec = vi.fn(async (_messageType, payload: Uint8Array) => { captured = payload; return { resultCode: "ok", payloadBytes: new Uint8Array() }; }); await submitOrder(mockClient(exec), GAME_ID, [sampleRename]); expect(captured).not.toBeNull(); const decoded = UserGamesOrder.getRootAsUserGamesOrder( new (await import("flatbuffers")).ByteBuffer(captured!), ); expect(decoded.commandsLength()).toBe(1); const item = decoded.commands(0); expect(item).not.toBeNull(); expect(item!.cmdId()).toBe(sampleRename.id); expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRename); const inner = new CommandPlanetRename(); item!.payload(inner); expect(Number(inner.number())).toBe(7); expect(inner.name()).toBe("Earth"); }); test("encodes setProductionType as CommandPlanetProduce on the wire", async () => { let captured: Uint8Array | null = null; const exec = vi.fn(async (_messageType, payload: Uint8Array) => { captured = payload; return { resultCode: "ok", payloadBytes: new Uint8Array() }; }); const cmd: OrderCommand = { kind: "setProductionType", id: "00000000-0000-0000-0000-00000000cccc", planetNumber: 17, productionType: "SHIP", subject: "Scout", }; await submitOrder(mockClient(exec), GAME_ID, [cmd]); expect(captured).not.toBeNull(); const decoded = UserGamesOrder.getRootAsUserGamesOrder( new (await import("flatbuffers")).ByteBuffer(captured!), ); expect(decoded.commandsLength()).toBe(1); const item = decoded.commands(0); expect(item).not.toBeNull(); expect(item!.cmdId()).toBe(cmd.id); expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetProduce); const inner = new CommandPlanetProduce(); item!.payload(inner); expect(Number(inner.number())).toBe(17); expect(inner.production()).toBe(PlanetProduction.SHIP); expect(inner.subject()).toBe("Scout"); }); test("encodes setCargoRoute as CommandPlanetRouteSet on the wire", async () => { let captured: Uint8Array | null = null; const exec = vi.fn(async (_messageType, payload: Uint8Array) => { captured = payload; return { resultCode: "ok", payloadBytes: new Uint8Array() }; }); const cmd: OrderCommand = { kind: "setCargoRoute", id: "00000000-0000-0000-0000-00000000aaaa", sourcePlanetNumber: 17, destinationPlanetNumber: 23, loadType: "COL", }; await submitOrder(mockClient(exec), GAME_ID, [cmd]); expect(captured).not.toBeNull(); const decoded = UserGamesOrder.getRootAsUserGamesOrder( new (await import("flatbuffers")).ByteBuffer(captured!), ); const item = decoded.commands(0); expect(item).not.toBeNull(); expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRouteSet); const inner = new CommandPlanetRouteSet(); item!.payload(inner); expect(Number(inner.origin())).toBe(17); expect(Number(inner.destination())).toBe(23); expect(inner.loadType()).toBe(PlanetRouteLoadType.COL); }); test("encodes removeCargoRoute as CommandPlanetRouteRemove on the wire", async () => { let captured: Uint8Array | null = null; const exec = vi.fn(async (_messageType, payload: Uint8Array) => { captured = payload; return { resultCode: "ok", payloadBytes: new Uint8Array() }; }); const cmd: OrderCommand = { kind: "removeCargoRoute", id: "00000000-0000-0000-0000-00000000bbbb", sourcePlanetNumber: 17, loadType: "MAT", }; await submitOrder(mockClient(exec), GAME_ID, [cmd]); const decoded = UserGamesOrder.getRootAsUserGamesOrder( new (await import("flatbuffers")).ByteBuffer(captured!), ); const item = decoded.commands(0); expect(item!.payloadType()).toBe(CommandPayload.CommandPlanetRouteRemove); const inner = new CommandPlanetRouteRemove(); item!.payload(inner); expect(Number(inner.origin())).toBe(17); expect(inner.loadType()).toBe(PlanetRouteLoadType.MAT); }); test("maps every cargoLoadType literal to its FBS enum value", async () => { const cases: Array<{ loadType: CargoLoadType; fbs: PlanetRouteLoadType }> = [ { loadType: "COL", fbs: PlanetRouteLoadType.COL }, { loadType: "CAP", fbs: PlanetRouteLoadType.CAP }, { loadType: "MAT", fbs: PlanetRouteLoadType.MAT }, { loadType: "EMP", fbs: PlanetRouteLoadType.EMP }, ]; for (const tc of cases) { let captured: Uint8Array | null = null; const exec = vi.fn(async (_messageType, payload: Uint8Array) => { captured = payload; return { resultCode: "ok", payloadBytes: new Uint8Array() }; }); const cmd: OrderCommand = { kind: "setCargoRoute", id: `id-${tc.loadType}`, sourcePlanetNumber: 5, destinationPlanetNumber: 6, loadType: tc.loadType, }; await submitOrder(mockClient(exec), GAME_ID, [cmd]); const decoded = UserGamesOrder.getRootAsUserGamesOrder( new (await import("flatbuffers")).ByteBuffer(captured!), ); const inner = new CommandPlanetRouteSet(); decoded.commands(0)!.payload(inner); expect(inner.loadType()).toBe(tc.fbs); } }); test("maps every productionType literal to its FBS enum value", async () => { const cases: Array<{ productionType: ProductionType; fbs: PlanetProduction; subject: string; }> = [ { productionType: "MAT", fbs: PlanetProduction.MAT, subject: "" }, { productionType: "CAP", fbs: PlanetProduction.CAP, subject: "" }, { productionType: "DRIVE", fbs: PlanetProduction.DRIVE, subject: "" }, { productionType: "WEAPONS", fbs: PlanetProduction.WEAPONS, subject: "", }, { productionType: "SHIELDS", fbs: PlanetProduction.SHIELDS, subject: "", }, { productionType: "CARGO", fbs: PlanetProduction.CARGO, subject: "" }, { productionType: "SCIENCE", fbs: PlanetProduction.SCIENCE, subject: "AlphaSci", }, { productionType: "SHIP", fbs: PlanetProduction.SHIP, subject: "Scout", }, ]; for (const tc of cases) { let captured: Uint8Array | null = null; const exec = vi.fn(async (_messageType, payload: Uint8Array) => { captured = payload; return { resultCode: "ok", payloadBytes: new Uint8Array() }; }); const cmd: OrderCommand = { kind: "setProductionType", id: `id-${tc.productionType}`, planetNumber: 5, productionType: tc.productionType, subject: tc.subject, }; await submitOrder(mockClient(exec), GAME_ID, [cmd]); expect(captured).not.toBeNull(); const decoded = UserGamesOrder.getRootAsUserGamesOrder( new (await import("flatbuffers")).ByteBuffer(captured!), ); const inner = new CommandPlanetProduce(); decoded.commands(0)!.payload(inner); expect(inner.production()).toBe(tc.fbs); expect(inner.subject()).toBe(tc.subject); } }); });