ui/phase-25: backend turn-cutoff guard + auto-pause + UI sync protocol
Backend now owns the turn-cutoff and pause guards the order tab relies on: the scheduler flips runtime_status between generation_in_progress and running around every engine tick, a failed tick auto-pauses the game through OnRuntimeSnapshot, and a new game.paused notification kind fans out alongside game.turn.ready. The user-games handlers reject submits with HTTP 409 turn_already_closed or game_paused depending on the runtime state. UI delegates auto-sync to a new OrderQueue: offline detection, single retry on reconnect, conflict / paused classification. OrderDraftStore surfaces conflictBanner / pausedBanner runes, clears them on local mutation or on a game.turn.ready push via resetForNewTurn. The order tab renders the matching banners and the new conflict per-row badge; i18n bundles cover en + ru. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,10 +33,25 @@ interface RecordedCall {
|
||||
commandIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* RecordingOutcome enumerates the synthetic server reactions a test
|
||||
* can drive through `recordingClient.setOutcome`. Phase 25 adds the
|
||||
* `turn_already_closed` and `game_paused` codes (the order-queue
|
||||
* classifies them into `conflict` / `paused` outcomes) and `throw`
|
||||
* which lets the test exercise the network-error branch of
|
||||
* `OrderQueue.send`.
|
||||
*/
|
||||
export type RecordingOutcome =
|
||||
| "ok"
|
||||
| "rejected"
|
||||
| "turn_already_closed"
|
||||
| "game_paused"
|
||||
| "throw";
|
||||
|
||||
interface RecordingHandle {
|
||||
client: GalaxyClient;
|
||||
calls: RecordedCall[];
|
||||
setOutcome(outcome: "ok" | "rejected"): void;
|
||||
setOutcome(outcome: RecordingOutcome): void;
|
||||
waitForCalls(n: number): Promise<void>;
|
||||
waitForIdle(): Promise<void>;
|
||||
}
|
||||
@@ -51,11 +66,11 @@ interface RecordingHandle {
|
||||
*/
|
||||
export function recordingClient(
|
||||
gameId: string,
|
||||
initialOutcome: "ok" | "rejected",
|
||||
initialOutcome: RecordingOutcome,
|
||||
options: { delayMs?: number } = {},
|
||||
): RecordingHandle {
|
||||
const calls: RecordedCall[] = [];
|
||||
let outcome: "ok" | "rejected" = initialOutcome;
|
||||
let outcome: RecordingOutcome = initialOutcome;
|
||||
let inFlight = 0;
|
||||
const waiters: (() => void)[] = [];
|
||||
|
||||
@@ -81,21 +96,45 @@ export function recordingClient(
|
||||
if (id !== null) commandIds.push(id);
|
||||
}
|
||||
calls.push({ messageType, commandIds });
|
||||
if (outcome === "ok") {
|
||||
return {
|
||||
resultCode: "ok",
|
||||
payloadBytes: encodeApplied(gameId, commandIds, true),
|
||||
};
|
||||
switch (outcome) {
|
||||
case "ok":
|
||||
return {
|
||||
resultCode: "ok",
|
||||
payloadBytes: encodeApplied(gameId, commandIds, true),
|
||||
};
|
||||
case "turn_already_closed":
|
||||
return {
|
||||
resultCode: "turn_already_closed",
|
||||
payloadBytes: new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
code: "turn_already_closed",
|
||||
message: "turn closed before submit",
|
||||
}),
|
||||
),
|
||||
};
|
||||
case "game_paused":
|
||||
return {
|
||||
resultCode: "game_paused",
|
||||
payloadBytes: new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
code: "game_paused",
|
||||
message: "game is paused",
|
||||
}),
|
||||
),
|
||||
};
|
||||
case "throw":
|
||||
throw new Error("network down");
|
||||
default:
|
||||
return {
|
||||
resultCode: "invalid_request",
|
||||
payloadBytes: new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
code: "validation_failed",
|
||||
message: "rejected by fixture",
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
return {
|
||||
resultCode: "invalid_request",
|
||||
payloadBytes: new TextEncoder().encode(
|
||||
JSON.stringify({
|
||||
code: "validation_failed",
|
||||
message: "rejected by fixture",
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected messageType ${messageType}`);
|
||||
} finally {
|
||||
@@ -113,7 +152,7 @@ export function recordingClient(
|
||||
return {
|
||||
client,
|
||||
calls,
|
||||
setOutcome(next: "ok" | "rejected") {
|
||||
setOutcome(next: RecordingOutcome) {
|
||||
outcome = next;
|
||||
},
|
||||
async waitForCalls(n: number) {
|
||||
|
||||
Reference in New Issue
Block a user