fix legacy SCUM RCON runtime profile resolution

This commit is contained in:
npc0-hue
2026-08-24 11:27:08 +08:00
parent b69939a5ce
commit dc9b0eaf2a
4 changed files with 64 additions and 8 deletions
+20 -2
View File
@@ -102,6 +102,23 @@ func runtimeLifecycleProfile(profiles domain.GamePluginRuntimeProfiles, key stri
return profile, true 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 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) { func normalizeRuntimeBinding(plugin domain.GamePlugin, binding domain.RuntimeBinding) (domain.RuntimeBinding, error) {
profile, _ := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey) profile, ok := runtimeLifecycleProfile(plugin.RuntimeProfiles, binding.ProfileKey)
if profile.Key == "" { if !ok {
return domain.RuntimeBinding{}, validationError("runtime profile is no longer declared") return domain.RuntimeBinding{}, validationError("runtime profile is no longer declared")
} }
binding.ProfileKey = profile.Key
required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile) required, allowed := runtimeBindingKeys(plugin.RuntimeProfiles, profile)
for key := range binding.Bindings { for key := range binding.Bindings {
if _, ok := allowed[key]; !ok { if _, ok := allowed[key]; !ok {
+23
View File
@@ -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) { func TestRuntimeBindingValidationAndLifecycleGating(t *testing.T) {
svc := newTestCoreService() svc := newTestCoreService()
plugin, endpoint := createPluginAndRunEndpoint(t, svc) plugin, endpoint := createPluginAndRunEndpoint(t, svc)
+1 -6
View File
@@ -441,12 +441,7 @@ func runtimeProfileActionRef(actions domain.PluginLifecycleActions, action domai
} }
func runtimeLifecycleProfileForKey(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) { func runtimeLifecycleProfileForKey(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) {
for _, profile := range profiles.LifecycleProfiles { return runtimeLifecycleProfile(profiles, key)
if profile.Key == key {
return profile, true
}
}
return domain.RuntimeLifecycleProfile{}, false
} }
func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) ([]domain.RuntimeDLLExtensionPlan, error) { func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) ([]domain.RuntimeDLLExtensionPlan, error) {
+20
View File
@@ -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) { func TestSourceRCONBrokerExpiresWithoutReplay(t *testing.T) {
stamp := fixedTime stamp := fixedTime
broker := newSourceRCONCommandBroker(func() time.Time { return stamp }) broker := newSourceRCONCommandBroker(func() time.Time { return stamp })