package operationlog_test import ( "context" "testing" "time" "galaxy/gamemaster/internal/adapters/postgres/internal/pgtest" "galaxy/gamemaster/internal/adapters/postgres/operationlog" "galaxy/gamemaster/internal/domain/operation" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestMain(m *testing.M) { pgtest.RunMain(m) } func newStore(t *testing.T) *operationlog.Store { t.Helper() pgtest.TruncateAll(t) store, err := operationlog.New(operationlog.Config{ DB: pgtest.Ensure(t).Pool(), OperationTimeout: pgtest.OperationTimeout, }) require.NoError(t, err) return store } func successEntry(gameID string, kind operation.OpKind, source operation.OpSource, startedAt time.Time) operation.OperationEntry { finishedAt := startedAt.Add(50 * time.Millisecond) return operation.OperationEntry{ GameID: gameID, OpKind: kind, OpSource: source, SourceRef: "req-001", Outcome: operation.OutcomeSuccess, StartedAt: startedAt, FinishedAt: &finishedAt, } } func TestNewRejectsInvalidConfig(t *testing.T) { _, err := operationlog.New(operationlog.Config{}) require.Error(t, err) store, err := operationlog.New(operationlog.Config{ DB: pgtest.Ensure(t).Pool(), OperationTimeout: 0, }) require.Error(t, err) require.Nil(t, store) } func TestAppendSuccessEntry(t *testing.T) { ctx := context.Background() store := newStore(t) at := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC) entry := successEntry("game-001", operation.OpKindRegisterRuntime, operation.OpSourceLobbyInternal, at) id, err := store.Append(ctx, entry) require.NoError(t, err) assert.Greater(t, id, int64(0)) entries, err := store.ListByGame(ctx, "game-001", 10) require.NoError(t, err) require.Len(t, entries, 1) got := entries[0] assert.Equal(t, id, got.ID) assert.Equal(t, entry.GameID, got.GameID) assert.Equal(t, entry.OpKind, got.OpKind) assert.Equal(t, entry.OpSource, got.OpSource) assert.Equal(t, entry.SourceRef, got.SourceRef) assert.Equal(t, operation.OutcomeSuccess, got.Outcome) assert.Empty(t, got.ErrorCode) assert.Empty(t, got.ErrorMessage) assert.True(t, got.StartedAt.Equal(at)) require.NotNil(t, got.FinishedAt) assert.Equal(t, time.UTC, got.StartedAt.Location()) assert.Equal(t, time.UTC, got.FinishedAt.Location()) } func TestAppendFailureEntry(t *testing.T) { ctx := context.Background() store := newStore(t) at := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC) finishedAt := at.Add(time.Second) entry := operation.OperationEntry{ GameID: "game-001", OpKind: operation.OpKindTurnGeneration, OpSource: operation.OpSourceAdminRest, Outcome: operation.OutcomeFailure, ErrorCode: "engine_unreachable", ErrorMessage: "connection refused", StartedAt: at, FinishedAt: &finishedAt, } _, err := store.Append(ctx, entry) require.NoError(t, err) got, err := store.ListByGame(ctx, "game-001", 1) require.NoError(t, err) require.Len(t, got, 1) assert.Equal(t, operation.OutcomeFailure, got[0].Outcome) assert.Equal(t, "engine_unreachable", got[0].ErrorCode) assert.Equal(t, "connection refused", got[0].ErrorMessage) } func TestAppendIDsAreMonotonic(t *testing.T) { ctx := context.Background() store := newStore(t) at := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC) id1, err := store.Append(ctx, successEntry("game-001", operation.OpKindRegisterRuntime, operation.OpSourceLobbyInternal, at)) require.NoError(t, err) id2, err := store.Append(ctx, successEntry("game-001", operation.OpKindTurnGeneration, operation.OpSourceLobbyInternal, at.Add(time.Second))) require.NoError(t, err) assert.Greater(t, id2, id1, "bigserial ids must be monotonic across appends") } func TestAppendValidationRejection(t *testing.T) { ctx := context.Background() store := newStore(t) bad := operation.OperationEntry{} _, err := store.Append(ctx, bad) require.Error(t, err) } func TestListByGameOrderingDesc(t *testing.T) { ctx := context.Background() store := newStore(t) at := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC) _, err := store.Append(ctx, successEntry("game-001", operation.OpKindRegisterRuntime, operation.OpSourceLobbyInternal, at)) require.NoError(t, err) _, err = store.Append(ctx, successEntry("game-001", operation.OpKindTurnGeneration, operation.OpSourceLobbyInternal, at.Add(time.Second))) require.NoError(t, err) _, err = store.Append(ctx, successEntry("game-001", operation.OpKindStop, operation.OpSourceAdminRest, at.Add(2*time.Second))) require.NoError(t, err) got, err := store.ListByGame(ctx, "game-001", 10) require.NoError(t, err) require.Len(t, got, 3) assert.Equal(t, operation.OpKindStop, got[0].OpKind) assert.Equal(t, operation.OpKindTurnGeneration, got[1].OpKind) assert.Equal(t, operation.OpKindRegisterRuntime, got[2].OpKind) } func TestListByGameRespectsLimit(t *testing.T) { ctx := context.Background() store := newStore(t) at := time.Date(2026, time.April, 27, 12, 0, 0, 0, time.UTC) for index := range 5 { _, err := store.Append(ctx, successEntry("game-001", operation.OpKindTurnGeneration, operation.OpSourceLobbyInternal, at.Add(time.Duration(index)*time.Second))) require.NoError(t, err) } got, err := store.ListByGame(ctx, "game-001", 2) require.NoError(t, err) require.Len(t, got, 2) } func TestListByGameUnknownGame(t *testing.T) { ctx := context.Background() store := newStore(t) got, err := store.ListByGame(ctx, "unknown-game", 10) require.NoError(t, err) assert.Empty(t, got) } func TestListByGameRejectsBadArgs(t *testing.T) { ctx := context.Background() store := newStore(t) _, err := store.ListByGame(ctx, "", 10) require.Error(t, err) _, err = store.ListByGame(ctx, "game-001", 0) require.Error(t, err) _, err = store.ListByGame(ctx, "game-001", -1) require.Error(t, err) }