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
@@ -5,15 +5,16 @@ import (
"errors"
"io"
"log/slog"
"sync"
"testing"
"time"
"galaxy/lobby/internal/adapters/applicationstub"
"galaxy/lobby/internal/adapters/gamestub"
"galaxy/lobby/internal/adapters/gapactivationstub"
"galaxy/lobby/internal/adapters/intentpubstub"
"galaxy/lobby/internal/adapters/membershipstub"
"galaxy/lobby/internal/adapters/racenamestub"
"galaxy/lobby/internal/adapters/applicationinmem"
"galaxy/lobby/internal/adapters/gameinmem"
"galaxy/lobby/internal/adapters/gapactivationinmem"
"galaxy/lobby/internal/adapters/membershipinmem"
"galaxy/lobby/internal/adapters/mocks"
"galaxy/lobby/internal/adapters/racenameinmem"
"galaxy/lobby/internal/domain/application"
"galaxy/lobby/internal/domain/common"
"galaxy/lobby/internal/domain/game"
@@ -25,8 +26,44 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type intentRec struct {
mu sync.Mutex
published []notificationintent.Intent
err error
}
func (r *intentRec) record(_ context.Context, intent notificationintent.Intent) (string, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.err != nil {
return "", r.err
}
r.published = append(r.published, intent)
return "1", nil
}
func (r *intentRec) snapshot() []notificationintent.Intent {
r.mu.Lock()
defer r.mu.Unlock()
return append([]notificationintent.Intent(nil), r.published...)
}
func (r *intentRec) setErr(err error) {
r.mu.Lock()
defer r.mu.Unlock()
r.err = err
}
func newIntentMock(t *testing.T, rec *intentRec) *mocks.MockIntentPublisher {
t.Helper()
m := mocks.NewMockIntentPublisher(gomock.NewController(t))
m.EXPECT().Publish(gomock.Any(), gomock.Any()).DoAndReturn(rec.record).AnyTimes()
return m
}
func silentLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
func fixedClock(at time.Time) func() time.Time { return func() time.Time { return at } }
@@ -44,12 +81,13 @@ func (f fixedIDs) NewMembershipID() (common.MembershipID, error) { return f.me
type fixture struct {
now time.Time
games *gamestub.Store
memberships *membershipstub.Store
applications *applicationstub.Store
directory *racenamestub.Directory
gapStore *gapactivationstub.Store
intents *intentpubstub.Publisher
games *gameinmem.Store
memberships *membershipinmem.Store
applications *applicationinmem.Store
directory *racenameinmem.Directory
gapStore *gapactivationinmem.Store
intentRec *intentRec
intents *mocks.MockIntentPublisher
ids fixedIDs
openPublicGameID common.GameID
}
@@ -57,11 +95,11 @@ type fixture struct {
func newFixture(t *testing.T, maxPlayers, gapPlayers int) *fixture {
t.Helper()
now := time.Date(2026, 4, 25, 10, 0, 0, 0, time.UTC)
dir, err := racenamestub.NewDirectory(racenamestub.WithClock(fixedClock(now)))
dir, err := racenameinmem.NewDirectory(racenameinmem.WithClock(fixedClock(now)))
require.NoError(t, err)
games := gamestub.NewStore()
memberships := membershipstub.NewStore()
applications := applicationstub.NewStore()
games := gameinmem.NewStore()
memberships := membershipinmem.NewStore()
applications := applicationinmem.NewStore()
gameRecord, err := game.New(game.NewGameInput{
GameID: "game-public",
@@ -80,14 +118,16 @@ func newFixture(t *testing.T, maxPlayers, gapPlayers int) *fixture {
gameRecord.Status = game.StatusEnrollmentOpen
require.NoError(t, games.Save(context.Background(), gameRecord))
rec := &intentRec{}
return &fixture{
now: now,
games: games,
memberships: memberships,
applications: applications,
directory: dir,
gapStore: gapactivationstub.NewStore(),
intents: intentpubstub.NewPublisher(),
gapStore: gapactivationinmem.NewStore(),
intentRec: rec,
intents: newIntentMock(t, rec),
ids: fixedIDs{membershipID: "membership-fixed"},
openPublicGameID: gameRecord.GameID,
}
@@ -151,7 +191,7 @@ func TestApproveHappyPath(t *testing.T) {
assert.True(t, availability.Taken)
assert.Equal(t, "user-1", availability.HolderUserID)
intents := f.intents.Published()
intents := f.intentRec.snapshot()
require.Len(t, intents, 1)
assert.Equal(t, notificationintent.NotificationTypeLobbyMembershipApproved, intents[0].NotificationType)
assert.Equal(t, []string{"user-1"}, intents[0].RecipientUserIDs)
@@ -328,10 +368,10 @@ func TestApproveNameTakenByAnotherUser(t *testing.T) {
assert.Equal(t, "user-other", availability.HolderUserID)
}
// approveCASStub wraps applicationstub.Store but injects ErrConflict on
// approveCASStub wraps applicationinmem.Store but injects ErrConflict on
// the next UpdateStatus call so we can observe the rollback path.
type approveCASStub struct {
*applicationstub.Store
*applicationinmem.Store
failNext bool
}
@@ -379,7 +419,7 @@ func TestApprovePublishFailureDoesNotRollback(t *testing.T) {
t.Parallel()
f := newFixture(t, 4, 1)
app := seedSubmittedApplication(t, f, "application-1", "user-1", "SolarPilot")
f.intents.SetError(errors.New("publish failed"))
f.intentRec.setErr(errors.New("publish failed"))
svc := newService(t, f)
got, err := svc.Handle(context.Background(), approveapplication.Input{