2ca47eb4df
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>
128 lines
3.6 KiB
Go
128 lines
3.6 KiB
Go
package lobby
|
|
|
|
import "testing"
|
|
|
|
// TestNextStatusFromSnapshot covers the pure status-mapping function
|
|
// that drives `OnRuntimeSnapshot`'s lifecycle transitions. The Phase
|
|
// 25 contribution is the `running → paused` branch on
|
|
// `engine_unreachable` / `generation_failed`: the order handler relies
|
|
// on the `paused` game status to reject late submits with
|
|
// `turn_already_closed`.
|
|
func TestNextStatusFromSnapshot(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
currentStatus string
|
|
runtimeStatus string
|
|
wantStatus string
|
|
wantTransit bool
|
|
}{
|
|
{
|
|
name: "starting then running flips to running",
|
|
currentStatus: GameStatusStarting,
|
|
runtimeStatus: "running",
|
|
wantStatus: GameStatusRunning,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "running on running snapshot does not transit",
|
|
currentStatus: GameStatusRunning,
|
|
runtimeStatus: "running",
|
|
wantStatus: GameStatusRunning,
|
|
wantTransit: false,
|
|
},
|
|
{
|
|
name: "starting then engine_unreachable flips to start_failed",
|
|
currentStatus: GameStatusStarting,
|
|
runtimeStatus: "engine_unreachable",
|
|
wantStatus: GameStatusStartFailed,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "starting then generation_failed flips to start_failed",
|
|
currentStatus: GameStatusStarting,
|
|
runtimeStatus: "generation_failed",
|
|
wantStatus: GameStatusStartFailed,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "running then engine_unreachable flips to paused",
|
|
currentStatus: GameStatusRunning,
|
|
runtimeStatus: "engine_unreachable",
|
|
wantStatus: GameStatusPaused,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "running then generation_failed flips to paused",
|
|
currentStatus: GameStatusRunning,
|
|
runtimeStatus: "generation_failed",
|
|
wantStatus: GameStatusPaused,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "paused stays paused on repeated failed snapshot",
|
|
currentStatus: GameStatusPaused,
|
|
runtimeStatus: "generation_failed",
|
|
wantStatus: GameStatusPaused,
|
|
wantTransit: false,
|
|
},
|
|
{
|
|
name: "starting then start_failed flips to start_failed",
|
|
currentStatus: GameStatusStarting,
|
|
runtimeStatus: "start_failed",
|
|
wantStatus: GameStatusStartFailed,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "running ignores start_failed",
|
|
currentStatus: GameStatusRunning,
|
|
runtimeStatus: "start_failed",
|
|
wantStatus: GameStatusRunning,
|
|
wantTransit: false,
|
|
},
|
|
{
|
|
name: "running on finished flips to finished",
|
|
currentStatus: GameStatusRunning,
|
|
runtimeStatus: "finished",
|
|
wantStatus: GameStatusFinished,
|
|
wantTransit: true,
|
|
},
|
|
{
|
|
name: "finished stays finished on finished snapshot",
|
|
currentStatus: GameStatusFinished,
|
|
runtimeStatus: "finished",
|
|
wantStatus: GameStatusFinished,
|
|
wantTransit: false,
|
|
},
|
|
{
|
|
name: "cancelled stays cancelled on finished snapshot",
|
|
currentStatus: GameStatusCancelled,
|
|
runtimeStatus: "finished",
|
|
wantStatus: GameStatusCancelled,
|
|
wantTransit: false,
|
|
},
|
|
{
|
|
name: "paused on stopped snapshot flips to finished",
|
|
currentStatus: GameStatusPaused,
|
|
runtimeStatus: "stopped",
|
|
wantStatus: GameStatusFinished,
|
|
wantTransit: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got, transit := nextStatusFromSnapshot(tt.currentStatus, RuntimeSnapshot{
|
|
RuntimeStatus: tt.runtimeStatus,
|
|
})
|
|
if got != tt.wantStatus {
|
|
t.Errorf("status = %q, want %q", got, tt.wantStatus)
|
|
}
|
|
if transit != tt.wantTransit {
|
|
t.Errorf("transit = %v, want %v", transit, tt.wantTransit)
|
|
}
|
|
})
|
|
}
|
|
}
|