137 lines
8.1 KiB
Go
137 lines
8.1 KiB
Go
package service
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"browser.local/platform/domain"
|
|
)
|
|
|
|
func TestSCUMObservationProjectsRealRowsAndSeparatesProfileFromSteamID(t *testing.T) {
|
|
svc, _ := newRegisteredLogIngestService(t)
|
|
observed := time.Date(2026, 8, 10, 9, 0, 0, 0, time.UTC)
|
|
observation, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{
|
|
ServerInstanceID: "server-1",
|
|
PluginID: "server.scum",
|
|
Source: "run.sqlite.read",
|
|
QueryKey: "scum.player.profile",
|
|
Sequence: 10,
|
|
Checksum: "sha256:profile-10",
|
|
ObservedAt: observed,
|
|
Rows: []map[string]any{{
|
|
"gamePlayerId": "steam-1",
|
|
"userProfileId": "profile-99",
|
|
"steamId": "steam-1",
|
|
"displayName": "Moon",
|
|
"squadId": "squad-1",
|
|
"famePoints": 42,
|
|
"normalBalance": 500.0,
|
|
"goldBalance": 7.0,
|
|
"x": 100,
|
|
"y": 200,
|
|
"z": 30,
|
|
"lastSaveTime": observed.Add(-time.Minute).Format(time.RFC3339),
|
|
"future_column": "preserved",
|
|
}},
|
|
})
|
|
if err != nil || observation.Status != domain.SCUMObservationAccepted {
|
|
t.Fatalf("apply observation=%+v err=%v", observation, err)
|
|
}
|
|
player, err := svc.store.GamePlayers().Get(gamePlayerRecordID("server-1", "steam-1"))
|
|
if err != nil || player.DisplayName != "Moon" {
|
|
t.Fatalf("expected game player from real row: player=%+v err=%v", player, err)
|
|
}
|
|
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", UserProfileID: "profile-99"})
|
|
if err != nil || len(states) != 1 {
|
|
t.Fatalf("states=%+v err=%v", states, err)
|
|
}
|
|
state := states[0]
|
|
if state.GamePlayerID != "steam-1" || state.UserProfileID != "profile-99" || state.SteamID != "steam-1" || state.NormalBalance != 500 || state.Online {
|
|
t.Fatalf("identity/economy projection mixed IDs or inferred online incorrectly: %+v", state)
|
|
}
|
|
if !state.Position.HasCoordinates || state.Position.X != 100 || state.Position.Y != 200 || state.UnknownFields["future_column"] != "preserved" {
|
|
t.Fatalf("position/unknown fields not projected safely: %+v", state)
|
|
}
|
|
stale, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 9, Checksum: "sha256:profile-9", ObservedAt: observed.Add(-time.Hour), Rows: []map[string]any{{"gamePlayerId": "steam-1", "userProfileId": "profile-99", "displayName": "Old", "normalBalance": 9999}}})
|
|
if err != nil || stale.Status != domain.SCUMObservationStale || stale.ErrorCode != "older_observation" {
|
|
t.Fatalf("expected older observation stale, got %+v err=%v", stale, err)
|
|
}
|
|
again, err := svc.store.SCUMPlayerLiveStates().Get(state.ID)
|
|
if err != nil || again.DisplayName != "Moon" || again.NormalBalance != 500 {
|
|
t.Fatalf("older observation overwrote last-known-good: %+v err=%v", again, err)
|
|
}
|
|
}
|
|
|
|
func TestSCUMFailedObservationMarksStaleWithoutOverwritingProjection(t *testing.T) {
|
|
svc, _ := newRegisteredLogIngestService(t)
|
|
observed := time.Date(2026, 8, 10, 10, 0, 0, 0, time.UTC)
|
|
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:ok", ObservedAt: observed, Rows: []map[string]any{{"gamePlayerId": "steam-2", "userProfileId": "profile-2", "displayName": "Nova", "normalBalance": 125}}}); err != nil {
|
|
t.Fatalf("apply initial observation: %v", err)
|
|
}
|
|
failed, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 2, Checksum: "sha256:failed", Status: domain.SCUMObservationFailed, ErrorCode: "sqlite_busy", ObservedAt: observed.Add(time.Minute)})
|
|
if err != nil || failed.Status != domain.SCUMObservationFailed {
|
|
t.Fatalf("failed observation=%+v err=%v", failed, err)
|
|
}
|
|
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-2"})
|
|
if err != nil || len(states) != 1 {
|
|
t.Fatalf("states=%+v err=%v", states, err)
|
|
}
|
|
if states[0].NormalBalance != 125 || states[0].Freshness.Status != domain.SCUMProjectionStale || states[0].Freshness.StaleReason != "observation_failed" {
|
|
t.Fatalf("failed query did not preserve values and mark stale: %+v", states[0])
|
|
}
|
|
}
|
|
|
|
func TestSCUMLoginLogsProjectLiveStateAndDatabaseSaveTimeDoesNotProveOnline(t *testing.T) {
|
|
svc, token := newRegisteredLogIngestService(t)
|
|
createLogStreamFixture(t, svc)
|
|
base := time.Date(2026, 8, 10, 11, 0, 0, 0, time.UTC)
|
|
login := gamePlayerBatch(t, token, 1, []domain.LogEntry{{Seq: 1, Timestamp: base, Line: "login accepted", Fields: map[string]string{"eventType": "scum.login", "playerId": "steam-3", "playerName": "Comet", "sessionId": "session-3", "outcome": "accepted"}}})
|
|
if _, err := svc.IngestLogBatch(login); err != nil {
|
|
t.Fatalf("ingest login: %v", err)
|
|
}
|
|
states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-3"})
|
|
if err != nil || len(states) != 1 || !states[0].Online {
|
|
t.Fatalf("login did not mark live state online: states=%+v err=%v", states, err)
|
|
}
|
|
logout := gamePlayerBatch(t, token, 2, []domain.LogEntry{{Seq: 2, Timestamp: base.Add(time.Minute), Line: "logout", Fields: map[string]string{"eventType": "scum.logout", "playerId": "steam-3", "playerName": "Comet", "sessionId": "session-3", "reason": "disconnect"}}})
|
|
if _, err := svc.IngestLogBatch(logout); err != nil {
|
|
t.Fatalf("ingest logout: %v", err)
|
|
}
|
|
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 3, Checksum: "sha256:save-time", ObservedAt: base.Add(2 * time.Minute), Rows: []map[string]any{{"gamePlayerId": "steam-3", "userProfileId": "profile-3", "displayName": "Comet", "lastSaveTime": base.Add(90 * time.Second).Format(time.RFC3339)}}}); err != nil {
|
|
t.Fatalf("apply save-time observation: %v", err)
|
|
}
|
|
states, err = svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", GamePlayerID: "steam-3"})
|
|
if err != nil || len(states) != 1 {
|
|
t.Fatalf("states=%+v err=%v", states, err)
|
|
}
|
|
if states[0].Online || states[0].LastSaveTime.IsZero() {
|
|
t.Fatalf("last_save_time was incorrectly treated as online proof: %+v", states[0])
|
|
}
|
|
}
|
|
|
|
func TestSCUMObservationUsesDeclaredRowTargetInsteadOfQueryKeyName(t *testing.T) {
|
|
svc, _ := newRegisteredLogIngestService(t)
|
|
plugin, err := svc.store.GamePlugins().Get("server.scum")
|
|
if err != nil {
|
|
t.Fatalf("get plugin: %v", err)
|
|
}
|
|
plugin.GameClientBridge.QueryTemplates = append(plugin.GameClientBridge.QueryTemplates, domain.GameClientBridgeQueryTemplateDeclaration{Key: "v57.catalog.people", RowTarget: &domain.SCUMRowTargetDeclaration{TargetTable: string(domain.SCUMDataSetUsers), UpsertKeys: []string{"profileId"}, ColumnMappings: map[string]string{"profileId": "user_profile_id", "name": "display_name"}}})
|
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
|
t.Fatalf("update plugin: %v", err)
|
|
}
|
|
_, err = svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: "server-1", PluginID: "server.scum", Source: "run.sqlite.read", QueryKey: "v57.catalog.people", Sequence: 1, ObservedAt: time.Now().UTC(), Rows: []map[string]any{{"user_profile_id": "profile-1", "display_name": "Moon", "unmapped": "kept"}}})
|
|
if err != nil {
|
|
t.Fatalf("apply declared data row: %v", err)
|
|
}
|
|
rows, err := svc.store.SCUMDataRows().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1", TargetTable: domain.SCUMDataSetUsers})
|
|
if err != nil || len(rows) != 1 {
|
|
t.Fatalf("rows=%+v err=%v", rows, err)
|
|
}
|
|
if rows[0].Fields["profileId"] != "profile-1" || rows[0].Payload["unmapped"] != "kept" {
|
|
t.Fatalf("unexpected declared row: %+v", rows[0])
|
|
}
|
|
if states, err := svc.store.SCUMPlayerLiveStates().List(domain.SCUMProjectionFilter{ServerInstanceID: "server-1"}); err != nil || len(states) != 0 {
|
|
t.Fatalf("query key leaked into legacy projection dispatch: states=%+v err=%v", states, err)
|
|
}
|
|
}
|