local-dev: auto-purge terminal Dev Sandbox games on every boot
Previously a cancelled / finished / start_failed sandbox game would hang in the dev user's lobby until manually cleaned up — `make up` would create a new running game alongside it but the dead tiles piled up. Now backend's `devsandbox.Bootstrap` deletes every terminal sandbox game owned by the dev user before find-or-create runs, so the lobby always shows exactly one running tile. Schema: `runtime_records` and `player_mappings` gain `ON DELETE CASCADE` on their `game_id` foreign keys so a single `DELETE FROM games` cleans every referencing row in one write. Pre-prod migration rule applies — change goes into `00001_init.sql`, not a new migration. API: `lobby.Service.DeleteGame` is the new destructive helper that backs the bootstrap purge. It bypasses the cancel-cascade-notify pipeline; production callers must stay on the regular lifecycle. The dev-sandbox docs in `tools/local-dev/README.md` spell out the new behaviour. Tests: - backend/internal/lobby/lobby_e2e_test.go gains `TestDeleteGameCascadesEverything` proving CASCADE works end-to-end against a real Postgres testcontainer. - backend/internal/devsandbox keeps its existing terminal-status contract test; the new `purgeTerminalSandboxGames` helper rides on the same `terminalSandboxStatus` predicate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,24 @@ func (s *Service) ListMyGames(ctx context.Context, userID uuid.UUID) ([]GameReco
|
||||
return s.deps.Store.ListMyGames(ctx, userID)
|
||||
}
|
||||
|
||||
// DeleteGame removes the game and every referencing row (memberships,
|
||||
// applications, invites, runtime_records, player_mappings) via the
|
||||
// `ON DELETE CASCADE` constraints declared in `00001_init.sql`.
|
||||
// Idempotent: returns nil when no game matches.
|
||||
//
|
||||
// Phase 14 introduces this method for the dev-sandbox bootstrap so a
|
||||
// terminal "Dev Sandbox" tile from a previous local-dev session can
|
||||
// be scrubbed before a fresh game spawns. Production callers must
|
||||
// stay on the regular cancel / finish lifecycle — `DeleteGame` is
|
||||
// destructive and bypasses the cascade-notification machinery.
|
||||
func (s *Service) DeleteGame(ctx context.Context, gameID uuid.UUID) error {
|
||||
if err := s.deps.Store.DeleteGame(ctx, gameID); err != nil {
|
||||
return err
|
||||
}
|
||||
s.deps.Cache.RemoveGame(gameID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// State-machine transition handlers below take the same shape: load the
|
||||
// game (cache or store), check owner, validate the current status, run
|
||||
// the transition write, refresh the cache, optionally tell the runtime
|
||||
|
||||
Reference in New Issue
Block a user