From 92ad16068aa3eb44a7fae352c8bfe168da4cc329 Mon Sep 17 00:00:00 2001 From: tbphp Date: Sat, 1 Aug 2026 10:45:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(groups):=20=E6=A0=A1=E9=AA=8C=E5=8D=8F?= =?UTF-8?q?=E8=AE=AE=E5=8F=98=E6=9B=B4=E5=90=8E=E7=9A=84=E7=94=A8=E9=87=8F?= =?UTF-8?q?=E6=B3=A8=E5=85=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/control/group_settings.go | 21 ++++++++++--- internal/control/group_settings_test.go | 39 +++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/internal/control/group_settings.go b/internal/control/group_settings.go index 3efddccb..81b55293 100644 --- a/internal/control/group_settings.go +++ b/internal/control/group_settings.go @@ -246,13 +246,12 @@ func (s *Service) UpdateGroupSettings( updates["weight_manual"] = normalized.weightManual } if normalized.overridesSet { - if _, injectUsageOptionsSet := normalized.overrides[state.SettingInjectUsageOptions]; injectUsageOptionsSet && - !groupSupportsOpenAICompletions(group.Protocols) { - return app_errors.ErrValidation - } group.Config = normalized.encodedOverrides updates["config"] = group.Config } + if err := validateGroupInjectUsageOptionsConstraint(group); err != nil { + return err + } if err := validateGroupRowCandidate(ctx, tx, group); err != nil { return app_errors.ErrValidation } @@ -280,6 +279,20 @@ func (s *Service) UpdateGroupSettings( return result, nil } +func validateGroupInjectUsageOptionsConstraint(group models.Group) error { + settings := make(config.Settings) + if len(group.Config) > 0 { + if err := decodeGroupDiscoveryJSON(group.Config, &settings); err != nil { + return app_errors.ErrValidation + } + } + if _, injectUsageOptionsSet := settings[state.SettingInjectUsageOptions]; injectUsageOptionsSet && + !groupSupportsOpenAICompletions(group.Protocols) { + return app_errors.ErrValidation + } + return nil +} + func groupSupportsOpenAICompletions(raw models.JSON) bool { var protocols []protocol.Protocol if err := decodeGroupDiscoveryJSON(raw, &protocols); err != nil { diff --git a/internal/control/group_settings_test.go b/internal/control/group_settings_test.go index b6885287..8f5e3cb3 100644 --- a/internal/control/group_settings_test.go +++ b/internal/control/group_settings_test.go @@ -160,6 +160,45 @@ func TestUpdateGroupSettingsValidatesWeightInjectUsageAndUpstreamConfirmation(t } } +func TestUpdateGroupSettingsRejectsProtocolChangeThatLeavesInjectUsageOptionsOnNonOpenAIGroup(t *testing.T) { + fixture := newServiceFixture(t) + created, err := fixture.service.CreateGroup(t.Context(), GroupCreateRequest{ + UpstreamURL: "https://settings-protocol-constraint.example.com/v1", + Protocols: []protocol.Protocol{protocol.OpenAICompletions}, + Config: config.Settings{ + state.SettingInjectUsageOptions: true, + }, + Keys: "sk-settings-protocol-constraint", + }) + if err != nil { + t.Fatalf("CreateGroup() error = %v", err) + } + var before models.Group + if err := fixture.db.First(&before, created.GroupID).Error; err != nil { + t.Fatal(err) + } + beforeRevision := fixture.manager.Current().Revision + + _, err = fixture.service.UpdateGroupSettings(t.Context(), created.GroupID, GroupSettingsUpdateRequest{ + Protocols: optionalField[[]protocol.Protocol]{ + Set: true, Value: []protocol.Protocol{protocol.Anthropic}, + }, + }) + if !errors.Is(err, app_errors.ErrValidation) { + t.Fatalf("UpdateGroupSettings() error = %v, want validation", err) + } + var after models.Group + if err := fixture.db.First(&after, created.GroupID).Error; err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(after, before) { + t.Fatalf("protocol constraint changed persisted group: got=%#v want=%#v", after, before) + } + if got := fixture.manager.Current().Revision; got != beforeRevision { + t.Fatalf("protocol constraint published revision %d, want %d", got, beforeRevision) + } +} + func TestGroupSettingsHTTPRejectsStrictJSONAndUnauthorizedWithoutMutation(t *testing.T) { initControlI18n(t) fixture := newServiceFixture(t)