feat: runtime manager
This commit is contained in:
@@ -6,16 +6,16 @@ import (
|
||||
"io"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/adapters/applicationstub"
|
||||
"galaxy/lobby/internal/adapters/gamestub"
|
||||
"galaxy/lobby/internal/adapters/intentpubstub"
|
||||
"galaxy/lobby/internal/adapters/invitestub"
|
||||
"galaxy/lobby/internal/adapters/membershipstub"
|
||||
"galaxy/lobby/internal/adapters/racenamestub"
|
||||
"galaxy/lobby/internal/adapters/runtimemanagerstub"
|
||||
"galaxy/lobby/internal/adapters/applicationinmem"
|
||||
"galaxy/lobby/internal/adapters/gameinmem"
|
||||
"galaxy/lobby/internal/adapters/inviteinmem"
|
||||
"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"
|
||||
@@ -27,18 +27,94 @@ 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
|
||||
}
|
||||
|
||||
func (r *intentRec) record(_ context.Context, intent notificationintent.Intent) (string, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
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...)
|
||||
}
|
||||
|
||||
type runtimeRec struct {
|
||||
mu sync.Mutex
|
||||
stopIDs []string
|
||||
stopReas []ports.StopReason
|
||||
stopErr error
|
||||
}
|
||||
|
||||
func (r *runtimeRec) recordStart(_ context.Context, _, _ string) error { return nil }
|
||||
|
||||
func (r *runtimeRec) recordStop(_ context.Context, gameID string, reason ports.StopReason) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.stopErr != nil {
|
||||
return r.stopErr
|
||||
}
|
||||
r.stopIDs = append(r.stopIDs, gameID)
|
||||
r.stopReas = append(r.stopReas, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *runtimeRec) stopJobs() []string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return append([]string(nil), r.stopIDs...)
|
||||
}
|
||||
|
||||
func (r *runtimeRec) stopReasons() []ports.StopReason {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return append([]ports.StopReason(nil), r.stopReas...)
|
||||
}
|
||||
|
||||
func (r *runtimeRec) setStopErr(err error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.stopErr = 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 newRuntimeMock(t *testing.T, rec *runtimeRec) *mocks.MockRuntimeManager {
|
||||
t.Helper()
|
||||
m := mocks.NewMockRuntimeManager(gomock.NewController(t))
|
||||
m.EXPECT().PublishStartJob(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(rec.recordStart).AnyTimes()
|
||||
m.EXPECT().PublishStopJob(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||
DoAndReturn(rec.recordStop).AnyTimes()
|
||||
return m
|
||||
}
|
||||
|
||||
func silentLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }
|
||||
|
||||
type fixture struct {
|
||||
directory *racenamestub.Directory
|
||||
memberships *membershipstub.Store
|
||||
applications *applicationstub.Store
|
||||
invites *invitestub.Store
|
||||
games *gamestub.Store
|
||||
runtimeManager *runtimemanagerstub.Publisher
|
||||
intents *intentpubstub.Publisher
|
||||
directory *racenameinmem.Directory
|
||||
memberships *membershipinmem.Store
|
||||
applications *applicationinmem.Store
|
||||
invites *inviteinmem.Store
|
||||
games *gameinmem.Store
|
||||
runtimeRec *runtimeRec
|
||||
runtimeManager *mocks.MockRuntimeManager
|
||||
intentRec *intentRec
|
||||
intents *mocks.MockIntentPublisher
|
||||
worker *userlifecycle.Worker
|
||||
now time.Time
|
||||
}
|
||||
@@ -46,18 +122,22 @@ type fixture struct {
|
||||
func newFixture(t *testing.T) *fixture {
|
||||
t.Helper()
|
||||
|
||||
directory, err := racenamestub.NewDirectory()
|
||||
directory, err := racenameinmem.NewDirectory()
|
||||
require.NoError(t, err)
|
||||
|
||||
now := time.Date(2026, 4, 25, 12, 0, 0, 0, time.UTC)
|
||||
rtRec := &runtimeRec{}
|
||||
intRec := &intentRec{}
|
||||
f := &fixture{
|
||||
directory: directory,
|
||||
memberships: membershipstub.NewStore(),
|
||||
applications: applicationstub.NewStore(),
|
||||
invites: invitestub.NewStore(),
|
||||
games: gamestub.NewStore(),
|
||||
runtimeManager: runtimemanagerstub.NewPublisher(),
|
||||
intents: intentpubstub.NewPublisher(),
|
||||
memberships: membershipinmem.NewStore(),
|
||||
applications: applicationinmem.NewStore(),
|
||||
invites: inviteinmem.NewStore(),
|
||||
games: gameinmem.NewStore(),
|
||||
runtimeRec: rtRec,
|
||||
runtimeManager: newRuntimeMock(t, rtRec),
|
||||
intentRec: intRec,
|
||||
intents: newIntentMock(t, intRec),
|
||||
now: now,
|
||||
}
|
||||
|
||||
@@ -276,12 +356,16 @@ func TestHandleFullCascadePermanentBlock(t *testing.T) {
|
||||
gotOwned2, err := f.games.Get(context.Background(), ownedDraft.GameID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, game.StatusCancelled, gotOwned2.Status)
|
||||
stopJobs := f.runtimeManager.StopJobs()
|
||||
stopJobs := f.runtimeRec.stopJobs()
|
||||
require.Len(t, stopJobs, 1)
|
||||
assert.Equal(t, ownedRunning.GameID.String(), stopJobs[0])
|
||||
stopReasons := f.runtimeRec.stopReasons()
|
||||
require.Len(t, stopReasons, 1)
|
||||
assert.Equal(t, ports.StopReasonCancelled, stopReasons[0],
|
||||
"user-lifecycle cascade must classify the stop job as cancelled")
|
||||
|
||||
// Notification published only for the third-party private game owner.
|
||||
intents := f.intents.Published()
|
||||
intents := f.intentRec.snapshot()
|
||||
require.Len(t, intents, 1)
|
||||
assert.Equal(t, notificationintent.NotificationTypeLobbyMembershipBlocked, intents[0].NotificationType)
|
||||
assert.Equal(t, []string{"owner-other"}, intents[0].RecipientUserIDs)
|
||||
@@ -309,7 +393,7 @@ func TestHandleIsIdempotentOnReplay(t *testing.T) {
|
||||
require.NoError(t, f.worker.Handle(context.Background(), event))
|
||||
require.NoError(t, f.worker.Handle(context.Background(), event))
|
||||
|
||||
intents := f.intents.Published()
|
||||
intents := f.intentRec.snapshot()
|
||||
require.Len(t, intents, 1, "second pass must not double-publish")
|
||||
assert.Contains(t, intents[0].PayloadJSON, `"reason":"deleted"`)
|
||||
}
|
||||
@@ -378,7 +462,7 @@ func TestHandleUnknownEventTypeIsNoop(t *testing.T) {
|
||||
got, err := f.memberships.Get(context.Background(), member.MembershipID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, membership.StatusActive, got.Status)
|
||||
assert.Empty(t, f.intents.Published())
|
||||
assert.Empty(t, f.intentRec.snapshot())
|
||||
}
|
||||
|
||||
func TestHandlePropagatesStopJobError(t *testing.T) {
|
||||
@@ -386,7 +470,7 @@ func TestHandlePropagatesStopJobError(t *testing.T) {
|
||||
f := newFixture(t)
|
||||
f.seedGame(t, "game-owned-3", game.GameTypePrivate, "user-victim", game.StatusRunning)
|
||||
|
||||
f.runtimeManager.SetStopError(errors.New("runtime down"))
|
||||
f.runtimeRec.setStopErr(errors.New("runtime down"))
|
||||
|
||||
err := f.worker.Handle(context.Background(), ports.UserLifecycleEvent{
|
||||
EntryID: "1700000000000-0",
|
||||
@@ -399,10 +483,10 @@ func TestHandlePropagatesStopJobError(t *testing.T) {
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// flakyMembershipStore wraps membershipstub.Store with a one-shot
|
||||
// flakyMembershipStore wraps membershipinmem.Store with a one-shot
|
||||
// UpdateStatus failure injection used by the retry-after-error test.
|
||||
type flakyMembershipStore struct {
|
||||
*membershipstub.Store
|
||||
*membershipinmem.Store
|
||||
failOnce bool
|
||||
failError error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user