diff --git a/platform/service/runtime_bindings.go b/platform/service/runtime_bindings.go index 0ebd585..9cb6d7e 100644 --- a/platform/service/runtime_bindings.go +++ b/platform/service/runtime_bindings.go @@ -102,6 +102,23 @@ func runtimeLifecycleProfile(profiles domain.GamePluginRuntimeProfiles, key stri return profile, true } } + // Older persisted bindings used "local" for the only local-process profile. + // Infer that legacy key only when the plugin has one unambiguous candidate. + if key != "local" { + return domain.RuntimeLifecycleProfile{}, false + } + var candidate domain.RuntimeLifecycleProfile + count := 0 + for _, profile := range profiles.LifecycleProfiles { + if profile.Mode != "local-process" { + continue + } + candidate = profile + count++ + } + if count == 1 { + return candidate, true + } return domain.RuntimeLifecycleProfile{}, false } @@ -152,10 +169,11 @@ func runtimeBindingKeys(profiles domain.GamePluginRuntimeProfiles, profile domai } func normalizeRuntimeBinding(plugin domain.GamePlugin, binding domain.RuntimeBinding) (domain.RuntimeBinding, error) { - profile, _ := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey) - if profile.Key == "" { + profile, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey) + if !ok { return domain.RuntimeBinding{}, validationError("runtime profile is no longer declared") } + binding.ProfileKey = profile.Key required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile) for key := range binding.Bindings { if _, ok := allowed[key]; !ok { diff --git a/platform/service/runtime_bindings_test.go b/platform/service/runtime_bindings_test.go index 5f70e6a..61a67e0 100644 --- a/platform/service/runtime_bindings_test.go +++ b/platform/service/runtime_bindings_test.go @@ -41,6 +41,29 @@ func TestRegisteredRuntimeProfilesSurviveFileStoreReload(t *testing.T) { } } +func TestLegacyLocalRuntimeBindingCanonicalizesToUnambiguousLocalProcessProfile(t *testing.T) { + plugin := domain.GamePlugin{RuntimeProfiles: domain.GamePluginRuntimeProfiles{ + LifecycleProfiles: []domain.RuntimeLifecycleProfile{{Key: "run-local", Mode: "local-process"}}, + }} + binding, err := normalizeRuntimeBinding(plugin, domain.RuntimeBinding{ID: "binding-legacy", ServerInstanceID: "server-legacy", PluginID: "plugin-legacy", PluginVersion: "1.0.0", ProfileKey: "local", Bindings: map[string]string{}, CreatedAt: fixedTime, UpdatedAt: fixedTime}) + if err != nil { + t.Fatalf("normalize legacy runtime binding: %v", err) + } + if binding.ProfileKey != "run-local" || binding.Mode != "local-process" { + t.Fatalf("expected canonical local-process profile, got %+v", binding) + } +} + +func TestLegacyLocalRuntimeBindingDoesNotGuessAmbiguousProfiles(t *testing.T) { + profiles := domain.GamePluginRuntimeProfiles{LifecycleProfiles: []domain.RuntimeLifecycleProfile{ + {Key: "run-local", Mode: "local-process"}, + {Key: "run-local-alt", Mode: "local-process"}, + }} + if _, ok := runtimeLifecycleProfile(profiles, "local"); ok { + t.Fatal("expected ambiguous legacy local profile lookup to fail") + } +} + func TestRuntimeBindingValidationAndLifecycleGating(t *testing.T) { svc := newTestCoreService() plugin, endpoint := createPluginAndRunEndpoint(t, svc) diff --git a/platform/service/server_lifecycle.go b/platform/service/server_lifecycle.go index 54b0de5..6da2fb7 100644 --- a/platform/service/server_lifecycle.go +++ b/platform/service/server_lifecycle.go @@ -441,12 +441,7 @@ func runtimeProfileActionRef(actions domain.PluginLifecycleActions, action domai } func runtimeLifecycleProfileForKey(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) { - for _, profile := range profiles.LifecycleProfiles { - if profile.Key == key { - return profile, true - } - } - return domain.RuntimeLifecycleProfile{}, false + return runtimeLifecycleProfile(profiles, key) } func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) ([]domain.RuntimeDLLExtensionPlan, error) { diff --git a/platform/service/source_rcon_test.go b/platform/service/source_rcon_test.go index e99f461..cb9fec4 100644 --- a/platform/service/source_rcon_test.go +++ b/platform/service/source_rcon_test.go @@ -106,6 +106,26 @@ func TestSourceRCONDispatchRejectsUnsafeOrIncompatibleState(t *testing.T) { } } +func TestSourceRCONDispatchCanonicalizesLegacyLocalBinding(t *testing.T) { + svc, _, _, instance := newSourceRCONFixture(t) + plugin, err := svc.store.GamePlugins().Get(instance.PluginID) + if err != nil { + t.Fatal(err) + } + plugin.RuntimeProfiles.LifecycleProfiles[0].Key = "run-local" + if err := svc.store.GamePlugins().Update(plugin); err != nil { + t.Fatalf("update runtime profile key: %v", err) + } + + resolution, err := svc.resolveSourceRCONDispatch(instance) + if err != nil { + t.Fatalf("resolve RCON dispatch with legacy binding: %v", err) + } + if resolution.binding.ProfileKey != "run-local" { + t.Fatalf("expected legacy binding to be canonicalized, got %q", resolution.binding.ProfileKey) + } +} + func TestSourceRCONBrokerExpiresWithoutReplay(t *testing.T) { stamp := fixedTime broker := newSourceRCONCommandBroker(func() time.Time { return stamp })