| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- package config
- import (
- "encoding/json"
- "io/fs"
- "os"
- "path/filepath"
- "testing"
- "github.com/stretchr/testify/require"
- )
- // readConfigJSON reads and unmarshals the JSON config file at path.
- func readConfigJSON(t *testing.T, path string) map[string]any {
- t.Helper()
- baseDir := filepath.Dir(path)
- fileName := filepath.Base(path)
- b, err := fs.ReadFile(os.DirFS(baseDir), fileName)
- require.NoError(t, err)
- var out map[string]any
- require.NoError(t, json.Unmarshal(b, &out))
- return out
- }
- // readRecentModels reads the recent_models section from the config file.
- func readRecentModels(t *testing.T, path string) map[string]any {
- t.Helper()
- out := readConfigJSON(t, path)
- rm, ok := out["recent_models"].(map[string]any)
- require.True(t, ok)
- return rm
- }
- func TestRecordRecentModel_AddsAndPersists(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- err := cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "openai", Model: "gpt-4o"})
- require.NoError(t, err)
- // in-memory state
- require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 1)
- require.Equal(t, "openai", cfg.RecentModels[SelectedModelTypeLarge][0].Provider)
- require.Equal(t, "gpt-4o", cfg.RecentModels[SelectedModelTypeLarge][0].Model)
- // persisted state
- rm := readRecentModels(t, cfg.dataConfigDir)
- large, ok := rm[string(SelectedModelTypeLarge)].([]any)
- require.True(t, ok)
- require.Len(t, large, 1)
- item, ok := large[0].(map[string]any)
- require.True(t, ok)
- require.Equal(t, "openai", item["provider"])
- require.Equal(t, "gpt-4o", item["model"])
- }
- func TestRecordRecentModel_DedupeAndMoveToFront(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- // Add two entries
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "openai", Model: "gpt-4o"}))
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "anthropic", Model: "claude"}))
- // Re-add first; should move to front and not duplicate
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "openai", Model: "gpt-4o"}))
- got := cfg.RecentModels[SelectedModelTypeLarge]
- require.Len(t, got, 2)
- require.Equal(t, SelectedModel{Provider: "openai", Model: "gpt-4o"}, got[0])
- require.Equal(t, SelectedModel{Provider: "anthropic", Model: "claude"}, got[1])
- }
- func TestRecordRecentModel_TrimsToMax(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- // Insert 6 unique models; max is 5
- entries := []SelectedModel{
- {Provider: "p1", Model: "m1"},
- {Provider: "p2", Model: "m2"},
- {Provider: "p3", Model: "m3"},
- {Provider: "p4", Model: "m4"},
- {Provider: "p5", Model: "m5"},
- {Provider: "p6", Model: "m6"},
- }
- for _, e := range entries {
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, e))
- }
- // in-memory state
- got := cfg.RecentModels[SelectedModelTypeLarge]
- require.Len(t, got, 5)
- // Newest first, capped at 5: p6..p2
- require.Equal(t, SelectedModel{Provider: "p6", Model: "m6"}, got[0])
- require.Equal(t, SelectedModel{Provider: "p5", Model: "m5"}, got[1])
- require.Equal(t, SelectedModel{Provider: "p4", Model: "m4"}, got[2])
- require.Equal(t, SelectedModel{Provider: "p3", Model: "m3"}, got[3])
- require.Equal(t, SelectedModel{Provider: "p2", Model: "m2"}, got[4])
- // persisted state: verify trimmed to 5 and newest-first order
- rm := readRecentModels(t, cfg.dataConfigDir)
- large, ok := rm[string(SelectedModelTypeLarge)].([]any)
- require.True(t, ok)
- require.Len(t, large, 5)
- // Build provider:model IDs and verify order
- var ids []string
- for _, v := range large {
- m := v.(map[string]any)
- ids = append(ids, m["provider"].(string)+":"+m["model"].(string))
- }
- require.Equal(t, []string{"p6:m6", "p5:m5", "p4:m4", "p3:m3", "p2:m2"}, ids)
- }
- func TestRecordRecentModel_SkipsEmptyValues(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- // Missing provider
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "", Model: "m"}))
- // Missing model
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "p", Model: ""}))
- _, ok := cfg.RecentModels[SelectedModelTypeLarge]
- // Map may be initialized, but should have no entries
- if ok {
- require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 0)
- }
- // No file should be written (stat via fs.FS)
- baseDir := filepath.Dir(cfg.dataConfigDir)
- fileName := filepath.Base(cfg.dataConfigDir)
- _, err := fs.Stat(os.DirFS(baseDir), fileName)
- require.True(t, os.IsNotExist(err))
- }
- func TestRecordRecentModel_NoPersistOnNoop(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- entry := SelectedModel{Provider: "openai", Model: "gpt-4o"}
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, entry))
- baseDir := filepath.Dir(cfg.dataConfigDir)
- fileName := filepath.Base(cfg.dataConfigDir)
- before, err := fs.ReadFile(os.DirFS(baseDir), fileName)
- require.NoError(t, err)
- // Get file ModTime to verify no write occurs
- stBefore, err := fs.Stat(os.DirFS(baseDir), fileName)
- require.NoError(t, err)
- beforeMod := stBefore.ModTime()
- // Re-record same entry should be a no-op (no write)
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, entry))
- after, err := fs.ReadFile(os.DirFS(baseDir), fileName)
- require.NoError(t, err)
- require.Equal(t, string(before), string(after))
- // Verify ModTime unchanged to ensure truly no write occurred
- stAfter, err := fs.Stat(os.DirFS(baseDir), fileName)
- require.NoError(t, err)
- require.True(t, stAfter.ModTime().Equal(beforeMod), "file ModTime should not change on noop")
- }
- func TestUpdatePreferredModel_UpdatesRecents(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- sel := SelectedModel{Provider: "openai", Model: "gpt-4o"}
- require.NoError(t, cfg.UpdatePreferredModel(SelectedModelTypeSmall, sel))
- // in-memory
- require.Equal(t, sel, cfg.Models[SelectedModelTypeSmall])
- require.Len(t, cfg.RecentModels[SelectedModelTypeSmall], 1)
- // persisted (read via fs.FS)
- rm := readRecentModels(t, cfg.dataConfigDir)
- small, ok := rm[string(SelectedModelTypeSmall)].([]any)
- require.True(t, ok)
- require.Len(t, small, 1)
- }
- func TestRecordRecentModel_TypeIsolation(t *testing.T) {
- t.Parallel()
- dir := t.TempDir()
- cfg := &Config{}
- cfg.setDefaults(dir, "")
- cfg.dataConfigDir = filepath.Join(dir, "config.json")
- // Add models to both large and small types
- largeModel := SelectedModel{Provider: "openai", Model: "gpt-4o"}
- smallModel := SelectedModel{Provider: "anthropic", Model: "claude"}
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, largeModel))
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeSmall, smallModel))
- // in-memory: verify types maintain separate histories
- require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 1)
- require.Len(t, cfg.RecentModels[SelectedModelTypeSmall], 1)
- require.Equal(t, largeModel, cfg.RecentModels[SelectedModelTypeLarge][0])
- require.Equal(t, smallModel, cfg.RecentModels[SelectedModelTypeSmall][0])
- // Add another to large, verify small unchanged
- anotherLarge := SelectedModel{Provider: "google", Model: "gemini"}
- require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, anotherLarge))
- require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 2)
- require.Len(t, cfg.RecentModels[SelectedModelTypeSmall], 1)
- require.Equal(t, smallModel, cfg.RecentModels[SelectedModelTypeSmall][0])
- // persisted state: verify both types exist with correct lengths and contents
- rm := readRecentModels(t, cfg.dataConfigDir)
- large, ok := rm[string(SelectedModelTypeLarge)].([]any)
- require.True(t, ok)
- require.Len(t, large, 2)
- // Verify newest first for large type
- require.Equal(t, "google", large[0].(map[string]any)["provider"])
- require.Equal(t, "gemini", large[0].(map[string]any)["model"])
- require.Equal(t, "openai", large[1].(map[string]any)["provider"])
- require.Equal(t, "gpt-4o", large[1].(map[string]any)["model"])
- small, ok := rm[string(SelectedModelTypeSmall)].([]any)
- require.True(t, ok)
- require.Len(t, small, 1)
- require.Equal(t, "anthropic", small[0].(map[string]any)["provider"])
- require.Equal(t, "claude", small[0].(map[string]any)["model"])
- }
|