mirror of
https://github.com/coder/coder.git
synced 2026-11-01 04:47:17 +08:00
Moves chat model override settings from deployment scope to organization scope, following the org-scoping of chat model configs in #28440. Depends on #28440 (based on `ethan/chat-models-availability-consolidation`). Remote dogfood UAT passed on this change, including multi-organization scenarios on a licensed deployment with real models. ## Problem Chat model configs are now owned by organizations, but the override settings that reference them stayed deployment-wide: admin subagent/title/compaction overrides in `site_configs`, personal overrides in `user_configs`, and the advisor model embedded in the advisor runtime JSON. A single deployment-level override can only point at one organization's model config, so overrides effectively worked for the default organization only, and title generation had to soften its hard-failure contract to tolerate unusable overrides. ## Fix Admin overrides (general, explore, title generation, compaction, advisor) and personal overrides (root, general, explore) become organization-scoped rows in two new typed tables, `chat_organization_model_overrides` and `chat_user_model_overrides`. Composite foreign keys on `(organization_id, model_config_id)` make cross-organization model references unrepresentable. The migration deletes the legacy serialized `site_configs`/`user_configs` override keys instead of migrating them; admins and users re-select models in the new organization-scoped settings. New endpoints live under `/api/experimental/organizations/{organization}/chats/model-overrides` (admin defaults) and `/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides` (personal overrides); the legacy deployment-level routes are removed. chatd resolves overrides via the chat's organization, restoring the hard-failure contract for configured-but-unusable title generation overrides. The deployment-level `AllowUsers` switch and advisor runtime limits stay deployment-scoped. The Coder Agents settings UI gains a per-organization "Defaults & overrides" page under AI Settings > Models, and the personal overrides page gains an organization selector for users in multiple organizations. Advisor telemetry now reports per-organization override rows. **BREAKING CHANGE**: Existing deployment-level overrides are dropped, not migrated. Every organization starts with no overrides after upgrade, and model selections must be re-applied in the new settings. The legacy deployment-level override endpoints are removed. Closes https://linear.app/coder/issue/CODAGT-872 > [!NOTE] > Xum (AI agent) authored this PR on behalf of @ibetitsmike. <!-- xum-attribution: model=claude-opus-4-6 thinking=high --> --------- Co-authored-by: Ethan Dickson <ethan@coder.com>
120 lines
2.7 KiB
Go
120 lines
2.7 KiB
Go
package ptr_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
)
|
|
|
|
func Test_Ref_Deref(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("String", func(t *testing.T) {
|
|
t.Parallel()
|
|
val := "test"
|
|
p := ptr.Ref(val)
|
|
assert.Equal(t, &val, p)
|
|
})
|
|
|
|
t.Run("Bool", func(t *testing.T) {
|
|
t.Parallel()
|
|
val := true
|
|
p := ptr.Ref(val)
|
|
assert.Equal(t, &val, p)
|
|
})
|
|
|
|
t.Run("Int64", func(t *testing.T) {
|
|
t.Parallel()
|
|
val := int64(42)
|
|
p := ptr.Ref(val)
|
|
assert.Equal(t, &val, p)
|
|
})
|
|
|
|
t.Run("Float64", func(t *testing.T) {
|
|
t.Parallel()
|
|
val := float64(3.14159)
|
|
p := ptr.Ref(val)
|
|
assert.Equal(t, &val, p)
|
|
})
|
|
}
|
|
|
|
func Test_NilOrEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
nilString := (*string)(nil)
|
|
emptyString := ""
|
|
nonEmptyString := "hi"
|
|
|
|
assert.True(t, ptr.NilOrEmpty(nilString))
|
|
assert.True(t, ptr.NilOrEmpty(&emptyString))
|
|
assert.False(t, ptr.NilOrEmpty(&nonEmptyString))
|
|
}
|
|
|
|
func Test_NilToEmpty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.False(t, ptr.NilToEmpty((*bool)(nil)))
|
|
assert.Empty(t, ptr.NilToEmpty((*int64)(nil)))
|
|
assert.Empty(t, ptr.NilToEmpty((*string)(nil)))
|
|
assert.Equal(t, true, ptr.NilToEmpty(ptr.Ref(true)))
|
|
}
|
|
|
|
func Test_NilToDefault(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.True(t, ptr.NilToDefault(ptr.Ref(true), false))
|
|
assert.True(t, ptr.NilToDefault((*bool)(nil), true))
|
|
|
|
assert.Equal(t, int64(4), ptr.NilToDefault(ptr.Ref[int64](4), 5))
|
|
assert.Equal(t, int64(5), ptr.NilToDefault((*int64)(nil), 5))
|
|
|
|
assert.Equal(t, "hi", ptr.NilToDefault((*string)(nil), "hi"))
|
|
assert.Equal(t, "hello", ptr.NilToDefault(ptr.Ref("hello"), "hi"))
|
|
}
|
|
|
|
func Test_NilOrZero(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
nilInt64 := (*int64)(nil)
|
|
nilFloat64 := (*float64)(nil)
|
|
nilDuration := (*time.Duration)(nil)
|
|
|
|
zeroInt64 := int64(0)
|
|
zeroFloat64 := float64(0.0)
|
|
zeroDuration := time.Duration(0)
|
|
|
|
nonZeroInt64 := int64(1)
|
|
nonZeroFloat64 := float64(3.14159)
|
|
nonZeroDuration := time.Hour
|
|
|
|
assert.True(t, ptr.NilOrZero(nilInt64))
|
|
assert.True(t, ptr.NilOrZero(nilFloat64))
|
|
assert.True(t, ptr.NilOrZero(nilDuration))
|
|
|
|
assert.True(t, ptr.NilOrZero(&zeroInt64))
|
|
assert.True(t, ptr.NilOrZero(&zeroFloat64))
|
|
assert.True(t, ptr.NilOrZero(&zeroDuration))
|
|
|
|
assert.False(t, ptr.NilOrZero(&nonZeroInt64))
|
|
assert.False(t, ptr.NilOrZero(&nonZeroFloat64))
|
|
assert.False(t, ptr.NilOrZero(&nonZeroDuration))
|
|
}
|
|
|
|
func Test_FromNullString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.Nil(t, ptr.FromNullString(sql.NullString{}))
|
|
assert.Nil(t, ptr.FromNullString(sql.NullString{String: "ignored"}))
|
|
|
|
valid := ptr.FromNullString(sql.NullString{String: "value", Valid: true})
|
|
assert.NotNil(t, valid)
|
|
assert.Equal(t, "value", *valid)
|
|
|
|
empty := ptr.FromNullString(sql.NullString{Valid: true})
|
|
assert.NotNil(t, empty)
|
|
assert.Equal(t, "", *empty)
|
|
}
|