Integrate SCUM real ops workflows

This commit is contained in:
npc0-hue
2026-08-10 21:12:53 +08:00
parent 1063330710
commit a770bc6250
88 changed files with 6375 additions and 2719 deletions
+273
View File
@@ -0,0 +1,273 @@
package service
import (
"encoding/json"
"strings"
"testing"
"time"
"browser.local/platform/domain"
)
func TestSCUMRCONOperationApprovalDispatchesTransientCommandAndConfirms(t *testing.T) {
svc, session, runSession, instance := newSourceRCONFixture(t)
seedSCUMOperationTemplates(t, svc, instance.PluginID)
request := domain.SCUMOperationRequest{TemplateKey: "player.fame.set", PlayerID: "76561198000000001", Payload: map[string]any{"fame": 123}, Reason: "restore fame", IdempotencyKey: "fame-restore-1"}
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, request)
if err != nil || operation.Status != domain.SCUMWorkflowStepWaiting {
t.Fatalf("request operation=%+v err=%v", operation, err)
}
duplicate, err := svc.RequestSCUMOperationForSession(session, instance.ID, request)
if err != nil || duplicate.ID != operation.ID {
t.Fatalf("duplicate should return original operation: duplicate=%+v err=%v", duplicate, err)
}
approved, err := svc.ApproveSCUMOperationForSession(session, operation.ID)
if err != nil || approved.Status != domain.SCUMWorkflowStepQueued || approved.RunJobID == "" {
t.Fatalf("approve operation=%+v err=%v", approved, err)
}
job, err := svc.store.Jobs().Get(approved.RunJobID)
if err != nil {
t.Fatalf("get operation job: %v", err)
}
serializedOperation, _ := json.Marshal(approved)
serializedJob, _ := json.Marshal(job)
for _, forbidden := range []string{"#SetFamePoints", "SetCurrencyBalance", "password="} {
if strings.Contains(string(serializedOperation), forbidden) || strings.Contains(string(serializedJob), forbidden) {
t.Fatalf("operation/job persisted raw RCON text %q: operation=%s job=%s", forbidden, serializedOperation, serializedJob)
}
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
t.Fatalf("claim operation RCON job: claim=%+v err=%v", claim, err)
}
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
if err != nil || !ack.Accepted {
t.Fatalf("ack operation RCON job: ack=%+v err=%v", ack, err)
}
input, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt})
if err != nil {
t.Fatalf("read transient operation command: %v", err)
}
if input.Command != "#SetFamePoints 123 \"76561198000000001\"" {
t.Fatalf("unexpected generated RCON command: %q", input.Command)
}
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "source-rcon.succeeded", AuditSummary: "typed RCON delivered"}}); err != nil {
t.Fatalf("complete operation job: %v", err)
}
reconciled, err := svc.ReconcileSCUMOperation(approved.ID)
if err != nil || reconciled.Status != domain.SCUMWorkflowStepConfirming {
t.Fatalf("expected confirming after delivery before readback: %+v err=%v", reconciled, err)
}
confirmed, err := svc.ConfirmSCUMOperation(approved.ID, domain.SCUMOperationConfirmation{Status: "confirmed", ConfirmedFields: map[string]any{"fame": 123}, ObservedAt: fixedTime.Add(time.Minute)})
if err != nil || confirmed.Status != domain.SCUMWorkflowStepConfirmed || confirmed.CompletedAt.IsZero() {
t.Fatalf("confirm operation=%+v err=%v", confirmed, err)
}
}
func TestSCUMRCONOperationPermissionUnknownAndConfirmationFailure(t *testing.T) {
svc, session, runSession, instance := newSourceRCONFixture(t)
seedSCUMOperationTemplates(t, svc, instance.PluginID)
adminOnly, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.currency.gold.set", PlayerID: "76561198000000002", Payload: map[string]any{"amount": 9}, Reason: "admin-only", IdempotencyKey: "gold-admin-only"})
if err != nil {
t.Fatalf("request admin-only operation: %v", err)
}
if _, err := svc.ApproveSCUMOperationForSession(session, adminOnly.ID); err != ErrForbidden {
t.Fatalf("expected platform-admin approval denial, got %v", err)
}
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.currency.normal.set", PlayerID: "76561198000000002", Payload: map[string]any{"amount": 500}, Reason: "repair balance", IdempotencyKey: "normal-unknown"})
if err != nil {
t.Fatalf("request normal currency operation: %v", err)
}
approved, err := svc.ApproveSCUMOperationForSession(session, operation.ID)
if err != nil {
t.Fatalf("approve normal currency operation: %v", err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunRCONCommand}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
t.Fatalf("claim normal currency job: claim=%+v err=%v", claim, err)
}
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
if err != nil {
t.Fatalf("ack normal currency job: %v", err)
}
if _, err := svc.GetSourceRCONExecutionInput(domain.SourceRCONExecutionInputRequest{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt}); err != nil {
t.Fatalf("consume normal currency command: %v", err)
}
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateFailed, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "source-rcon.unknown", AuditSummary: "unknown command state"}}); err != nil {
t.Fatalf("complete unknown operation job: %v", err)
}
unknown, err := svc.ReconcileSCUMOperation(approved.ID)
if err != nil || unknown.Status != domain.SCUMWorkflowStepUnknown {
t.Fatalf("expected unknown terminal state: %+v err=%v", unknown, err)
}
failure, err := svc.ConfirmSCUMOperation(operation.ID, domain.SCUMOperationConfirmation{Status: "failed", SafeSummary: domain.SCUMSafeSummary{Title: "Readback mismatch", Message: "Projection did not match expected currency."}, ObservedAt: time.Date(2026, 8, 10, 12, 0, 0, 0, time.UTC)})
if err != nil || failure.Status != domain.SCUMWorkflowStepFailed {
t.Fatalf("expected confirmation failure: %+v err=%v", failure, err)
}
}
func TestSCUMSQLiteMutationOperationSafetyGatesAndDispatchesTypedJob(t *testing.T) {
svc, session, runSession, instance := newSourceRCONFixture(t)
seedSCUMOperationTemplates(t, svc, instance.PluginID)
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-online", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "76561198000000855", "displayName": "Attribute Tester", "online": true, "855": 100}}}); err != nil {
t.Fatalf("seed online projection: %v", err)
}
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "76561198000000855", Payload: map[string]any{"fieldKey": "855", "before": 100, "after": 150, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/20260810"}, Reason: "repair attribute 855", IdempotencyKey: "attribute-855-1"})
if err != nil || operation.Status != domain.SCUMWorkflowStepWaiting {
t.Fatalf("request sqlite mutation=%+v err=%v", operation, err)
}
waiting, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
if err != nil || waiting.Status != domain.SCUMWorkflowStepWaiting || waiting.RunJobID != "" || !strings.Contains(waiting.SafeSummary.Title, "离线") {
t.Fatalf("online player should block dispatch: %+v err=%v", waiting, err)
}
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 2, Checksum: "sha256:profile-offline", ObservedAt: fixedTime.Add(time.Minute), Rows: []map[string]any{{"gamePlayerId": "76561198000000855", "displayName": "Attribute Tester", "online": false, "855": 100}}}); err != nil {
t.Fatalf("seed offline projection: %v", err)
}
approved, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
if err != nil || approved.Status != domain.SCUMWorkflowStepQueued || approved.RunJobID == "" {
t.Fatalf("approve sqlite mutation=%+v err=%v", approved, err)
}
job, err := svc.store.Jobs().Get(approved.RunJobID)
if err != nil {
t.Fatalf("get sqlite mutation job: %v", err)
}
if job.Capability != domain.JobCapabilityRemoteRunProtectedSQL || job.ExecutionInput.Inputs["fieldKey"] != "855" || job.ExecutionInput.Inputs["before"] != "100" || job.ExecutionInput.Inputs["after"] != "150" || job.ExecutionInput.Inputs["maxRowsAffected"] != "1" {
t.Fatalf("unexpected typed mutation job: %+v", job)
}
serializedOperation, _ := json.Marshal(approved)
serializedJob, _ := json.Marshal(job)
for _, forbidden := range []string{"UPDATE ", "DELETE ", "INSERT ", "SELECT ", "SCUM.db", "/Saved/", "requestText"} {
if strings.Contains(strings.ToUpper(string(serializedOperation)), strings.ToUpper(forbidden)) || strings.Contains(strings.ToUpper(string(serializedJob)), strings.ToUpper(forbidden)) {
t.Fatalf("operation/job persisted raw DB material %q: operation=%s job=%s", forbidden, serializedOperation, serializedJob)
}
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != approved.RunJobID {
t.Fatalf("claim sqlite mutation job: claim=%+v err=%v", claim, err)
}
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"})
if err != nil || !ack.Accepted {
t.Fatalf("ack sqlite mutation job: ack=%+v err=%v", ack, err)
}
mutationChecksum := "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
content := mustJSON(t, map[string]any{"outcome": "succeeded", "affectedRows": 1, "mutationChecksum": mutationChecksum, "confirmationRows": []map[string]any{{"playerId": "76561198000000855", "fieldKey": "855", "value": 150}}})
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "scum.sqlite-mutation.succeeded", Checksum: mutationChecksum, AuditSummary: "typed SCUM DB mutation result", Content: content}}); err != nil {
t.Fatalf("complete sqlite mutation job: %v", err)
}
confirmed, err := svc.ReconcileSCUMOperation(approved.ID)
if err != nil || confirmed.Status != domain.SCUMWorkflowStepConfirmed || confirmed.Confirmation.AffectedRows != 1 || confirmed.Confirmation.MutationChecksum != mutationChecksum {
t.Fatalf("expected confirmed sqlite mutation: %+v err=%v", confirmed, err)
}
}
func TestSCUMSQLiteMutationBlocksMissingSafetyAndStaleBefore(t *testing.T) {
svc, session, _, instance := newSourceRCONFixture(t)
seedSCUMOperationTemplates(t, svc, instance.PluginID)
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-855", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "steam-855", "displayName": "Guarded", "online": false, "855": 100}}}); err != nil {
t.Fatalf("seed projection: %v", err)
}
missingSafety, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-855", Payload: map[string]any{"fieldKey": "855", "before": 100, "after": 101}, Reason: "missing maintenance", IdempotencyKey: "attribute-855-missing-safety"})
if err != nil {
t.Fatalf("request missing safety mutation: %v", err)
}
waiting, err := svc.ApproveSCUMOperationForSession(adminSession, missingSafety.ID)
if err != nil || waiting.Status != domain.SCUMWorkflowStepWaiting || waiting.RunJobID != "" || !strings.Contains(waiting.SafeSummary.Title, "维护") {
t.Fatalf("expected missing maintenance/backup wait: %+v err=%v", waiting, err)
}
stale, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-855", Payload: map[string]any{"fieldKey": "855", "before": 99, "after": 101, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/stale"}, Reason: "stale before", IdempotencyKey: "attribute-855-stale-before"})
if err != nil {
t.Fatalf("request stale mutation: %v", err)
}
blocked, err := svc.ApproveSCUMOperationForSession(adminSession, stale.ID)
if err != nil || blocked.Status != domain.SCUMWorkflowStepBlocked || blocked.RunJobID != "" || !strings.Contains(blocked.SafeSummary.Title, "before") {
t.Fatalf("expected stale before block: %+v err=%v", blocked, err)
}
}
func TestSCUMSQLiteMutationResultValidationRejectsOverBoundRows(t *testing.T) {
svc, session, runSession, instance := newSourceRCONFixture(t)
seedSCUMOperationTemplates(t, svc, instance.PluginID)
adminSession := enableSCUMSQLiteMutationOperationSupport(t, svc, instance)
if _, err := svc.ApplySCUMObservationResult(domain.SCUMObservationResult{ServerInstanceID: instance.ID, PluginID: instance.PluginID, Source: "run.sqlite.read", QueryKey: "scum.player.profile", Sequence: 1, Checksum: "sha256:profile-overbound", ObservedAt: fixedTime, Rows: []map[string]any{{"gamePlayerId": "steam-overbound", "online": false, "855": 10}}}); err != nil {
t.Fatalf("seed projection: %v", err)
}
operation, err := svc.RequestSCUMOperationForSession(session, instance.ID, domain.SCUMOperationRequest{TemplateKey: "player.attribute.855.set", PlayerID: "steam-overbound", Payload: map[string]any{"fieldKey": "855", "before": 10, "after": 11, "safetyWindow": "maintenance-2026-08-10", "backupRef": "snapshot://scum/server-rcon/overbound"}, Reason: "overbound test", IdempotencyKey: "attribute-855-overbound"})
if err != nil {
t.Fatalf("request overbound mutation: %v", err)
}
approved, err := svc.ApproveSCUMOperationForSession(adminSession, operation.ID)
if err != nil || approved.RunJobID == "" {
t.Fatalf("approve overbound mutation=%+v err=%v", approved, err)
}
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: runSession, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}})
if err != nil || !claim.HasJob || claim.Job == nil {
t.Fatalf("claim overbound job: claim=%+v err=%v", claim, err)
}
ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt})
if err != nil {
t.Fatalf("ack overbound job: %v", err)
}
content := mustJSON(t, map[string]any{"outcome": "succeeded", "affectedRows": 2, "mutationChecksum": "sha256:mutation-overbound"})
if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: ack.Job.LeaseToken, Attempt: ack.Job.Attempt, State: domain.JobStateSucceeded, Progress: domain.RunJobProgressReport{Percent: 100}, ExecutionResult: domain.JobExecutionResult{Kind: "scum.sqlite-mutation.succeeded", Content: content, AuditSummary: "typed SCUM DB mutation result"}}); err != nil {
t.Fatalf("complete overbound job: %v", err)
}
unknown, err := svc.ReconcileSCUMOperation(approved.ID)
if err != nil || unknown.Status != domain.SCUMWorkflowStepUnknown {
t.Fatalf("expected over-bound rows to become unknown: %+v err=%v", unknown, err)
}
}
func seedSCUMOperationTemplates(t *testing.T, svc *CoreService, pluginID string) {
t.Helper()
plugin, err := svc.store.GamePlugins().Get(pluginID)
if err != nil {
t.Fatal(err)
}
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.command")
plugin.GameClientBridge.OperationTemplates = []domain.GameClientBridgeOperationTemplateDeclaration{
{Key: "player.fame.set", Title: "Set player fame", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-fame-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
{Key: "player.currency.normal.set", Title: "Set player normal currency", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-currency-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
{Key: "player.currency.gold.set", Title: "Set player gold currency", Permission: "server.game-client.command", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindRCON, TransportKey: "rcon", TargetKey: "rcon", PayloadSchemaRef: "schemas/bridge/player-currency-set.payload.schema.json", TimeoutSeconds: 60, MaxPayloadBytes: 2048, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresConfirmation: true}},
}
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update plugin operation templates: %v", err)
}
}
func enableSCUMSQLiteMutationOperationSupport(t *testing.T, svc *CoreService, instance domain.ServerInstance) string {
t.Helper()
adminSession := createServiceUserAndLogin(t, svc, domain.User{ID: "platform-admin-scum", DisplayName: "SCUM Admin", Email: "scum-admin@example.test", Roles: []string{"platform-admin"}, PasswordHash: "secret-password"})
plugin, err := svc.store.GamePlugins().Get(instance.PluginID)
if err != nil {
t.Fatal(err)
}
plugin.DeclaredPermissions = append(plugin.DeclaredPermissions, "server.game-client.maintenance")
plugin.RequiredRunCapabilities = append(plugin.RequiredRunCapabilities, domain.JobCapabilityRemoteRunProtectedSQL)
plugin.RemoteAccess.RunCapabilities = append(plugin.RemoteAccess.RunCapabilities, domain.JobCapabilityRemoteRunProtectedSQL)
plugin.RemoteAccess.DatabaseEngines = append(plugin.RemoteAccess.DatabaseEngines, "sqlite")
plugin.RuntimeProfiles.TransportProfiles = append(plugin.RuntimeProfiles.TransportProfiles, domain.RuntimeTransportProfile{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}})
plugin.GameClientBridge.OperationTemplates = append(plugin.GameClientBridge.OperationTemplates, domain.GameClientBridgeOperationTemplateDeclaration{Key: "player.attribute.855.set", Title: "Set player attribute 855", Permission: "server.game-client.maintenance", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, Kind: domain.GameClientBridgeOperationKindSQLiteMutation, TransportKey: "scum-database", TargetKey: "scum-database", PayloadSchemaRef: "schemas/bridge/player-attribute-855-set.payload.schema.json", ResultSchemaRef: "schemas/bridge/player-attribute-855-set.result.schema.json", ConfirmationSchemaRef: "schemas/bridge/player-attribute-855-set.confirmation.schema.json", TimeoutSeconds: 120, MaxPayloadBytes: 4096, MaxRowsAffected: 1, Mutation: domain.GameClientBridgeOperationMutationDeclaration{FieldKey: "855", TableKey: "prisoner", IdentityKey: "user_profile_id", ValueKey: "value", ConfirmationQueryKey: "scum.player.profile", AllowedValueType: "integer", MinValue: 0, MaxValue: 100000}, Safety: domain.GameClientBridgeOperationSafety{RequiresApproval: true, RequiresOfflinePlayer: true, RequiresMaintenanceWindow: true, RequiresBeforeValue: true, RequiresConfirmation: true, BackupRequired: true}})
if err := svc.store.GamePlugins().Update(plugin); err != nil {
t.Fatalf("update SCUM DB mutation plugin: %v", err)
}
endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
if err != nil {
t.Fatal(err)
}
endpoint.Capabilities = append(endpoint.Capabilities, domain.JobCapabilityRemoteRunProtectedSQL)
if err := svc.store.RunEndpoints().Update(endpoint); err != nil {
t.Fatalf("update SCUM DB mutation endpoint: %v", err)
}
return adminSession
}
func mustJSON(t *testing.T, value any) string {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatalf("marshal test JSON: %v", err)
}
return string(encoded)
}