fix(game): #59 — per-command rejection on PUT /api/v1/order #71
@@ -34,6 +34,10 @@ interface CommandResultFixtureBase {
|
|||||||
cmdId: string;
|
cmdId: string;
|
||||||
applied: boolean | null;
|
applied: boolean | null;
|
||||||
errorCode: number | null;
|
errorCode: number | null;
|
||||||
|
// Optional engine-formatted rejection reason; mirrors the
|
||||||
|
// `cmd_error_message` field added in the same patch as
|
||||||
|
// `cmdErrorCode`'s shelf renumber. Omit on applied commands.
|
||||||
|
errorMessage?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanetRenameResultFixture extends CommandResultFixtureBase {
|
export interface PlanetRenameResultFixture extends CommandResultFixtureBase {
|
||||||
@@ -132,6 +136,14 @@ export function buildOrderResponsePayload(
|
|||||||
updatedAt: number,
|
updatedAt: number,
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
const builder = new Builder(256);
|
const builder = new Builder(256);
|
||||||
|
// flatbuffers@25 elides any field equal to its generated default;
|
||||||
|
// for `cmd_applied: bool = null` the default is int8 0, so the
|
||||||
|
// false case (per-command rejection) would silently disappear from
|
||||||
|
// the encoded payload. The production Go transcoder uses explicit
|
||||||
|
// `Slot()` calls that force the write, and the unit-test helpers
|
||||||
|
// in `tests/helpers/fake-order-client.ts` flip `forceDefaults`;
|
||||||
|
// these e2e fixtures need the same toggle.
|
||||||
|
builder.forceDefaults(true);
|
||||||
const itemOffsets = commands.map((c) => encodeItem(builder, c));
|
const itemOffsets = commands.map((c) => encodeItem(builder, c));
|
||||||
const commandsVec = UserGamesOrderResponse.createCommandsVector(
|
const commandsVec = UserGamesOrderResponse.createCommandsVector(
|
||||||
builder,
|
builder,
|
||||||
@@ -155,6 +167,11 @@ export function buildOrderGetResponsePayload(
|
|||||||
found = true,
|
found = true,
|
||||||
): Uint8Array {
|
): Uint8Array {
|
||||||
const builder = new Builder(256);
|
const builder = new Builder(256);
|
||||||
|
// See `buildOrderResponsePayload` — the GET response path needs
|
||||||
|
// the same `forceDefaults` toggle so a stored per-command rejection
|
||||||
|
// (cmd_applied=false / cmd_error_code=0) survives the round trip
|
||||||
|
// to the UI's `hydrateFromServer`.
|
||||||
|
builder.forceDefaults(true);
|
||||||
|
|
||||||
let orderOffset = 0;
|
let orderOffset = 0;
|
||||||
if (found) {
|
if (found) {
|
||||||
@@ -290,6 +307,10 @@ function encodeItem(builder: Builder, c: CommandResultFixture): number {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const errMsgOffset =
|
||||||
|
c.errorMessage !== undefined && c.errorMessage !== null
|
||||||
|
? builder.createString(c.errorMessage)
|
||||||
|
: 0;
|
||||||
CommandItem.startCommandItem(builder);
|
CommandItem.startCommandItem(builder);
|
||||||
CommandItem.addCmdId(builder, cmdIdOffset);
|
CommandItem.addCmdId(builder, cmdIdOffset);
|
||||||
if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied);
|
if (c.applied !== null) CommandItem.addCmdApplied(builder, c.applied);
|
||||||
@@ -298,6 +319,9 @@ function encodeItem(builder: Builder, c: CommandResultFixture): number {
|
|||||||
}
|
}
|
||||||
CommandItem.addPayloadType(builder, payloadType);
|
CommandItem.addPayloadType(builder, payloadType);
|
||||||
CommandItem.addPayload(builder, inner);
|
CommandItem.addPayload(builder, inner);
|
||||||
|
if (errMsgOffset !== 0) {
|
||||||
|
CommandItem.addCmdErrorMessage(builder, errMsgOffset);
|
||||||
|
}
|
||||||
return CommandItem.endCommandItem(builder);
|
return CommandItem.endCommandItem(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
|
|||||||
name: submittedName,
|
name: submittedName,
|
||||||
applied,
|
applied,
|
||||||
errorCode: applied ? null : 1,
|
errorCode: applied ? null : 1,
|
||||||
|
errorMessage: applied
|
||||||
|
? null
|
||||||
|
: "Entity does not exists: planet #99",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (opts.submitOutcome === "applied") {
|
if (opts.submitOutcome === "applied") {
|
||||||
@@ -320,14 +323,19 @@ test("rejected submit keeps the old name and surfaces the failure", async ({
|
|||||||
const orderTool = page.getByTestId("sidebar-tool-order");
|
const orderTool = page.getByTestId("sidebar-tool-order");
|
||||||
|
|
||||||
// The auto-sync pipeline reaches the server immediately after
|
// The auto-sync pipeline reaches the server immediately after
|
||||||
// the inline confirm; the rejected verdict surfaces through the
|
// the inline confirm. Per-command rejection is a player-correctable
|
||||||
// per-row status badge and the sync bar.
|
// state: the round trip succeeded, the engine just refused this
|
||||||
|
// command, so the sync bar stays `synced` while the rejected row
|
||||||
|
// surfaces the engine-formatted reason for the player.
|
||||||
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
|
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
|
||||||
"rejected",
|
"rejected",
|
||||||
);
|
);
|
||||||
|
await expect(orderTool.getByTestId("order-command-error-0")).toHaveText(
|
||||||
|
"Entity does not exists: planet #99",
|
||||||
|
);
|
||||||
await expect(orderTool.getByTestId("order-sync")).toHaveAttribute(
|
await expect(orderTool.getByTestId("order-sync")).toHaveAttribute(
|
||||||
"data-sync-status",
|
"data-sync-status",
|
||||||
"error",
|
"synced",
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByTestId("sidebar-tab-inspector").click();
|
await page.getByTestId("sidebar-tab-inspector").click();
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ async function mockGateway(page: Page, opts: MockOpts): Promise<MockHandle> {
|
|||||||
cargo: lastCreate.cargo,
|
cargo: lastCreate.cargo,
|
||||||
applied,
|
applied,
|
||||||
errorCode: applied ? null : 1,
|
errorCode: applied ? null : 1,
|
||||||
|
errorMessage: applied
|
||||||
|
? null
|
||||||
|
: "Entity already exists: ship type \"Drone\"",
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -392,9 +395,10 @@ test("rejected createShipClass keeps the table empty and surfaces the failure",
|
|||||||
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
|
await expect(orderTool.getByTestId("order-command-status-0")).toHaveText(
|
||||||
"rejected",
|
"rejected",
|
||||||
);
|
);
|
||||||
|
await expect(orderTool.getByTestId("order-command-error-0")).toBeVisible();
|
||||||
await expect(orderTool.getByTestId("order-sync")).toHaveAttribute(
|
await expect(orderTool.getByTestId("order-sync")).toHaveAttribute(
|
||||||
"data-sync-status",
|
"data-sync-status",
|
||||||
"error",
|
"synced",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Switch sidebar back to inspector so the active-view (table)
|
// Switch sidebar back to inspector so the active-view (table)
|
||||||
|
|||||||
Reference in New Issue
Block a user