feat: runtime manager
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
package operationlogstore_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"galaxy/rtmanager/internal/adapters/postgres/internal/pgtest"
|
||||
"galaxy/rtmanager/internal/adapters/postgres/operationlogstore"
|
||||
"galaxy/rtmanager/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) *operationlogstore.Store {
|
||||
t.Helper()
|
||||
pgtest.TruncateAll(t)
|
||||
store, err := operationlogstore.New(operationlogstore.Config{
|
||||
DB: pgtest.Ensure(t).Pool(),
|
||||
OperationTimeout: pgtest.OperationTimeout,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return store
|
||||
}
|
||||
|
||||
func successStartEntry(gameID string, startedAt time.Time, sourceRef string) operation.OperationEntry {
|
||||
finishedAt := startedAt.Add(time.Second)
|
||||
return operation.OperationEntry{
|
||||
GameID: gameID,
|
||||
OpKind: operation.OpKindStart,
|
||||
OpSource: operation.OpSourceLobbyStream,
|
||||
SourceRef: sourceRef,
|
||||
ImageRef: "galaxy/game:v1.2.3",
|
||||
ContainerID: "container-1",
|
||||
Outcome: operation.OutcomeSuccess,
|
||||
StartedAt: startedAt,
|
||||
FinishedAt: &finishedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendReturnsPositiveIDs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
startedAt := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
||||
id1, err := store.Append(ctx, successStartEntry("game-001", startedAt, "1700000000000-0"))
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, id1, int64(0))
|
||||
|
||||
id2, err := store.Append(ctx, successStartEntry("game-001", startedAt.Add(time.Minute), "1700000000001-0"))
|
||||
require.NoError(t, err)
|
||||
assert.Greater(t, id2, id1)
|
||||
}
|
||||
|
||||
func TestAppendValidatesEntry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*operation.OperationEntry)
|
||||
}{
|
||||
{"empty game id", func(e *operation.OperationEntry) { e.GameID = "" }},
|
||||
{"unknown op kind", func(e *operation.OperationEntry) { e.OpKind = "exotic" }},
|
||||
{"unknown op source", func(e *operation.OperationEntry) { e.OpSource = "exotic" }},
|
||||
{"unknown outcome", func(e *operation.OperationEntry) { e.Outcome = "exotic" }},
|
||||
{"zero started at", func(e *operation.OperationEntry) { e.StartedAt = time.Time{} }},
|
||||
{"failure without error code", func(e *operation.OperationEntry) {
|
||||
e.Outcome = operation.OutcomeFailure
|
||||
e.ErrorCode = ""
|
||||
}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
entry := successStartEntry("game-001",
|
||||
time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC), "ref")
|
||||
tt.mutate(&entry)
|
||||
_, err := store.Append(ctx, entry)
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListByGameReturnsEntriesNewestFirst(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
base := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
||||
for index := range 3 {
|
||||
_, err := store.Append(ctx, successStartEntry("game-001",
|
||||
base.Add(time.Duration(index)*time.Minute),
|
||||
"ref-game-001-"))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// Foreign-game entry must not appear in the list.
|
||||
_, err := store.Append(ctx, successStartEntry("game-other", base, "ref-other"))
|
||||
require.NoError(t, err)
|
||||
|
||||
entries, err := store.ListByGame(ctx, "game-001", 10)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 3)
|
||||
for index := range 2 {
|
||||
assert.True(t,
|
||||
!entries[index].StartedAt.Before(entries[index+1].StartedAt),
|
||||
"entries must be ordered started_at DESC; got %s before %s",
|
||||
entries[index].StartedAt, entries[index+1].StartedAt,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListByGameRespectsLimit(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
base := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
||||
for index := range 5 {
|
||||
_, err := store.Append(ctx, successStartEntry("game-001",
|
||||
base.Add(time.Duration(index)*time.Minute), "ref"))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
entries, err := store.ListByGame(ctx, "game-001", 2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, entries, 2)
|
||||
}
|
||||
|
||||
func TestListByGameReturnsEmptyForUnknownGame(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
entries, err := store.ListByGame(ctx, "game-missing", 10)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, entries)
|
||||
}
|
||||
|
||||
func TestListByGameRejectsInvalidArgs(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", -3)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAppendRoundTripsAllFields(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newStore(t)
|
||||
|
||||
startedAt := time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
|
||||
finishedAt := startedAt.Add(2 * time.Second)
|
||||
original := operation.OperationEntry{
|
||||
GameID: "game-001",
|
||||
OpKind: operation.OpKindStop,
|
||||
OpSource: operation.OpSourceGMRest,
|
||||
SourceRef: "request-7",
|
||||
ImageRef: "galaxy/game:v2.0.0",
|
||||
ContainerID: "container-X",
|
||||
Outcome: operation.OutcomeFailure,
|
||||
ErrorCode: "container_start_failed",
|
||||
ErrorMessage: "stop deadline exceeded",
|
||||
StartedAt: startedAt,
|
||||
FinishedAt: &finishedAt,
|
||||
}
|
||||
id, err := store.Append(ctx, original)
|
||||
require.NoError(t, err)
|
||||
|
||||
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, original.GameID, got.GameID)
|
||||
assert.Equal(t, original.OpKind, got.OpKind)
|
||||
assert.Equal(t, original.OpSource, got.OpSource)
|
||||
assert.Equal(t, original.SourceRef, got.SourceRef)
|
||||
assert.Equal(t, original.ImageRef, got.ImageRef)
|
||||
assert.Equal(t, original.ContainerID, got.ContainerID)
|
||||
assert.Equal(t, original.Outcome, got.Outcome)
|
||||
assert.Equal(t, original.ErrorCode, got.ErrorCode)
|
||||
assert.Equal(t, original.ErrorMessage, got.ErrorMessage)
|
||||
assert.True(t, original.StartedAt.Equal(got.StartedAt))
|
||||
require.NotNil(t, got.FinishedAt)
|
||||
assert.True(t, original.FinishedAt.Equal(*got.FinishedAt))
|
||||
assert.Equal(t, time.UTC, got.StartedAt.Location())
|
||||
assert.Equal(t, time.UTC, got.FinishedAt.Location())
|
||||
}
|
||||
|
||||
func TestNewRejectsNilDB(t *testing.T) {
|
||||
_, err := operationlogstore.New(operationlogstore.Config{OperationTimeout: time.Second})
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewRejectsNonPositiveTimeout(t *testing.T) {
|
||||
_, err := operationlogstore.New(operationlogstore.Config{
|
||||
DB: pgtest.Ensure(t).Pool(),
|
||||
})
|
||||
require.Error(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user