package service import ( "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", GameClientBridge: domain.GameClientBridgeManifest{Commands: []domain.GameClientBridgeCommandDeclaration{{Type: "diagnostic.ping", 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}}}, 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: "plugin-owned", CommandType: "diagnostic.ping", Payload: map[string]any{"message": "hello"}, IdempotencyKey: key, Priority: 10, ExpiresAt: now.Add(5 * time.Minute)} } func TestGameClientBridgeCommandQueueAndIdempotency(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) } if command.State != domain.GameClientBridgeCommandPending || command.Claim.SessionID != "" { t.Fatalf("queued command should remain unclaimed: %#v", command) } 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 { t.Fatalf("expected one durable command: %#v", commands) } } 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 TestGameClientBridgeReconciliationExpiresPendingCommand(t *testing.T) { svc, clock := newGameClientBridgeService(t) command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "expires-1")) if err != nil { t.Fatal(err) } *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 || expired.Claim.SessionID != "" { t.Fatalf("expected expired unclaimed command: %#v err=%v", expired, 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 != "" { t.Fatalf("deadline cancellation did not preserve expiry: %#v err=%v", expired, err) } } func TestGameClientBridgeOperatorCancellationIsIdempotentAndTerminal(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) } 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 || cancelled.Result.Status != domain.GameClientBridgeResultCancelled { t.Fatalf("cancel bridge command: %#v err=%v", cancelled, err) } 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) { t.Fatalf("repeated cancellation was not idempotent: %#v err=%v", repeated, err) } } 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: "plugin-owned", 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: "stream-players-current", ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "plugin-owned", 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) } }