package service import ( "encoding/json" "strings" "testing" "time" "browser.local/platform/domain" "browser.local/platform/repo" ) func newGameClientBridgeService(t *testing.T) (*CoreService, *time.Time) { t.Helper() now := time.Date(2026, 7, 20, 10, 0, 0, 0, time.UTC) store := repo.NewMemoryStore() plugin := domain.GamePlugin{ID: "game.scum", RuntimeProfiles: domain.GamePluginRuntimeProfiles{ClientManagers: []domain.RuntimeClientManagerProfile{{Key: "scum-client", Health: domain.RuntimeClientManagerHealth{RequiredCapabilities: []string{gameClientBridgeCapability}}}}}, GameClientBridge: domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "announcement.send", ApprovalLevel: domain.GameClientBridgeApprovalLevelOperator, TimeoutSeconds: 600, MaxPayloadBytes: 4096}}, Snapshots: []domain.GameClientBridgeSnapshotDeclaration{{Type: "players", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}, {Type: "health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 60}}, {Type: "companion.health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}} if err := store.GamePlugins().Create(plugin); err != nil { t.Fatalf("seed bridge plugin: %v", err) } svc := newCoreService(store, func() time.Time { return now }) return svc, &now } func bridgeQueueRequest(now time.Time, key string) domain.GameClientBridgeQueueRequest { return domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "announcement.send", Payload: map[string]any{"message": "hello"}, IdempotencyKey: key, Priority: 10, ExpiresAt: now.Add(5 * time.Minute)} } func bridgeComponent() gameClientBridgeComponentSession { return gameClientBridgeComponentSession{ Session: domain.ClientManagerSession{ID: "component-session-1", ServerInstanceID: "server-1", ProfileKey: "scum-client", DeploymentGeneration: 3}, Installation: domain.ClientManagerInstallation{ID: "installation-1", PluginID: "game.scum"}, } } func TestGameClientBridgeCommandLifecycleAndIdempotency(t *testing.T) { svc, clock := newGameClientBridgeService(t) request := bridgeQueueRequest(*clock, "announce-1") command, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil { t.Fatalf("queue bridge command: %v", err) } duplicate, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil || duplicate.ID != command.ID { t.Fatalf("idempotency reuse: command=%#v err=%v", duplicate, err) } commands, _ := svc.store.GameClientBridgeCommands().List(domain.GameClientBridgeCommandFilter{}) if len(commands) != 1 || len(command.AuditReferences) != 1 { t.Fatalf("expected one durable audited command: %#v", commands) } component := bridgeComponent() claimed, err := svc.claimGameClientBridgeCommands(component, 10) if err != nil || len(claimed) != 1 || claimed[0].State != domain.GameClientBridgeCommandClaimed || claimed[0].Claim.FencingToken != 1 { t.Fatalf("claim bridge command: %#v err=%v", claimed, err) } initialLeaseExpiry := claimed[0].Claim.LeaseExpiresAt if _, err := svc.ackGameClientBridgeCommand(component, domain.GameClientBridgeAckRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: 2}); err == nil { t.Fatal("expected stale fencing token rejection") } *clock = clock.Add(10 * time.Second) acked, err := svc.ackGameClientBridgeCommand(component, domain.GameClientBridgeAckRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: 1}) if err != nil || acked.Claim.AcknowledgedAt.IsZero() || !acked.Claim.LeaseExpiresAt.Equal(clock.Add(defaultGameClientBridgeLeaseDuration)) || !acked.Claim.LeaseExpiresAt.After(initialLeaseExpiry) { t.Fatalf("ack bridge command: %#v err=%v", acked, err) } if _, err := svc.completeGameClientBridgeCommand(component, domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: 1, Status: domain.GameClientBridgeResultSucceeded, Payload: map[string]any{"sessionToken": "must-not-persist"}}); err == nil { t.Fatal("expected unsafe result material to be rejected") } resultRequest := domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: 1, Status: domain.GameClientBridgeResultSucceeded, Summary: "delivered", Payload: map[string]any{"delivered": true}} completed, err := svc.completeGameClientBridgeCommand(component, resultRequest) if err != nil || completed.State != domain.GameClientBridgeCommandSucceeded || completed.Result.Status != domain.GameClientBridgeResultSucceeded || completed.CompletedAt.IsZero() { t.Fatalf("complete bridge command: %#v err=%v", completed, err) } if len(completed.AuditReferences) < 3 { t.Fatalf("expected queue, claim, and result audit references: %#v", completed.AuditReferences) } auditReferenceCount := len(completed.AuditReferences) replayed, err := svc.completeGameClientBridgeCommand(component, resultRequest) if err != nil || replayed.ID != completed.ID || replayed.State != completed.State || !replayed.CompletedAt.Equal(completed.CompletedAt) || len(replayed.AuditReferences) != auditReferenceCount { t.Fatalf("exact terminal result retry was not idempotent: replayed=%#v err=%v", replayed, err) } if _, err := svc.completeGameClientBridgeCommand(component, domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: 1, Status: domain.GameClientBridgeResultFailed, Summary: "conflict"}); err == nil { t.Fatal("expected conflicting terminal result rejection") } } func TestProtectedGameClientBridgeRequestIsScopedAndRedacted(t *testing.T) { svc, clock := newGameClientBridgeService(t) plugin, err := svc.store.GamePlugins().Get("game.scum") if err != nil { t.Fatal(err) } plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "database", Kind: "sqlite", TargetKey: "database", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}}} plugin.GameClientBridge.Commands = append(plugin.GameClientBridge.Commands, domain.GameClientBridgeCommandDeclaration{Type: "database.request", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, TimeoutSeconds: 60, MaxPayloadBytes: 4096, ProtectedRequest: &domain.GameClientBridgeProtectedRequestDeclaration{Kind: "sql", TransportKey: "database", TargetKey: "database", TextField: "requestText", MaxTextBytes: 1024}}) if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatal(err) } text := "UPDATE players SET rank = 2 WHERE id = 7" request := domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "database.request", Payload: map[string]any{"requestText": text}, IdempotencyKey: "protected-1", ExpiresAt: clock.Add(time.Minute)} command, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil { t.Fatalf("queue protected request: %v", err) } if command.ApprovalState != domain.GameClientBridgeApprovalPending { t.Fatalf("protected request bypassed approval: %#v", command) } if _, err := svc.queueGameClientBridgeCommand("user-1", domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "database.request", Payload: map[string]any{"requestText": text, "unexpected": true}, IdempotencyKey: "protected-extra", ExpiresAt: clock.Add(time.Minute)}); err == nil { t.Fatal("protected request accepted undeclared payload field") } events, err := svc.store.AuditEvents().List(domain.AuditEventFilter{ResourceID: command.ID}) if err != nil || len(events) != 1 { t.Fatalf("protected request audit: events=%#v err=%v", events, err) } if strings.Contains(events[0].Summary, text) || !strings.Contains(events[0].Summary, "text=redacted") { t.Fatalf("audit leaked protected request: %#v", events[0]) } } func TestProtectedGameClientBridgeRequestDispatchesOneTimeRunInput(t *testing.T) { svc, clock := newGameClientBridgeService(t) plugin, err := svc.store.GamePlugins().Get("game.scum") if err != nil { t.Fatal(err) } plugin.RequiredRunCapabilities = []string{domain.JobCapabilityRemoteRunProtectedSQL} plugin.RuntimeProfiles.TransportProfiles = []domain.RuntimeTransportProfile{{Key: "scum-database", Kind: "sqlite", TargetKey: "scum-database", Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}}} plugin.GameClientBridge.Commands = append(plugin.GameClientBridge.Commands, domain.GameClientBridgeCommandDeclaration{Type: "database.request", ApprovalLevel: domain.GameClientBridgeApprovalLevelPlatformAdmin, TimeoutSeconds: 120, MaxPayloadBytes: 4096, ProtectedRequest: &domain.GameClientBridgeProtectedRequestDeclaration{Kind: "sql", TransportKey: "scum-database", TargetKey: "scum-database", TextField: "requestText", MaxTextBytes: 1024}}) if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatal(err) } if err := svc.store.Users().Create(domain.User{ID: "platform-admin", Email: "admin@example.test", Roles: []string{"platform-admin"}}); err != nil { t.Fatal(err) } if err := svc.store.ServerInstances().Create(domain.ServerInstance{ID: "server-1", PluginID: plugin.ID, RunEndpointID: "run-local", Name: "Protected Bridge", State: domain.ServerInstanceStateRunning}); err != nil { t.Fatal(err) } hello := validRunControlHello() hello.CapabilityReport.Capabilities = []string{domain.JobCapabilityRemoteRunProtectedSQL} hello.CapabilityReport.Fingerprint = "protected-request-capabilities" run, err := svc.RegisterRunHello(hello) if err != nil { t.Fatalf("register Run: %v", err) } text := "SELECT player_id, position FROM players WHERE player_id = 7" command, err := svc.queueGameClientBridgeCommand("platform-admin", domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: plugin.ID, ProfileKey: "scum-client", CommandType: "database.request", Payload: map[string]any{"requestText": text}, IdempotencyKey: "protected-run-1", ExpiresAt: clock.Add(time.Minute)}) if err != nil { t.Fatalf("queue protected request: %v", err) } if command.RunJobID == "" || command.Payload["requestText"] != "redacted" || command.ApprovalState != domain.GameClientBridgeApprovalApproved { t.Fatalf("protected command was not redacted and dispatched: %#v", command) } commandJSON, _ := json.Marshal(command) if strings.Contains(string(commandJSON), text) { t.Fatalf("protected bridge command persisted request text: %s", commandJSON) } if claimed, err := svc.claimGameClientBridgeCommands(bridgeComponent(), 10); err != nil || len(claimed) != 0 { t.Fatalf("protected request must not be exposed to the Companion: commands=%#v err=%v", claimed, err) } claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: "run-local", SessionToken: run.SessionToken, Capabilities: []string{domain.JobCapabilityRemoteRunProtectedSQL}, Capacity: domain.RunCapacity{MaxJobs: 1}}) if err != nil || !claim.HasJob || claim.Job == nil || claim.Job.JobID != command.RunJobID || claim.Job.FencingToken == 0 { t.Fatalf("claim protected Run job: claim=%#v err=%v", claim, err) } assignmentJSON, _ := json.Marshal(claim.Job) if strings.Contains(string(assignmentJSON), text) { t.Fatalf("Run assignment exposed protected request text: %s", assignmentJSON) } ack, err := svc.AckRunJob(domain.RunJobAck{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Message: "accepted"}) if err != nil || !ack.Accepted { t.Fatalf("ack protected Run job: ack=%#v err=%v", ack, err) } if _, err := svc.GetProtectedRequestExecutionInput(domain.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, FencingToken: claim.Job.FencingToken + 1}); err == nil { t.Fatal("expected fencing mismatch rejection") } input, err := svc.GetProtectedRequestExecutionInput(domain.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, FencingToken: claim.Job.FencingToken}) if err != nil || input.RequestText != text || input.Kind != "sql" || input.TransportKey != "scum-database" || !input.Authorized { t.Fatalf("read protected Run input: input=%#v err=%v", input, err) } if _, err := svc.GetProtectedRequestExecutionInput(domain.ProtectedRequestExecutionInputRequest{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, FencingToken: claim.Job.FencingToken}); err == nil { t.Fatal("expected one-time protected input rejection") } if _, err := svc.CompleteRunJob(domain.RunJobResult{RunEndpointID: "run-local", SessionToken: run.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, State: domain.JobStateFailed, Progress: domain.RunJobProgressReport{Percent: 100, Message: "unknown request"}, ErrorCode: "protected_request_unknown", ExecutionResult: domain.JobExecutionResult{Kind: "protected.sql.unknown", AuditSummary: "protected request outcome is unknown"}}); err != nil { t.Fatalf("complete protected Run job: %v", err) } completed, err := svc.store.GameClientBridgeCommands().Get(command.ID) if err != nil || completed.State != domain.GameClientBridgeCommandUnknown || completed.Result.Status != domain.GameClientBridgeResultUnknown || strings.Contains(completed.Result.Summary, text) { t.Fatalf("project protected Run result: command=%#v err=%v", completed, err) } } func TestGameClientBridgeIdempotencyScopeIsAppliedByService(t *testing.T) { svc, clock := newGameClientBridgeService(t) request := bridgeQueueRequest(*clock, "scope-key") first, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil { t.Fatal(err) } request.Payload = map[string]any{"message": "changed but same idempotency scope"} reused, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil || reused.ID != first.ID { t.Fatalf("same service idempotency scope was not reused: first=%#v reused=%#v err=%v", first, reused, err) } otherRequester, err := svc.queueGameClientBridgeCommand("user-2", request) if err != nil || otherRequester.ID == first.ID { t.Fatalf("requester was omitted from idempotency scope: %#v err=%v", otherRequester, err) } request.IdempotencyKey = "scope-key-2" otherKey, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil || otherKey.ID == first.ID { t.Fatalf("idempotency key was omitted from service scope: %#v err=%v", otherKey, err) } request.ServerInstanceID = "server-2" request.IdempotencyKey = "scope-key" otherServer, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil || otherServer.ID == first.ID { t.Fatalf("server was omitted from service idempotency scope: %#v err=%v", otherServer, err) } } func TestGameClientBridgeLeaseReclaimAndExpiry(t *testing.T) { svc, clock := newGameClientBridgeService(t) command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "lease-1")) if err != nil { t.Fatal(err) } component := bridgeComponent() first, err := svc.claimGameClientBridgeCommands(component, 1) if err != nil || len(first) != 1 { t.Fatalf("first claim: %#v err=%v", first, err) } *clock = clock.Add(defaultGameClientBridgeLeaseDuration + time.Second) second, err := svc.claimGameClientBridgeCommands(component, 1) if err != nil || len(second) != 1 || second[0].Claim.FencingToken != 2 { t.Fatalf("reclaim expired lease: %#v err=%v", second, err) } if _, err := svc.completeGameClientBridgeCommand(component, domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: 1, Status: domain.GameClientBridgeResultSucceeded}); err == nil { t.Fatal("expected old claim fencing rejection") } *clock = command.ExpiresAt.Add(time.Second) if err := svc.ReconcileGameClientBridgeCommands(); err != nil { t.Fatalf("reconcile expired command: %v", err) } expired, err := svc.store.GameClientBridgeCommands().Get(command.ID) if err != nil || expired.State != domain.GameClientBridgeCommandExpired { t.Fatalf("expected expired command: %#v err=%v", expired, err) } claimed, err := svc.claimGameClientBridgeCommands(component, 1) if err != nil || len(claimed) != 0 { t.Fatalf("expired command was claimable: %#v err=%v", claimed, err) } } func TestGameClientBridgePendingCommandExpiresBeforeFirstClaim(t *testing.T) { svc, clock := newGameClientBridgeService(t) request := bridgeQueueRequest(*clock, "pending-expiry") request.ExpiresAt = clock.Add(30 * time.Second) command, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil { t.Fatal(err) } *clock = request.ExpiresAt claimed, err := svc.claimGameClientBridgeCommands(bridgeComponent(), 1) if err != nil || len(claimed) != 0 { t.Fatalf("expired pending command was claimable: %#v err=%v", claimed, err) } expired, err := svc.store.GameClientBridgeCommands().Get(command.ID) if err != nil || expired.State != domain.GameClientBridgeCommandExpired || expired.CompletedAt.IsZero() || expired.Claim.FencingToken != 0 || len(expired.AuditReferences) != 2 { t.Fatalf("first claim did not persist pending command expiry: %#v err=%v", expired, err) } } func TestGameClientBridgeExpiredLeaseRejectsMutationsBeforeReclaim(t *testing.T) { svc, clock := newGameClientBridgeService(t) for _, key := range []string{"expired-lease-ack", "expired-lease-result"} { if _, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, key)); err != nil { t.Fatalf("queue %s: %v", key, err) } } component := bridgeComponent() claimed, err := svc.claimGameClientBridgeCommands(component, 2) if err != nil || len(claimed) != 2 { t.Fatalf("claim lease-expiry commands: %#v err=%v", claimed, err) } *clock = claimed[0].Claim.LeaseExpiresAt if _, err := svc.ackGameClientBridgeCommand(component, domain.GameClientBridgeAckRequest{SessionToken: "session-token", CommandID: claimed[0].ID, FencingToken: claimed[0].Claim.FencingToken}); err == nil { t.Fatal("expected ack at claim lease expiry to be rejected") } if _, err := svc.completeGameClientBridgeCommand(component, domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: claimed[1].ID, FencingToken: claimed[1].Claim.FencingToken, Status: domain.GameClientBridgeResultSucceeded}); err == nil { t.Fatal("expected result at claim lease expiry to be rejected") } for _, command := range claimed { protected, getErr := svc.store.GameClientBridgeCommands().Get(command.ID) if getErr != nil || protected.State != domain.GameClientBridgeCommandClaimed || !protected.Claim.AcknowledgedAt.IsZero() || protected.Result.Status != "" || !protected.CompletedAt.IsZero() || protected.Claim.FencingToken != command.Claim.FencingToken || len(protected.AuditReferences) != 2 { t.Fatalf("expired lease mutation changed protected command: %#v err=%v", protected, getErr) } } reclaimed, err := svc.claimGameClientBridgeCommands(component, 2) if err != nil || len(reclaimed) != 2 { t.Fatalf("reclaim protected commands after lease sweep: %#v err=%v", reclaimed, err) } for _, command := range reclaimed { if command.Claim.FencingToken != 2 { t.Fatalf("reclaimed command did not advance fencing token: %#v", command) } } } func TestGameClientBridgeClaimMutationsExpireAtCommandDeadline(t *testing.T) { svc, clock := newGameClientBridgeService(t) deadline := clock.Add(30 * time.Second) commands := make([]domain.GameClientBridgeCommand, 0, 2) for _, key := range []string{"deadline-ack", "deadline-result"} { request := bridgeQueueRequest(*clock, key) request.ExpiresAt = deadline command, err := svc.queueGameClientBridgeCommand("user-1", request) if err != nil { t.Fatalf("queue deadline command: %v", err) } commands = append(commands, command) } component := bridgeComponent() claimed, err := svc.claimGameClientBridgeCommands(component, 2) if err != nil || len(claimed) != 2 { t.Fatalf("claim deadline commands: %#v err=%v", claimed, err) } for _, command := range claimed { if !command.Claim.LeaseExpiresAt.Equal(deadline) { t.Fatalf("claim lease exceeded command deadline: %#v", command.Claim) } } *clock = deadline if _, err := svc.ackGameClientBridgeCommand(component, domain.GameClientBridgeAckRequest{SessionToken: "session-token", CommandID: claimed[0].ID, FencingToken: claimed[0].Claim.FencingToken}); err == nil { t.Fatal("expected ack at command deadline to be rejected") } if _, err := svc.completeGameClientBridgeCommand(component, domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: claimed[1].ID, FencingToken: claimed[1].Claim.FencingToken, Status: domain.GameClientBridgeResultSucceeded}); err == nil { t.Fatal("expected result at command deadline to be rejected") } for _, command := range commands { expired, err := svc.store.GameClientBridgeCommands().Get(command.ID) if err != nil || expired.State != domain.GameClientBridgeCommandExpired || expired.CompletedAt.IsZero() || len(expired.AuditReferences) < 3 { t.Fatalf("deadline mutation did not persist audited expiry: %#v err=%v", expired, err) } } } func TestGameClientBridgeFailedResultIsPersisted(t *testing.T) { svc, clock := newGameClientBridgeService(t) command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "failed-result")) if err != nil { t.Fatal(err) } component := bridgeComponent() claimed, err := svc.claimGameClientBridgeCommands(component, 1) if err != nil || len(claimed) != 1 { t.Fatalf("claim failed-result command: %#v err=%v", claimed, err) } request := domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken, Status: domain.GameClientBridgeResultFailed, Summary: "game window unavailable", Payload: map[string]any{"retryable": true}} failed, err := svc.completeGameClientBridgeCommand(component, request) if err != nil || failed.State != domain.GameClientBridgeCommandFailed || failed.Result.Status != domain.GameClientBridgeResultFailed || failed.Result.Summary != request.Summary || failed.Result.CompletedBy != component.Session.ID || failed.CompletedAt.IsZero() || len(failed.AuditReferences) != 3 { t.Fatalf("record failed result: %#v err=%v", failed, err) } persisted, err := svc.store.GameClientBridgeCommands().Get(command.ID) if err != nil || persisted.State != domain.GameClientBridgeCommandFailed || persisted.Result.Status != domain.GameClientBridgeResultFailed || persisted.Result.Payload["retryable"] != true || !persisted.CompletedAt.Equal(failed.CompletedAt) || len(persisted.AuditReferences) != len(failed.AuditReferences) { t.Fatalf("failed result was not persisted: %#v err=%v", persisted, err) } } func TestGameClientBridgeOperatorCancellationExpiresAtCommandDeadline(t *testing.T) { svc, clock := newGameClientBridgeService(t) user := domain.User{ID: "user-1", DisplayName: "Owner", Roles: []string{"server-owner"}, Status: domain.UserStatusActive, CreatedAt: *clock, UpdatedAt: *clock} if err := svc.store.Users().Create(user); err != nil { t.Fatal(err) } auth, err := svc.issueAuthSession(user, "test") if err != nil { t.Fatal(err) } if err := svc.store.ServerInstances().Create(domain.ServerInstance{ID: "server-1", PluginID: "game.scum", OwnerUserID: user.ID}); err != nil { t.Fatal(err) } request := bridgeQueueRequest(*clock, "cancel-deadline") request.ExpiresAt = clock.Add(30 * time.Second) command, err := svc.queueGameClientBridgeCommand(user.ID, request) if err != nil { t.Fatal(err) } *clock = request.ExpiresAt if _, err := svc.CancelGameClientBridgeCommandForSession(auth.SessionID, domain.GameClientBridgeCancelRequest{CommandID: command.ID, Reason: "too late"}); err == nil { t.Fatal("expected cancellation at command deadline to be rejected") } expired, err := svc.store.GameClientBridgeCommands().Get(command.ID) if err != nil || expired.State != domain.GameClientBridgeCommandExpired || expired.CompletedAt.IsZero() || expired.Cancellation.RequestedBy != "" || len(expired.AuditReferences) != 2 { t.Fatalf("deadline cancellation did not preserve audited expiry: %#v err=%v", expired, err) } } func TestGameClientBridgeOperatorCancellationRejectsLateSuccess(t *testing.T) { svc, clock := newGameClientBridgeService(t) user := domain.User{ID: "user-1", DisplayName: "Owner", Roles: []string{"server-owner"}, Status: domain.UserStatusActive, CreatedAt: *clock, UpdatedAt: *clock} if err := svc.store.Users().Create(user); err != nil { t.Fatal(err) } auth, err := svc.issueAuthSession(user, "test") if err != nil { t.Fatal(err) } if err := svc.store.ServerInstances().Create(domain.ServerInstance{ID: "server-1", PluginID: "game.scum", OwnerUserID: user.ID}); err != nil { t.Fatal(err) } command, err := svc.queueGameClientBridgeCommand(user.ID, bridgeQueueRequest(*clock, "cancel-1")) if err != nil { t.Fatal(err) } component := bridgeComponent() claimed, err := svc.claimGameClientBridgeCommands(component, 1) if err != nil || len(claimed) != 1 { t.Fatalf("claim: %#v err=%v", claimed, err) } cancelled, err := svc.CancelGameClientBridgeCommandForSession(auth.SessionID, domain.GameClientBridgeCancelRequest{CommandID: command.ID, Reason: "operator requested"}) if err != nil || cancelled.State != domain.GameClientBridgeCommandCancelled || cancelled.Cancellation.RequestedBy != user.ID { t.Fatalf("cancel bridge command: %#v err=%v", cancelled, err) } auditReferenceCount := len(cancelled.AuditReferences) repeated, err := svc.CancelGameClientBridgeCommandForSession(auth.SessionID, domain.GameClientBridgeCancelRequest{CommandID: command.ID, Reason: "operator requested"}) if err != nil || repeated.State != domain.GameClientBridgeCommandCancelled || !repeated.Cancellation.CancelledAt.Equal(cancelled.Cancellation.CancelledAt) || len(repeated.AuditReferences) != auditReferenceCount { t.Fatalf("repeated cancellation was not idempotent: %#v err=%v", repeated, err) } remaining, err := svc.claimGameClientBridgeCommands(component, 1) if err != nil || len(remaining) != 0 { t.Fatalf("cancelled command remained claimable: %#v err=%v", remaining, err) } if _, err := svc.completeGameClientBridgeCommand(component, domain.GameClientBridgeResultRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken, Status: domain.GameClientBridgeResultSucceeded, Summary: "late success"}); err == nil { t.Fatal("expected late success after cancellation rejection") } if _, err := svc.ackGameClientBridgeCommand(component, domain.GameClientBridgeAckRequest{SessionToken: "session-token", CommandID: command.ID, FencingToken: claimed[0].Claim.FencingToken}); err == nil { t.Fatal("expected late ack after cancellation rejection") } } func TestGameClientBridgeReconciliationPrunesRetentionWithoutResettingStreamSequence(t *testing.T) { svc, clock := newGameClientBridgeService(t) plugin, err := svc.store.GamePlugins().Get("game.scum") if err != nil { t.Fatal(err) } plugin.GameClientBridge.Retention = domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 2} plugin.GameClientBridge.Snapshots[0].Retention = domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 2} if err := svc.store.GamePlugins().Update(plugin); err != nil { t.Fatal(err) } oldCommand := domain.GameClientBridgeCommand{ID: "old-command", ServerInstanceID: "server-1", PluginID: "game.scum", State: domain.GameClientBridgeCommandSucceeded, CompletedAt: clock.Add(-2 * time.Hour)} if err := svc.store.GameClientBridgeCommands().Create(oldCommand); err != nil { t.Fatal(err) } for sequence := uint64(1); sequence <= 4; sequence++ { snapshot := domain.GameClientBridgeSnapshot{ID: "snapshot-" + string(rune('0'+sequence)), ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", SchemaVersion: "1", StreamKey: "current", Sequence: sequence, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 2}, ExpiresAt: clock.Add(time.Hour)} if sequence == 1 { snapshot.ExpiresAt = clock.Add(-time.Second) } if err := svc.store.GameClientBridgeSnapshots().Create(snapshot); err != nil { t.Fatal(err) } } stream := domain.GameClientBridgeSnapshotStream{ID: gameClientBridgeStreamID("server-1", "game.scum", "scum-client", "players", "current"), ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", Type: "players", StreamKey: "current", LatestSequence: 4} if err := svc.store.GameClientBridgeSnapshotStreams().Create(stream); err != nil { t.Fatal(err) } if err := svc.ReconcileGameClientBridgeCommands(); err != nil { t.Fatalf("reconcile bridge retention: %v", err) } if _, err := svc.store.GameClientBridgeCommands().Get(oldCommand.ID); err == nil { t.Fatal("old terminal command was not pruned") } snapshots, err := svc.store.GameClientBridgeSnapshots().List(domain.GameClientBridgeSnapshotFilter{ServerInstanceID: "server-1", Type: "players"}) if err != nil || len(snapshots) != 2 || snapshots[0].Sequence != 4 || snapshots[1].Sequence != 3 { t.Fatalf("snapshot retention projection: %#v err=%v", snapshots, err) } retainedStream, err := svc.store.GameClientBridgeSnapshotStreams().Get(stream.ID) if err != nil || retainedStream.LatestSequence != 4 { t.Fatalf("stream sequence was reset by retention: %#v err=%v", retainedStream, err) } }