ui/phase-14: rename planet end-to-end + order read-back
Wires the first end-to-end command through the full pipeline:
inspector rename action → local order draft → user.games.order
submit → optimistic overlay on map / inspector → server hydration
on cache miss via the new user.games.order.get message type.
Backend: GET /api/v1/user/games/{id}/orders forwards to engine
GET /api/v1/order. Gateway parses the engine PUT response into the
extended UserGamesOrderResponse FBS envelope and adds
executeUserGamesOrderGet for the read-back path. Frontend ports
ValidateTypeName to TS, lands the inline rename editor + Submit
button, and exposes a renderedReport context so consumers see the
overlay-applied snapshot.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -175,4 +175,158 @@ describe("OrderDraftStore", () => {
|
||||
expect(reload.commands.map((c) => c.id)).toEqual(["c1"]);
|
||||
reload.dispose();
|
||||
});
|
||||
|
||||
test("absent cache row flips needsServerHydration flag", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
expect(store.needsServerHydration).toBe(true);
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("explicitly empty cache row honours the user's empty draft", async () => {
|
||||
const seeded = new OrderDraftStore();
|
||||
await seeded.init({ cache, gameId: GAME_ID });
|
||||
await seeded.add({
|
||||
kind: "planetRename",
|
||||
id: "00000000-0000-0000-0000-000000000001",
|
||||
planetNumber: 7,
|
||||
name: "Earth",
|
||||
});
|
||||
await seeded.remove("00000000-0000-0000-0000-000000000001");
|
||||
seeded.dispose();
|
||||
|
||||
const reload = new OrderDraftStore();
|
||||
await reload.init({ cache, gameId: GAME_ID });
|
||||
expect(reload.needsServerHydration).toBe(false);
|
||||
expect(reload.commands).toEqual([]);
|
||||
reload.dispose();
|
||||
});
|
||||
|
||||
test("planetRename validates locally and statuses reflect valid/invalid", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "planetRename",
|
||||
id: "id-valid",
|
||||
planetNumber: 1,
|
||||
name: "Earth",
|
||||
});
|
||||
await store.add({
|
||||
kind: "planetRename",
|
||||
id: "id-invalid",
|
||||
planetNumber: 2,
|
||||
name: "$bad",
|
||||
});
|
||||
expect(store.statuses["id-valid"]).toBe("valid");
|
||||
expect(store.statuses["id-invalid"]).toBe("invalid");
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("markSubmitting / applyResults flip the status map", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "planetRename",
|
||||
id: "id-1",
|
||||
planetNumber: 1,
|
||||
name: "Earth",
|
||||
});
|
||||
store.markSubmitting(["id-1"]);
|
||||
expect(store.statuses["id-1"]).toBe("submitting");
|
||||
store.applyResults({
|
||||
results: new Map([["id-1", "applied"] as const]),
|
||||
updatedAt: 99,
|
||||
});
|
||||
expect(store.statuses["id-1"]).toBe("applied");
|
||||
expect(store.updatedAt).toBe(99);
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("markRejected switches submitting entries to rejected", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "planetRename",
|
||||
id: "id-1",
|
||||
planetNumber: 1,
|
||||
name: "Earth",
|
||||
});
|
||||
store.markSubmitting(["id-1"]);
|
||||
store.markRejected(["id-1"]);
|
||||
expect(store.statuses["id-1"]).toBe("rejected");
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("revertSubmittingToValid restores status after a thrown submit", async () => {
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
await store.add({
|
||||
kind: "planetRename",
|
||||
id: "id-1",
|
||||
planetNumber: 1,
|
||||
name: "Earth",
|
||||
});
|
||||
store.markSubmitting(["id-1"]);
|
||||
store.revertSubmittingToValid();
|
||||
expect(store.statuses["id-1"]).toBe("valid");
|
||||
store.dispose();
|
||||
});
|
||||
|
||||
test("hydrateFromServer seeds the draft on a fresh cache", async () => {
|
||||
const fakeClient = {
|
||||
executeCommand: async () => {
|
||||
const { Builder } = await import("flatbuffers");
|
||||
const { UUID } = await import("../src/proto/galaxy/fbs/common");
|
||||
const order = await import("../src/proto/galaxy/fbs/order");
|
||||
const builder = new Builder(128);
|
||||
const cmdId = builder.createString("hydr-1");
|
||||
const name = builder.createString("Hydrated");
|
||||
const inner = order.CommandPlanetRename.createCommandPlanetRename(
|
||||
builder,
|
||||
BigInt(7),
|
||||
name,
|
||||
);
|
||||
order.CommandItem.startCommandItem(builder);
|
||||
order.CommandItem.addCmdId(builder, cmdId);
|
||||
order.CommandItem.addPayloadType(
|
||||
builder,
|
||||
order.CommandPayload.CommandPlanetRename,
|
||||
);
|
||||
order.CommandItem.addPayload(builder, inner);
|
||||
const item = order.CommandItem.endCommandItem(builder);
|
||||
const cmds = order.UserGamesOrder.createCommandsVector(builder, [item]);
|
||||
const [hi, lo] = (await import("../src/api/game-state")).uuidToHiLo(
|
||||
GAME_ID,
|
||||
);
|
||||
const gameIdOffset = UUID.createUUID(builder, hi, lo);
|
||||
order.UserGamesOrder.startUserGamesOrder(builder);
|
||||
order.UserGamesOrder.addGameId(builder, gameIdOffset);
|
||||
order.UserGamesOrder.addUpdatedAt(builder, BigInt(7));
|
||||
order.UserGamesOrder.addCommands(builder, cmds);
|
||||
const orderOffset = order.UserGamesOrder.endUserGamesOrder(builder);
|
||||
order.UserGamesOrderGetResponse.startUserGamesOrderGetResponse(builder);
|
||||
order.UserGamesOrderGetResponse.addFound(builder, true);
|
||||
order.UserGamesOrderGetResponse.addOrder(builder, orderOffset);
|
||||
const offset =
|
||||
order.UserGamesOrderGetResponse.endUserGamesOrderGetResponse(builder);
|
||||
builder.finish(offset);
|
||||
return {
|
||||
resultCode: "ok",
|
||||
payloadBytes: builder.asUint8Array(),
|
||||
};
|
||||
},
|
||||
};
|
||||
const store = new OrderDraftStore();
|
||||
await store.init({ cache, gameId: GAME_ID });
|
||||
expect(store.needsServerHydration).toBe(true);
|
||||
await store.hydrateFromServer({
|
||||
client: fakeClient as never,
|
||||
turn: 5,
|
||||
});
|
||||
expect(store.commands).toHaveLength(1);
|
||||
expect(store.commands[0]!.id).toBe("hydr-1");
|
||||
expect(store.updatedAt).toBe(7);
|
||||
expect(store.needsServerHydration).toBe(false);
|
||||
store.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user