package service import ( "errors" "testing" "time" "browser.local/platform/domain" "browser.local/platform/repo" ) func TestSCUMMapTrajectoryProjectionFiltersAndRetention(t *testing.T) { svc, runToken := newRegisteredLogIngestService(t) createLogStreamFixture(t, svc) enableSCUMMapTrajectory(t, svc) registered, err := svc.RegisterUser(domain.UserRegistration{DisplayName: "Map Owner", Email: "map-owner@example.test", Password: "secret-password"}) if err != nil { t.Fatal(err) } operator := registered.SessionID instance, _ := svc.store.ServerInstances().Get("server-1") instance.OwnerUserID = registered.User.ID if err := svc.store.ServerInstances().Update(instance); err != nil { t.Fatal(err) } base := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC) entries := []domain.LogEntry{ {Seq: 1, Timestamp: base, Line: "login", Fields: map[string]string{"eventType": "scum.login", "playerId": "steam-map", "playerName": "Moon", "sessionId": "map", "outcome": "accepted"}}, mapEntry(2, base, "player.position", "steam-map", "", "0", "0"), mapEntry(3, base.Add(time.Second), "player.position", "steam-map", "", "1", "1"), mapEntry(4, base.Add(-time.Minute), "player.position", "steam-map", "", "-100", "-100"), mapEntry(5, base.Add(2*time.Minute), "vehicle.position", "", "jeep-1", "200", "300"), mapEntry(6, base.Add(3*time.Minute), "player.vehicle.enter", "steam-map", "jeep-1", "", ""), mapEntry(7, base.Add(4*time.Minute), "player.vehicle.enter", "steam-map", "truck-2", "", ""), } if _, err := svc.IngestLogBatch(gamePlayerBatch(t, runToken, 1, entries)); err != nil { t.Fatal(err) } players, _ := svc.store.GamePlayers().List(domain.GamePlayerFilter{ServerInstanceID: "server-1"}) if len(players) != 1 { t.Fatalf("expected player projection, got %+v", players) } view, err := svc.GetGameMapTrajectoriesForSession(operator, domain.GameMapTrajectoryQuery{ServerInstanceID: "server-1", From: base.Add(-2 * time.Hour), To: base.Add(time.Hour), PlayerRecordIDs: []string{players[0].ID}, VehicleIDs: []string{"jeep-1"}}) if err != nil { t.Fatal(err) } if view.Status != "ready" || len(view.Players) != 1 || len(view.Players[0].Points) != 2 || view.Players[0].Points[0].MapX >= view.Players[0].Points[1].MapX || view.Players[0].Points[1].MapX != 500 { t.Fatalf("expected sorted converted and sampled player trail, got %+v", view.Players) } if len(view.Vehicles) != 1 || view.Vehicles[0].Points[0].MapX != 700 || len(view.RideSegments) != 1 || view.RideSegments[0].VehicleID != "jeep-1" || view.RideSegments[0].EndedAt.IsZero() { t.Fatalf("expected filtered vehicle and closed cross-vehicle segment, got vehicles=%+v segments=%+v", view.Vehicles, view.RideSegments) } if err := svc.store.GameMapTrackPoints().Create(domain.GameMapTrackPoint{ID: "cross-server", EventID: "cross", ServerInstanceID: "server-2", MapID: "scum-island", MapVersion: "0.9", EntityKind: domain.GameMapTrackEntityVehicle, EntityID: "cross-vehicle", MapX: 1, MapY: 1, OccurredAt: base, CollectedAt: base, ExpiresAt: base.Add(time.Hour)}); err != nil { t.Fatal(err) } isolation, err := svc.GetGameMapTrajectoriesForSession(operator, domain.GameMapTrajectoryQuery{ServerInstanceID: "server-1", From: base.Add(-time.Hour), To: base.Add(time.Hour), VehicleIDs: []string{"cross-vehicle"}}) if err != nil || len(isolation.Vehicles) != 0 { t.Fatalf("cross-server vehicle leaked: %+v err=%v", isolation, err) } if _, err := svc.GetGameMapTrajectoriesForSession("", domain.GameMapTrajectoryQuery{ServerInstanceID: "server-1"}); !errors.Is(err, ErrUnauthorized) { t.Fatalf("expected unauthorized denial, got %v", err) } points, _ := svc.store.GameMapTrackPoints().List(domain.GameMapTrackPointFilter{ServerInstanceID: "server-1"}) points[0].ExpiresAt = base.Add(-time.Second) if err := svc.store.GameMapTrackPoints().Delete(points[0].ID); err != nil { t.Fatal(err) } if err := svc.store.GameMapTrackPoints().Create(points[0]); err != nil { t.Fatal(err) } svc.now = func() time.Time { return base } if err := svc.pruneGameMapTrajectories("server-1"); err != nil { t.Fatal(err) } if _, err := svc.store.GameMapTrackPoints().Get(points[0].ID); !errors.Is(err, repo.ErrNotFound) { t.Fatalf("expired map point retained: %v", err) } } func mapEntry(seq uint64, at time.Time, eventType, playerID, vehicleID, x, y string) domain.LogEntry { fields := map[string]string{"eventType": eventType, "occurredAt": at.Format(time.RFC3339), "collectedAt": at.Add(time.Second).Format(time.RFC3339), "source": "companion", "mapId": "scum-island", "mapVersion": "0.9", "playerId": playerID, "vehicleId": vehicleID} if x != "" { fields["worldX"] = x fields["worldY"] = y } return domain.LogEntry{Seq: seq, Timestamp: at, Line: eventType, Fields: fields} } func enableSCUMMapTrajectory(t *testing.T, svc *CoreService) { t.Helper() plugin, err := svc.store.GamePlugins().Get("server.scum") if err != nil { t.Fatal(err) } plugin.MapTrajectories = &domain.GameMapTrajectoryDeclaration{MapID: "scum-island", MapVersion: "0.9", WorldMinX: -500, WorldMinY: -500, WorldMaxX: 500, WorldMaxY: 500, ImageWidth: 2048, ImageHeight: 2048, Precision: 1, SampleDistance: 4, SampleIntervalSeconds: 20, RetentionSeconds: 3600} if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatal(err) } }