feat: runtime manager

This commit is contained in:
Ilia Denisov
2026-04-28 20:39:18 +02:00
committed by GitHub
parent e0a99b346b
commit a7cee15115
289 changed files with 45660 additions and 2207 deletions
@@ -8,8 +8,8 @@ import (
"testing"
"time"
"galaxy/lobby/internal/adapters/gamestub"
"galaxy/lobby/internal/adapters/gmclientstub"
"galaxy/lobby/internal/adapters/gameinmem"
"galaxy/lobby/internal/adapters/mocks"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
"galaxy/lobby/internal/ports"
@@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func silentLogger() *slog.Logger {
@@ -33,7 +34,7 @@ func fixedClock(at time.Time) func() time.Time {
// source status.
func seedGameWithStatus(
t *testing.T,
store *gamestub.Store,
store *gameinmem.Store,
id common.GameID,
gameType game.GameType,
ownerUserID string,
@@ -94,13 +95,18 @@ func newService(
return svc
}
func newGMMock(t *testing.T) *mocks.MockGMClient {
t.Helper()
return mocks.NewMockGMClient(gomock.NewController(t))
}
func TestNewServiceRejectsMissingDeps(t *testing.T) {
t.Parallel()
_, err := resumegame.NewService(resumegame.Dependencies{})
require.Error(t, err)
_, err = resumegame.NewService(resumegame.Dependencies{Games: gamestub.NewStore()})
_, err = resumegame.NewService(resumegame.Dependencies{Games: gameinmem.NewStore()})
require.Error(t, err)
}
@@ -108,10 +114,11 @@ func TestResumeGameAdminHappyPath(t *testing.T) {
t.Parallel()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
store := gamestub.NewStore()
store := gameinmem.NewStore()
record := seedGameWithStatus(t, store, "game-pub", game.GameTypePublic, "", game.StatusPaused, now)
gm := gmclientstub.NewClient()
gm := newGMMock(t)
gm.EXPECT().Ping(gomock.Any()).Return(nil).Times(1)
service := newService(t, store, gm, fixedClock(now.Add(time.Hour)))
updated, err := service.Handle(context.Background(), resumegame.Input{
@@ -120,17 +127,17 @@ func TestResumeGameAdminHappyPath(t *testing.T) {
})
require.NoError(t, err)
assert.Equal(t, game.StatusRunning, updated.Status)
assert.Equal(t, 1, gm.PingCalls())
}
func TestResumeGamePrivateOwnerHappyPath(t *testing.T) {
t.Parallel()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
store := gamestub.NewStore()
store := gameinmem.NewStore()
record := seedGameWithStatus(t, store, "game-priv", game.GameTypePrivate, "user-owner", game.StatusPaused, now)
gm := gmclientstub.NewClient()
gm := newGMMock(t)
gm.EXPECT().Ping(gomock.Any()).Return(nil).Times(1)
service := newService(t, store, gm, fixedClock(now.Add(time.Hour)))
updated, err := service.Handle(context.Background(), resumegame.Input{
@@ -139,17 +146,16 @@ func TestResumeGamePrivateOwnerHappyPath(t *testing.T) {
})
require.NoError(t, err)
assert.Equal(t, game.StatusRunning, updated.Status)
assert.Equal(t, 1, gm.PingCalls())
}
func TestResumeGameRejectsNonOwnerUser(t *testing.T) {
t.Parallel()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
store := gamestub.NewStore()
store := gameinmem.NewStore()
record := seedGameWithStatus(t, store, "game-priv", game.GameTypePrivate, "user-owner", game.StatusPaused, now)
gm := gmclientstub.NewClient()
gm := newGMMock(t)
service := newService(t, store, gm, fixedClock(now.Add(time.Hour)))
_, err := service.Handle(context.Background(), resumegame.Input{
@@ -157,17 +163,16 @@ func TestResumeGameRejectsNonOwnerUser(t *testing.T) {
GameID: record.GameID,
})
require.ErrorIs(t, err, shared.ErrForbidden)
assert.Equal(t, 0, gm.PingCalls(), "ping must not run before authorization passes")
}
func TestResumeGameRejectsUserActorOnPublicGame(t *testing.T) {
t.Parallel()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
store := gamestub.NewStore()
store := gameinmem.NewStore()
record := seedGameWithStatus(t, store, "game-pub", game.GameTypePublic, "", game.StatusPaused, now)
gm := gmclientstub.NewClient()
gm := newGMMock(t)
service := newService(t, store, gm, fixedClock(now.Add(time.Hour)))
_, err := service.Handle(context.Background(), resumegame.Input{
@@ -175,7 +180,6 @@ func TestResumeGameRejectsUserActorOnPublicGame(t *testing.T) {
GameID: record.GameID,
})
require.ErrorIs(t, err, shared.ErrForbidden)
assert.Equal(t, 0, gm.PingCalls())
}
func TestResumeGameRejectsWrongStatuses(t *testing.T) {
@@ -197,10 +201,10 @@ func TestResumeGameRejectsWrongStatuses(t *testing.T) {
t.Parallel()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
store := gamestub.NewStore()
store := gameinmem.NewStore()
record := seedGameWithStatus(t, store, "game-x", game.GameTypePublic, "", status, now)
gm := gmclientstub.NewClient()
gm := newGMMock(t)
service := newService(t, store, gm, fixedClock(now.Add(time.Hour)))
_, err := service.Handle(context.Background(), resumegame.Input{
@@ -208,7 +212,6 @@ func TestResumeGameRejectsWrongStatuses(t *testing.T) {
GameID: record.GameID,
})
require.ErrorIs(t, err, game.ErrConflict)
assert.Equal(t, 0, gm.PingCalls(), "ping must not run before status check passes")
})
}
}
@@ -217,11 +220,13 @@ func TestResumeGameGMUnavailableKeepsPaused(t *testing.T) {
t.Parallel()
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
store := gamestub.NewStore()
store := gameinmem.NewStore()
record := seedGameWithStatus(t, store, "game-pub", game.GameTypePublic, "", game.StatusPaused, now)
gm := gmclientstub.NewClient()
gm.SetPingError(errors.Join(ports.ErrGMUnavailable, errors.New("dial tcp: connection refused")))
gm := newGMMock(t)
gm.EXPECT().Ping(gomock.Any()).
Return(errors.Join(ports.ErrGMUnavailable, errors.New("dial tcp: connection refused"))).
Times(1)
service := newService(t, store, gm, fixedClock(now.Add(time.Hour)))
_, err := service.Handle(context.Background(), resumegame.Input{
@@ -231,7 +236,6 @@ func TestResumeGameGMUnavailableKeepsPaused(t *testing.T) {
require.Error(t, err)
assert.ErrorIs(t, err, shared.ErrServiceUnavailable)
assert.ErrorIs(t, err, ports.ErrGMUnavailable)
assert.Equal(t, 1, gm.PingCalls())
persisted, err := store.Get(context.Background(), record.GameID)
require.NoError(t, err)
@@ -242,8 +246,8 @@ func TestResumeGameGMUnavailableKeepsPaused(t *testing.T) {
func TestResumeGameRejectsMissingRecord(t *testing.T) {
t.Parallel()
gm := gmclientstub.NewClient()
store := gamestub.NewStore()
gm := newGMMock(t)
store := gameinmem.NewStore()
service := newService(t, store, gm, fixedClock(time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)))
_, err := service.Handle(context.Background(), resumegame.Input{
@@ -251,14 +255,13 @@ func TestResumeGameRejectsMissingRecord(t *testing.T) {
GameID: common.GameID("game-missing"),
})
require.ErrorIs(t, err, game.ErrNotFound)
assert.Equal(t, 0, gm.PingCalls())
}
func TestResumeGameInvalidActor(t *testing.T) {
t.Parallel()
gm := gmclientstub.NewClient()
store := gamestub.NewStore()
gm := newGMMock(t)
store := gameinmem.NewStore()
service := newService(t, store, gm, fixedClock(time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)))
_, err := service.Handle(context.Background(), resumegame.Input{
@@ -272,8 +275,8 @@ func TestResumeGameInvalidActor(t *testing.T) {
func TestResumeGameInvalidGameID(t *testing.T) {
t.Parallel()
gm := gmclientstub.NewClient()
store := gamestub.NewStore()
gm := newGMMock(t)
store := gameinmem.NewStore()
service := newService(t, store, gm, fixedClock(time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)))
_, err := service.Handle(context.Background(), resumegame.Input{