recent_models_test.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. package config
  2. import (
  3. "encoding/json"
  4. "io/fs"
  5. "os"
  6. "path/filepath"
  7. "testing"
  8. "github.com/stretchr/testify/require"
  9. )
  10. // readConfigJSON reads and unmarshals the JSON config file at path.
  11. func readConfigJSON(t *testing.T, path string) map[string]any {
  12. t.Helper()
  13. baseDir := filepath.Dir(path)
  14. fileName := filepath.Base(path)
  15. b, err := fs.ReadFile(os.DirFS(baseDir), fileName)
  16. require.NoError(t, err)
  17. var out map[string]any
  18. require.NoError(t, json.Unmarshal(b, &out))
  19. return out
  20. }
  21. // readRecentModels reads the recent_models section from the config file.
  22. func readRecentModels(t *testing.T, path string) map[string]any {
  23. t.Helper()
  24. out := readConfigJSON(t, path)
  25. rm, ok := out["recent_models"].(map[string]any)
  26. require.True(t, ok)
  27. return rm
  28. }
  29. func TestRecordRecentModel_AddsAndPersists(t *testing.T) {
  30. t.Parallel()
  31. dir := t.TempDir()
  32. cfg := &Config{}
  33. cfg.setDefaults(dir, "")
  34. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  35. err := cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "openai", Model: "gpt-4o"})
  36. require.NoError(t, err)
  37. // in-memory state
  38. require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 1)
  39. require.Equal(t, "openai", cfg.RecentModels[SelectedModelTypeLarge][0].Provider)
  40. require.Equal(t, "gpt-4o", cfg.RecentModels[SelectedModelTypeLarge][0].Model)
  41. // persisted state
  42. rm := readRecentModels(t, cfg.dataConfigDir)
  43. large, ok := rm[string(SelectedModelTypeLarge)].([]any)
  44. require.True(t, ok)
  45. require.Len(t, large, 1)
  46. item, ok := large[0].(map[string]any)
  47. require.True(t, ok)
  48. require.Equal(t, "openai", item["provider"])
  49. require.Equal(t, "gpt-4o", item["model"])
  50. }
  51. func TestRecordRecentModel_DedupeAndMoveToFront(t *testing.T) {
  52. t.Parallel()
  53. dir := t.TempDir()
  54. cfg := &Config{}
  55. cfg.setDefaults(dir, "")
  56. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  57. // Add two entries
  58. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "openai", Model: "gpt-4o"}))
  59. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "anthropic", Model: "claude"}))
  60. // Re-add first; should move to front and not duplicate
  61. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "openai", Model: "gpt-4o"}))
  62. got := cfg.RecentModels[SelectedModelTypeLarge]
  63. require.Len(t, got, 2)
  64. require.Equal(t, SelectedModel{Provider: "openai", Model: "gpt-4o"}, got[0])
  65. require.Equal(t, SelectedModel{Provider: "anthropic", Model: "claude"}, got[1])
  66. }
  67. func TestRecordRecentModel_TrimsToMax(t *testing.T) {
  68. t.Parallel()
  69. dir := t.TempDir()
  70. cfg := &Config{}
  71. cfg.setDefaults(dir, "")
  72. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  73. // Insert 6 unique models; max is 5
  74. entries := []SelectedModel{
  75. {Provider: "p1", Model: "m1"},
  76. {Provider: "p2", Model: "m2"},
  77. {Provider: "p3", Model: "m3"},
  78. {Provider: "p4", Model: "m4"},
  79. {Provider: "p5", Model: "m5"},
  80. {Provider: "p6", Model: "m6"},
  81. }
  82. for _, e := range entries {
  83. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, e))
  84. }
  85. // in-memory state
  86. got := cfg.RecentModels[SelectedModelTypeLarge]
  87. require.Len(t, got, 5)
  88. // Newest first, capped at 5: p6..p2
  89. require.Equal(t, SelectedModel{Provider: "p6", Model: "m6"}, got[0])
  90. require.Equal(t, SelectedModel{Provider: "p5", Model: "m5"}, got[1])
  91. require.Equal(t, SelectedModel{Provider: "p4", Model: "m4"}, got[2])
  92. require.Equal(t, SelectedModel{Provider: "p3", Model: "m3"}, got[3])
  93. require.Equal(t, SelectedModel{Provider: "p2", Model: "m2"}, got[4])
  94. // persisted state: verify trimmed to 5 and newest-first order
  95. rm := readRecentModels(t, cfg.dataConfigDir)
  96. large, ok := rm[string(SelectedModelTypeLarge)].([]any)
  97. require.True(t, ok)
  98. require.Len(t, large, 5)
  99. // Build provider:model IDs and verify order
  100. var ids []string
  101. for _, v := range large {
  102. m := v.(map[string]any)
  103. ids = append(ids, m["provider"].(string)+":"+m["model"].(string))
  104. }
  105. require.Equal(t, []string{"p6:m6", "p5:m5", "p4:m4", "p3:m3", "p2:m2"}, ids)
  106. }
  107. func TestRecordRecentModel_SkipsEmptyValues(t *testing.T) {
  108. t.Parallel()
  109. dir := t.TempDir()
  110. cfg := &Config{}
  111. cfg.setDefaults(dir, "")
  112. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  113. // Missing provider
  114. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "", Model: "m"}))
  115. // Missing model
  116. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, SelectedModel{Provider: "p", Model: ""}))
  117. _, ok := cfg.RecentModels[SelectedModelTypeLarge]
  118. // Map may be initialized, but should have no entries
  119. if ok {
  120. require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 0)
  121. }
  122. // No file should be written (stat via fs.FS)
  123. baseDir := filepath.Dir(cfg.dataConfigDir)
  124. fileName := filepath.Base(cfg.dataConfigDir)
  125. _, err := fs.Stat(os.DirFS(baseDir), fileName)
  126. require.True(t, os.IsNotExist(err))
  127. }
  128. func TestRecordRecentModel_NoPersistOnNoop(t *testing.T) {
  129. t.Parallel()
  130. dir := t.TempDir()
  131. cfg := &Config{}
  132. cfg.setDefaults(dir, "")
  133. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  134. entry := SelectedModel{Provider: "openai", Model: "gpt-4o"}
  135. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, entry))
  136. baseDir := filepath.Dir(cfg.dataConfigDir)
  137. fileName := filepath.Base(cfg.dataConfigDir)
  138. before, err := fs.ReadFile(os.DirFS(baseDir), fileName)
  139. require.NoError(t, err)
  140. // Get file ModTime to verify no write occurs
  141. stBefore, err := fs.Stat(os.DirFS(baseDir), fileName)
  142. require.NoError(t, err)
  143. beforeMod := stBefore.ModTime()
  144. // Re-record same entry should be a no-op (no write)
  145. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, entry))
  146. after, err := fs.ReadFile(os.DirFS(baseDir), fileName)
  147. require.NoError(t, err)
  148. require.Equal(t, string(before), string(after))
  149. // Verify ModTime unchanged to ensure truly no write occurred
  150. stAfter, err := fs.Stat(os.DirFS(baseDir), fileName)
  151. require.NoError(t, err)
  152. require.True(t, stAfter.ModTime().Equal(beforeMod), "file ModTime should not change on noop")
  153. }
  154. func TestUpdatePreferredModel_UpdatesRecents(t *testing.T) {
  155. t.Parallel()
  156. dir := t.TempDir()
  157. cfg := &Config{}
  158. cfg.setDefaults(dir, "")
  159. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  160. sel := SelectedModel{Provider: "openai", Model: "gpt-4o"}
  161. require.NoError(t, cfg.UpdatePreferredModel(SelectedModelTypeSmall, sel))
  162. // in-memory
  163. require.Equal(t, sel, cfg.Models[SelectedModelTypeSmall])
  164. require.Len(t, cfg.RecentModels[SelectedModelTypeSmall], 1)
  165. // persisted (read via fs.FS)
  166. rm := readRecentModels(t, cfg.dataConfigDir)
  167. small, ok := rm[string(SelectedModelTypeSmall)].([]any)
  168. require.True(t, ok)
  169. require.Len(t, small, 1)
  170. }
  171. func TestRecordRecentModel_TypeIsolation(t *testing.T) {
  172. t.Parallel()
  173. dir := t.TempDir()
  174. cfg := &Config{}
  175. cfg.setDefaults(dir, "")
  176. cfg.dataConfigDir = filepath.Join(dir, "config.json")
  177. // Add models to both large and small types
  178. largeModel := SelectedModel{Provider: "openai", Model: "gpt-4o"}
  179. smallModel := SelectedModel{Provider: "anthropic", Model: "claude"}
  180. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, largeModel))
  181. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeSmall, smallModel))
  182. // in-memory: verify types maintain separate histories
  183. require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 1)
  184. require.Len(t, cfg.RecentModels[SelectedModelTypeSmall], 1)
  185. require.Equal(t, largeModel, cfg.RecentModels[SelectedModelTypeLarge][0])
  186. require.Equal(t, smallModel, cfg.RecentModels[SelectedModelTypeSmall][0])
  187. // Add another to large, verify small unchanged
  188. anotherLarge := SelectedModel{Provider: "google", Model: "gemini"}
  189. require.NoError(t, cfg.recordRecentModel(SelectedModelTypeLarge, anotherLarge))
  190. require.Len(t, cfg.RecentModels[SelectedModelTypeLarge], 2)
  191. require.Len(t, cfg.RecentModels[SelectedModelTypeSmall], 1)
  192. require.Equal(t, smallModel, cfg.RecentModels[SelectedModelTypeSmall][0])
  193. // persisted state: verify both types exist with correct lengths and contents
  194. rm := readRecentModels(t, cfg.dataConfigDir)
  195. large, ok := rm[string(SelectedModelTypeLarge)].([]any)
  196. require.True(t, ok)
  197. require.Len(t, large, 2)
  198. // Verify newest first for large type
  199. require.Equal(t, "google", large[0].(map[string]any)["provider"])
  200. require.Equal(t, "gemini", large[0].(map[string]any)["model"])
  201. require.Equal(t, "openai", large[1].(map[string]any)["provider"])
  202. require.Equal(t, "gpt-4o", large[1].(map[string]any)["model"])
  203. small, ok := rm[string(SelectedModelTypeSmall)].([]any)
  204. require.True(t, ok)
  205. require.Len(t, small, 1)
  206. require.Equal(t, "anthropic", small[0].(map[string]any)["provider"])
  207. require.Equal(t, "claude", small[0].(map[string]any)["model"])
  208. }