package service import ( "testing" "time" "browser.local/platform/domain" ) func TestDurableStdoutProjectionCreatesUsersAndSuppressesRapidDuplicates(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin.RuntimeProfiles.ClientManagers = append(plugin.RuntimeProfiles.ClientManagers, domain.RuntimeClientManagerProfile{Key: "scum-client", Health: domain.RuntimeClientManagerHealth{RequiredCapabilities: []string{"game-client.bridge"}}}) plugin.GameClientBridge.LogProjections = []domain.GameClientBridgeLogProjectionDeclaration{{ Key: "player.login", StreamKeys: []string{"stdout"}, CorrelationFields: []string{"playerSlot"}, MaxInterveningLines: 4, Steps: []domain.GameClientBridgeLogProjectionStepDeclaration{ {Pattern: `Player "(?P[^"]+)" reported as player (?P[0-9]+)`}, {Pattern: `Player (?P[0-9]+) SteamID \(assumed\): (?P[0-9]+)`}, }, Target: domain.GameClientBridgeLogProjectionTargetDeclaration{ Collection: "scum_users", UpsertKeys: []string{"steamId"}, CaptureMappings: map[string]string{"steamId": "steamId", "displayName": "displayName", "playerSlot": "playerSlot"}, FixedValues: map[string]string{"online": "true", "source": "supervised-stdout"}, ObservedAtField: "lastLoginAt", }, Presence: &domain.GameClientBridgeLogProjectionPresenceDeclaration{ TimestampField: "lastLoginAt", ActiveWindowSeconds: 600, ActivityTarget: &domain.GameClientBridgeLogProjectionTargetDeclaration{ Collection: "scum_activity_events", UpsertKeys: []string{"steamId", "observedAt"}, CaptureMappings: map[string]string{"steamId": "steamId", "displayName": "displayName"}, FixedValues: map[string]string{"eventType": "login"}, ObservedAtField: "observedAt", }, }, }} if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update plugin projection: %v", err) } instance, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-log-projection", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM projection", State: domain.ServerInstanceStateRunning}) if err != nil { t.Fatalf("create server: %v", err) } helloRequest := validRunControlHello() helloRequest.CapabilityReport.Fingerprint = "cap-log-projection" hello, err := svc.RegisterRunHello(helloRequest) if err != nil { t.Fatalf("register Run: %v", err) } stream, err := svc.CreateLogStream(domain.LogStream{ID: "log-projection", ServerInstanceID: instance.ID, Source: domain.LogStreamSourceProcess, StreamKey: "stdout", StorageBackend: domain.LogStorageBackendLocalSegments, RetentionPolicy: "default"}) if err != nil { t.Fatalf("create stdout stream: %v", err) } base := time.Date(2026, 8, 18, 23, 25, 12, 0, time.UTC) ingestProjectionLines(t, svc, hello.SessionToken, endpoint.ID, instance.ID, stream.ID, 1, base, []string{ `LogBattlEye: Display: Player "love_fitting" reported as player 0`, `LogBattlEye: Display: Player #0 love_fitting (redacted) connected`, }) ingestProjectionLines(t, svc, hello.SessionToken, endpoint.ID, instance.ID, stream.ID, 3, base.Add(2*time.Second), []string{ `LogBattlEye: Display: Player 0 SteamID (assumed): 76561199510658111`, }) assertPresenceProjectionCounts(t, svc, plugin.ID, instance.ID, 1, 1, 0) ingestProjectionLines(t, svc, hello.SessionToken, endpoint.ID, instance.ID, stream.ID, 4, base.Add(5*time.Minute), []string{ `LogBattlEye: Display: Player "love_fitting" reported as player 0`, `LogBattlEye: Display: Player 0 SteamID (assumed): 76561199510658111`, }) assertPresenceProjectionCounts(t, svc, plugin.ID, instance.ID, 1, 1, 0) ingestProjectionLines(t, svc, hello.SessionToken, endpoint.ID, instance.ID, stream.ID, 6, base.Add(11*time.Minute), []string{ `LogBattlEye: Display: Player "love_fitting" reported as player 0`, `LogBattlEye: Display: Player 0 SteamID (assumed): 76561199510658111`, }) assertPresenceProjectionCounts(t, svc, plugin.ID, instance.ID, 1, 2, 0) } func TestLifecycleProjectionMarksOnlineUsersOffline(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin.GameClientBridge.LifecycleProjections = []domain.GameClientBridgeLifecycleProjectionDeclaration{{ Key: "server.stop", Capabilities: []string{domain.LifecycleCapabilityStop}, ProcessStates: []string{"stopped"}, Target: domain.GameClientBridgeBulkProjectionTargetDeclaration{Collection: "scum_users", MatchField: "online", MatchValue: "true", FixedValues: map[string]string{"online": "false", "status": "offline", "logoutReason": "server-stop"}, ObservedAtField: "lastLogoutAt", ActivityTarget: &domain.GameClientBridgeBulkActivityTargetDeclaration{Collection: "scum_activity_events", UpsertKeys: []string{"steamId", "observedAt", "eventType"}, RowMappings: map[string]string{"steamId": "steamId", "displayName": "displayName"}, FixedValues: map[string]string{"eventType": "logout", "reason": "server-stop"}, ObservedAtField: "observedAt"}}, }} if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update lifecycle projection plugin: %v", err) } instance, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-lifecycle-projection", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM lifecycle", State: domain.ServerInstanceStateRunning}) if err != nil { t.Fatalf("create server: %v", err) } if _, err := svc.applyPluginDataTransaction(domain.PluginDataTransaction{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users", Mutations: []domain.PluginDataMutation{ {Operation: domain.PluginDataMutationPut, Key: "steam-1", Value: map[string]any{"steamId": "steam-1", "displayName": "Ada", "online": "true"}}, {Operation: domain.PluginDataMutationPut, Key: "steam-2", Value: map[string]any{"steamId": "steam-2", "displayName": "Lin", "online": "false"}}, }}); err != nil { t.Fatalf("seed plugin users: %v", err) } helloRequest := validRunControlHello() helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, domain.LifecycleCapabilityStop) helloRequest.CapabilityReport.Fingerprint = "cap-lifecycle-projection" hello, err := svc.RegisterRunHello(helloRequest) if err != nil { t.Fatalf("register Run: %v", err) } _, err = svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, ServerInstanceID: instance.ID, Capability: domain.LifecycleCapabilityStop, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "stopped", ExitClassification: "requested-stop"}}) if err != nil { t.Fatalf("report lifecycle stop: %v", err) } users, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users"}) if err != nil || len(users) != 2 { t.Fatalf("list lifecycle users=%+v err=%v", users, err) } for _, user := range users { if user.Key == "steam-1" && (user.Value["online"] != "false" || user.Value["logoutReason"] != "server-stop" || user.Value["lastLogoutAt"] == nil) { t.Fatalf("online user was not logged out: %+v", user) } if user.Key == "steam-2" && user.Value["logoutReason"] != nil { t.Fatalf("offline user should not receive duplicate logout: %+v", user) } } activity, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_activity_events"}) if err != nil || len(activity) != 1 || activity[0].Value["steamId"] != "steam-1" || activity[0].Value["eventType"] != "logout" { t.Fatalf("lifecycle logout activity not projected: %+v err=%v", activity, err) } } func TestLifecycleRestartReportMarksOnlineUsersOffline(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin.GameClientBridge.LifecycleProjections = []domain.GameClientBridgeLifecycleProjectionDeclaration{{ Key: "server.restart", Capabilities: []string{"process.restart"}, Target: domain.GameClientBridgeBulkProjectionTargetDeclaration{Collection: "scum_users", MatchField: "online", MatchValue: "true", FixedValues: map[string]string{"online": "false", "status": "offline", "logoutReason": "server-stop"}, ObservedAtField: "lastLogoutAt"}, }} if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatalf("update restart projection plugin: %v", err) } instance, err := svc.CreateServerInstance(domain.ServerInstance{ID: "server-restart-projection", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "SCUM restart", State: domain.ServerInstanceStateRunning}) if err != nil { t.Fatalf("create server: %v", err) } if _, err := svc.applyPluginDataTransaction(domain.PluginDataTransaction{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users", Mutations: []domain.PluginDataMutation{{Operation: domain.PluginDataMutationPut, Key: "steam-1", Value: map[string]any{"steamId": "steam-1", "displayName": "Ada", "online": true}}}}); err != nil { t.Fatalf("seed plugin users: %v", err) } helloRequest := validRunControlHello() helloRequest.CapabilityReport.Capabilities = append(helloRequest.CapabilityReport.Capabilities, "process.restart") helloRequest.CapabilityReport.Fingerprint = "cap-restart-lifecycle-projection" hello, err := svc.RegisterRunHello(helloRequest) if err != nil { t.Fatalf("register Run: %v", err) } report, err := svc.ReportRunLifecycle(domain.RunLifecycleReport{RunEndpointID: endpoint.ID, SessionToken: hello.SessionToken, ServerInstanceID: instance.ID, Capability: "process.restart", State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "process", ProcessState: "running"}}) if err != nil || report.ProjectedState != domain.ServerInstanceStateRunning { t.Fatalf("report lifecycle restart: report=%+v err=%v", report, err) } users, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: "scum_users"}) if err != nil || len(users) != 1 || users[0].Value["online"] != "false" || users[0].Value["logoutReason"] != "server-stop" || users[0].Value["lastLogoutAt"] == nil { t.Fatalf("restart did not log out online users: %+v err=%v", users, err) } } func ingestProjectionLines(t *testing.T, svc *CoreService, sessionToken, endpointID, serverID, streamID string, firstSeq uint64, observedAt time.Time, lines []string) { t.Helper() entries := make([]domain.LogEntry, len(lines)) for index, line := range lines { entries[index] = domain.LogEntry{Seq: firstSeq + uint64(index), Timestamp: observedAt.Add(time.Duration(index) * time.Second), Level: "display", Line: line} } lastSeq := firstSeq + uint64(len(entries)) - 1 batch := domain.LogBatchIngest{RunEndpointID: endpointID, SessionToken: sessionToken, LogStreamID: streamID, ServerInstanceID: serverID, StreamKey: "stdout", Source: domain.LogStreamSourceProcess, FirstSeq: firstSeq, LastSeq: lastSeq, Compression: "none", Checksum: checksumForEntries(t, entries), Entries: entries} if result, err := svc.IngestLogBatch(batch); err != nil || !result.Accepted { t.Fatalf("ingest projection lines result=%+v err=%v", result, err) } } func assertPresenceProjectionCounts(t *testing.T, svc *CoreService, pluginID, serverID string, users, activities, commands int) { t.Helper() userRows, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: pluginID, ServerInstanceID: serverID, Collection: "scum_users"}) if err != nil || len(userRows) != users { t.Fatalf("projected users=%+v err=%v", userRows, err) } activityRows, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: pluginID, ServerInstanceID: serverID, Collection: "scum_activity_events"}) if err != nil || len(activityRows) != activities { t.Fatalf("projected activities=%+v err=%v", activityRows, err) } queued, err := svc.store.GameClientBridgeCommands().List(domain.GameClientBridgeCommandFilter{ServerInstanceID: serverID, PluginID: pluginID}) if err != nil || len(queued) != commands { t.Fatalf("presence bridge commands=%+v err=%v", queued, err) } }