// 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, CommandPlanetRename, CommandPayload, UserGamesOrder, UserGamesOrderResponse, } from "../src/proto/galaxy/fbs/order"; import { submitOrder } from "../src/sync/submit"; import type { OrderCommand } 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"); }); });