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:
@@ -232,6 +232,22 @@ func (s *Store) ListMyGames(ctx context.Context, userID uuid.UUID) ([]GameRecord
|
||||
return modelsToGameRecords(rows)
|
||||
}
|
||||
|
||||
// DeleteGame removes the row at gameID. Cascades through every
|
||||
// referencing table (memberships / applications / invites /
|
||||
// runtime_records / player_mappings — all declared with ON DELETE
|
||||
// CASCADE in `00001_init.sql`). Idempotent: returns nil when no row
|
||||
// matches. Used by the dev-sandbox bootstrap to scrub terminal
|
||||
// games on every backend boot so the developer's lobby never piles
|
||||
// up cancelled tiles.
|
||||
func (s *Store) DeleteGame(ctx context.Context, gameID uuid.UUID) error {
|
||||
g := table.Games
|
||||
stmt := g.DELETE().WHERE(g.GameID.EQ(postgres.UUID(gameID)))
|
||||
if _, err := stmt.ExecContext(ctx, s.db); err != nil {
|
||||
return fmt.Errorf("lobby store: delete game %s: %w", gameID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// gameUpdate is the parameter struct for UpdateGame. Nil pointers leave
|
||||
// the corresponding column alone.
|
||||
type gameUpdate struct {
|
||||
|
||||
Reference in New Issue
Block a user