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) } }