Remove legacy client-manager workflows

This commit is contained in:
npc0-hue
2026-09-03 13:08:08 +08:00
parent bf3c382d15
commit fe09d21a56
56 changed files with 304 additions and 4121 deletions
+14 -198
View File
@@ -12,7 +12,7 @@ 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: "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}}, {Type: "companion.health", SchemaVersion: "1", Retention: domain.GameClientBridgeRetention{KeepForSeconds: 3600, MaxRecords: 100}}}, Retention: domain.GameClientBridgeRetention{KeepForSeconds: 86400, MaxRecords: 1000}}}
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)
}
@@ -21,23 +21,19 @@ func newGameClientBridgeService(t *testing.T) (*CoreService, *time.Time) {
}
func bridgeQueueRequest(now time.Time, key string) domain.GameClientBridgeQueueRequest {
return domain.GameClientBridgeQueueRequest{ServerInstanceID: "server-1", PluginID: "game.scum", ProfileKey: "scum-client", CommandType: "diagnostic.ping", Payload: map[string]any{"message": "hello"}, IdempotencyKey: key, Priority: 10, ExpiresAt: now.Add(5 * time.Minute)}
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 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) {
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)
@@ -46,36 +42,6 @@ func TestGameClientBridgeCommandLifecycleAndIdempotency(t *testing.T) {
if len(commands) != 1 {
t.Fatalf("expected one durable 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)
}
replayed, err := svc.completeGameClientBridgeCommand(component, resultRequest)
if err != nil || replayed.ID != completed.ID || replayed.State != completed.State || !replayed.CompletedAt.Equal(completed.CompletedAt) {
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 TestGameClientBridgeIdempotencyScopeIsAppliedByService(t *testing.T) {
@@ -107,154 +73,19 @@ func TestGameClientBridgeIdempotencyScopeIsAppliedByService(t *testing.T) {
}
}
func TestGameClientBridgeLeaseReclaimAndExpiry(t *testing.T) {
func TestGameClientBridgeReconciliationExpiresPendingCommand(t *testing.T) {
svc, clock := newGameClientBridgeService(t)
command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "lease-1"))
command, err := svc.queueGameClientBridgeCommand("user-1", bridgeQueueRequest(*clock, "expires-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 {
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 {
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() {
t.Fatalf("deadline mutation did not persist 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() {
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) {
t.Fatalf("failed result was not persisted: %#v err=%v", persisted, err)
if err != nil || expired.State != domain.GameClientBridgeCommandExpired || expired.Claim.SessionID != "" {
t.Fatalf("expected expired unclaimed command: %#v err=%v", expired, err)
}
}
@@ -287,7 +118,7 @@ func TestGameClientBridgeOperatorCancellationExpiresAtCommandDeadline(t *testing
}
}
func TestGameClientBridgeOperatorCancellationRejectsLateSuccess(t *testing.T) {
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 {
@@ -304,29 +135,14 @@ func TestGameClientBridgeOperatorCancellationRejectsLateSuccess(t *testing.T) {
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 {
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)
}
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) {
@@ -345,7 +161,7 @@ func TestGameClientBridgeReconciliationPrunesRetentionWithoutResettingStreamSequ
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)}
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)
}
@@ -353,7 +169,7 @@ func TestGameClientBridgeReconciliationPrunesRetentionWithoutResettingStreamSequ
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}
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)
}