Files
galaxy-game/backend/internal/lobby/runtime_hooks_unit_test.go
T
Ilia Denisov 2ca47eb4df 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>
2026-05-11 22:00:16 +02:00

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)
}
})
}
}