| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/config" |
| 9 | ) |
| 10 | |
| 11 | func TestModelSettingsEveryOperationWithoutSessionRejectsForeignFields(t *testing.T) { |
| 12 | kinds := []string{"default", "planner", "vision", "search", "subagent", "subagent_effort", "profile_model", "profile_effort", "depth", "concurrency", "writers", "provider_save", "credential", "web_search_capability", "connection_add", "official_add", "preset_add", "preset_reset", "protocol_upgrade", "catalogs", "provider_remove", "access_remove", "rename"} |
| 13 | for _, kind := range kinds { |
| 14 | t.Run(kind, func(t *testing.T) { |
| 15 | isolateDesktopUserDirs(t) |
| 16 | _, ref := configureSwitchableDefaultModels(t) |
| 17 | app := NewApp() |
| 18 | app.ctx = context.Background() |
| 19 | change := sessionlessModelSettingsOperation(t, app, kind, ref) |
| 20 | beforeConfig, _ := os.ReadFile(config.UserConfigPath()) |
| 21 | beforeKeys, _ := os.ReadFile(config.UserCredentialsPath()) |
| 22 | invalid := change |
| 23 | invalid.RequestID = "foreign-fields" |
| 24 | invalid.ExpectedFingerprint = app.Settings().ModelSettingsFingerprint |
| 25 | if change.Kind == "preference" { |
| 26 | invalid.BaseURL = "https://example.invalid/foreign" |
| 27 | } else { |
| 28 | invalid.Field = "foreign" |
| 29 | } |
| 30 | if result := app.ApplyModelSettings(invalid); result.Persisted || len(result.Issues) == 0 { |
| 31 | t.Fatalf("foreign fields accepted: %+v", result) |
| 32 | } |
| 33 | afterConfig, _ := os.ReadFile(config.UserConfigPath()) |
| 34 | afterKeys, _ := os.ReadFile(config.UserCredentialsPath()) |
| 35 | if string(beforeConfig) != string(afterConfig) || string(beforeKeys) != string(afterKeys) { |
| 36 | t.Fatal("validation wrote configuration or credentials") |
| 37 | } |
| 38 | change.RequestID = "valid-edit" |
| 39 | change.ExpectedFingerprint = app.Settings().ModelSettingsFingerprint |
| 40 | result := app.ApplyModelSettings(change) |
| 41 | if !result.Persisted || result.Application != "not_required" || len(result.Targets) != 0 { |
| 42 | t.Fatalf("sessionless save: %+v", result) |
| 43 | } |
| 44 | if len(app.tabs) != 0 || len(app.detachedSessions) != 0 || app.activeTabID != "" { |
| 45 | t.Fatal("model settings created an implicit session") |
| 46 | } |
| 47 | if kind == "catalogs" && len(result.AppliedCatalogs) != 1 { |
| 48 | t.Fatal("catalog update was not committed") |
| 49 | } |
| 50 | }) |
| 51 | } |
| 52 | } |
| 53 | |
| 54 | func sessionlessModelSettingsOperation(t *testing.T, app *App, kind, ref string) ModelSettingsChange { |
| 55 | t.Helper() |
| 56 | key, enabled := "new-operation-key", false |
| 57 | change := ModelSettingsChange{Kind: kind} |
| 58 | switch kind { |
| 59 | case "default", "planner", "subagent", "profile_model": |
| 60 | change.Kind, change.Field, change.Ref = "preference", kind, ref |
| 61 | if kind == "profile_model" { |
| 62 | change.Name = "reviewer" |
| 63 | } |
| 64 | case "vision", "search", "subagent_effort", "profile_effort": |
| 65 | change.Kind, change.Field, change.Ref = "preference", kind, "auto" |
| 66 | if kind == "profile_effort" { |
| 67 | change.Name = "reviewer" |
| 68 | } |
| 69 | case "depth", "concurrency", "writers": |
| 70 | change.Kind, change.Field, change.Number = "preference", kind, 2 |
| 71 | case "provider_save": |
| 72 | change.Provider = &ProviderView{Name: "extra", Kind: "openai", BaseURL: "https://example.invalid/v1", Models: []string{"m"}} |
| 73 | change.Key = &key |
| 74 | case "credential": |
| 75 | change.Name, change.Key = "old", &key |
| 76 | case "web_search_capability": |
| 77 | if _, err := app.AddOfficialProviderAccess("deepseek", key); err != nil { |
| 78 | t.Fatal(err) |
| 79 | } |
| 80 | change.Names, change.Enabled = []string{"deepseek"}, &enabled |
| 81 | case "connection_add": |
| 82 | change.Name, change.Key = "old", &key |
| 83 | case "official_add": |
| 84 | change.Name, change.Key = "deepseek", &key |
| 85 | case "preset_add", "preset_reset": |
| 86 | change.PresetID = config.CuratedProviderPresets()[0].ID |
| 87 | if kind == "preset_add" { |
| 88 | change.Key = &key |
| 89 | } else if _, err := app.AddProviderPresetAccess(change.PresetID, key); err != nil { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | case "protocol_upgrade": |
| 93 | if _, err := app.AddOfficialProviderAccess("deepseek", key); err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | change.Name = "deepseek-flash" |
| 97 | case "catalogs": |
| 98 | cfg := config.LoadForEdit(config.UserConfigPath()) |
| 99 | entry, _ := cfg.Provider("old") |
| 100 | change.Catalogs = []ProviderModelCatalogUpdate{{Name: "old", ExpectedFingerprint: providerModelCatalogFingerprint(*entry), Models: []string{"old-model", "added-model"}}} |
| 101 | case "provider_remove", "access_remove", "rename": |
| 102 | change.Names = []string{"old"} |
| 103 | if kind == "rename" { |
| 104 | change.Ref = "Renamed connection" |
| 105 | } |
| 106 | default: |
| 107 | t.Fatalf("missing operation fixture: %s", kind) |
| 108 | } |
| 109 | return change |
| 110 | } |
| 111 |