package harness import ( "context" "errors" "testing" "time" "galaxy/rtmanager/internal/adapters/postgres/healthsnapshotstore" "galaxy/rtmanager/internal/adapters/postgres/operationlogstore" "galaxy/rtmanager/internal/adapters/postgres/runtimerecordstore" "galaxy/rtmanager/internal/domain/health" "galaxy/rtmanager/internal/domain/operation" "galaxy/rtmanager/internal/domain/runtime" "github.com/stretchr/testify/require" ) // RuntimeRecord returns the persisted runtime record for gameID. The // helper opens the store on every call (cheap; the harness `*sql.DB` // is shared) so individual scenarios stay isolated even if a previous // test mutated store state. func RuntimeRecord(t testing.TB, env *Env, gameID string) (runtime.RuntimeRecord, error) { t.Helper() store, err := runtimerecordstore.New(runtimerecordstore.Config{ DB: env.Postgres.Pool(), OperationTimeout: pgOperationTimeout, }) require.NoError(t, err) return store.Get(context.Background(), gameID) } // MustRuntimeRecord asserts that the record exists and returns it. func MustRuntimeRecord(t testing.TB, env *Env, gameID string) runtime.RuntimeRecord { t.Helper() record, err := RuntimeRecord(t, env, gameID) require.NoErrorf(t, err, "load runtime record for %s", gameID) return record } // EventuallyRuntimeRecord polls until predicate matches the runtime // record for gameID, or the deadline fires. Returns the matching // record. Used by lifecycle assertions that depend on async state // transitions (start consumer → record). func EventuallyRuntimeRecord(t testing.TB, env *Env, gameID string, predicate func(runtime.RuntimeRecord) bool, timeout time.Duration) runtime.RuntimeRecord { t.Helper() if timeout <= 0 { timeout = defaultStreamTimeout } deadline := time.Now().Add(timeout) for { record, err := RuntimeRecord(t, env, gameID) if err == nil && predicate(record) { return record } if err != nil && !errors.Is(err, runtime.ErrNotFound) { t.Fatalf("rtmanager integration: load runtime record: %v", err) } if time.Now().After(deadline) { if err != nil { t.Fatalf("rtmanager integration: runtime record predicate not met within %s; last err=%v", timeout, err) } t.Fatalf("rtmanager integration: runtime record predicate not met within %s; last record=%+v", timeout, record) } time.Sleep(defaultStreamPoll) } } // OperationEntries returns up to `limit` most-recent operation_log // entries for gameID, ordered descending by started_at. func OperationEntries(t testing.TB, env *Env, gameID string, limit int) []operation.OperationEntry { t.Helper() store, err := operationlogstore.New(operationlogstore.Config{ DB: env.Postgres.Pool(), OperationTimeout: pgOperationTimeout, }) require.NoError(t, err) entries, err := store.ListByGame(context.Background(), gameID, limit) require.NoErrorf(t, err, "list operation log entries for %s", gameID) return entries } // EventuallyOperationKind polls operation_log until at least one entry // for gameID has the requested kind, or the deadline fires. Returns // the matching entry. func EventuallyOperationKind(t testing.TB, env *Env, gameID string, kind operation.OpKind, timeout time.Duration) operation.OperationEntry { t.Helper() if timeout <= 0 { timeout = defaultStreamTimeout } deadline := time.Now().Add(timeout) for { entries := OperationEntries(t, env, gameID, 50) for _, entry := range entries { if entry.OpKind == kind { return entry } } if time.Now().After(deadline) { t.Fatalf("rtmanager integration: operation_log entry with op_kind=%s not seen within %s; observed=%v", kind, timeout, opKindSummary(entries)) } time.Sleep(defaultStreamPoll) } } // HealthSnapshot returns the latest persisted health snapshot for // gameID, or the underlying not-found sentinel when nothing has been // recorded yet. func HealthSnapshot(t testing.TB, env *Env, gameID string) (health.HealthSnapshot, error) { t.Helper() store, err := healthsnapshotstore.New(healthsnapshotstore.Config{ DB: env.Postgres.Pool(), OperationTimeout: pgOperationTimeout, }) require.NoError(t, err) return store.Get(context.Background(), gameID) } func opKindSummary(entries []operation.OperationEntry) []string { out := make([]string, 0, len(entries)) for _, entry := range entries { out = append(out, string(entry.OpKind)+"/"+string(entry.Outcome)) } return out }