Files
ds2api/internal/config/config_edge_test.go

761 lines
24 KiB
Go

package config
import (
"encoding/base64"
"encoding/json"
"strings"
"testing"
)
// ─── GetModelConfig edge cases ───────────────────────────────────────
func TestGetModelConfigDeepSeekChat(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-chat")
if !ok {
t.Fatal("expected ok for deepseek-chat")
}
if thinking || search {
t.Fatalf("expected no thinking/search for deepseek-chat, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelConfigDeepSeekReasoner(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-reasoner")
if !ok {
t.Fatal("expected ok for deepseek-reasoner")
}
if !thinking || search {
t.Fatalf("expected thinking=true search=false, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelConfigDeepSeekChatSearch(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-chat-search")
if !ok {
t.Fatal("expected ok for deepseek-chat-search")
}
if thinking || !search {
t.Fatalf("expected thinking=false search=true, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelConfigDeepSeekReasonerSearch(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-reasoner-search")
if !ok {
t.Fatal("expected ok for deepseek-reasoner-search")
}
if !thinking || !search {
t.Fatalf("expected both true, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelConfigDeepSeekExpertChat(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-expert-chat")
if !ok {
t.Fatal("expected ok for deepseek-expert-chat")
}
if thinking || search {
t.Fatalf("expected no thinking/search for deepseek-expert-chat, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelConfigDeepSeekExpertReasonerSearch(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-expert-reasoner-search")
if !ok {
t.Fatal("expected ok for deepseek-expert-reasoner-search")
}
if !thinking || !search {
t.Fatalf("expected both true, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelConfigDeepSeekVisionReasonerSearch(t *testing.T) {
thinking, search, ok := GetModelConfig("deepseek-vision-reasoner-search")
if !ok {
t.Fatal("expected ok for deepseek-vision-reasoner-search")
}
if !thinking || !search {
t.Fatalf("expected both true, got thinking=%v search=%v", thinking, search)
}
}
func TestGetModelTypeDefaultExpertAndVision(t *testing.T) {
defaultType, ok := GetModelType("deepseek-chat")
if !ok || defaultType != "default" {
t.Fatalf("expected default model_type, got ok=%v model_type=%q", ok, defaultType)
}
expertType, ok := GetModelType("deepseek-expert-chat")
if !ok || expertType != "expert" {
t.Fatalf("expected expert model_type, got ok=%v model_type=%q", ok, expertType)
}
visionType, ok := GetModelType("deepseek-vision-chat")
if !ok || visionType != "vision" {
t.Fatalf("expected vision model_type, got ok=%v model_type=%q", ok, visionType)
}
}
func TestGetModelConfigCaseInsensitive(t *testing.T) {
thinking, search, ok := GetModelConfig("DeepSeek-Chat")
if !ok {
t.Fatal("expected ok for case-insensitive deepseek-chat")
}
if thinking || search {
t.Fatalf("expected no thinking/search for case-insensitive deepseek-chat")
}
}
func TestGetModelConfigUnknownModel(t *testing.T) {
_, _, ok := GetModelConfig("gpt-4")
if ok {
t.Fatal("expected not ok for unknown model")
}
}
func TestGetModelConfigEmpty(t *testing.T) {
_, _, ok := GetModelConfig("")
if ok {
t.Fatal("expected not ok for empty model")
}
}
// ─── lower function ──────────────────────────────────────────────────
func TestLowerFunction(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"Hello", "hello"},
{"ALLCAPS", "allcaps"},
{"already-lower", "already-lower"},
{"Mixed-CASE-123", "mixed-case-123"},
{"", ""},
}
for _, tc := range tests {
got := lower(tc.input)
if got != tc.expected {
t.Errorf("lower(%q) = %q, want %q", tc.input, got, tc.expected)
}
}
}
// ─── Config.MarshalJSON / UnmarshalJSON roundtrip ────────────────────
func TestConfigJSONRoundtrip(t *testing.T) {
trueVal := true
falseVal := false
cfg := Config{
Keys: []string{"key1", "key2"},
Accounts: []Account{{Email: "user@example.com", Password: "pass", Token: "tok"}},
ClaudeMapping: map[string]string{
"fast": "deepseek-chat",
"slow": "deepseek-reasoner",
},
AutoDelete: AutoDeleteConfig{
Mode: "single",
},
HistorySplit: HistorySplitConfig{
Enabled: &trueVal,
TriggerAfterTurns: func() *int { v := 2; return &v }(),
},
Runtime: RuntimeConfig{
TokenRefreshIntervalHours: 12,
},
Compat: CompatConfig{
WideInputStrictOutput: &trueVal,
StripReferenceMarkers: &falseVal,
},
VercelSyncHash: "hash123",
VercelSyncTime: 1234567890,
AdditionalFields: map[string]any{
"custom_field": "custom_value",
},
}
data, err := cfg.MarshalJSON()
if err != nil {
t.Fatalf("marshal error: %v", err)
}
var decoded Config
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if len(decoded.Keys) != 2 || decoded.Keys[0] != "key1" {
t.Fatalf("unexpected keys: %#v", decoded.Keys)
}
if len(decoded.Accounts) != 1 || decoded.Accounts[0].Email != "user@example.com" {
t.Fatalf("unexpected accounts: %#v", decoded.Accounts)
}
if decoded.ClaudeMapping["fast"] != "deepseek-chat" {
t.Fatalf("unexpected claude mapping: %#v", decoded.ClaudeMapping)
}
if decoded.Runtime.TokenRefreshIntervalHours != 12 {
t.Fatalf("unexpected runtime refresh interval: %#v", decoded.Runtime.TokenRefreshIntervalHours)
}
if decoded.AutoDelete.Mode != "single" {
t.Fatalf("unexpected auto delete mode: %#v", decoded.AutoDelete.Mode)
}
if decoded.HistorySplit.Enabled == nil || !*decoded.HistorySplit.Enabled {
t.Fatalf("unexpected history split enabled: %#v", decoded.HistorySplit.Enabled)
}
if decoded.HistorySplit.TriggerAfterTurns == nil || *decoded.HistorySplit.TriggerAfterTurns != 2 {
t.Fatalf("unexpected history split trigger_after_turns: %#v", decoded.HistorySplit.TriggerAfterTurns)
}
if decoded.Compat.WideInputStrictOutput == nil || !*decoded.Compat.WideInputStrictOutput {
t.Fatalf("unexpected compat wide_input_strict_output: %#v", decoded.Compat.WideInputStrictOutput)
}
if decoded.Compat.StripReferenceMarkers == nil || *decoded.Compat.StripReferenceMarkers {
t.Fatalf("unexpected compat strip_reference_markers: %#v", decoded.Compat.StripReferenceMarkers)
}
if decoded.VercelSyncHash != "hash123" {
t.Fatalf("unexpected vercel sync hash: %q", decoded.VercelSyncHash)
}
if decoded.AdditionalFields["custom_field"] != "custom_value" {
t.Fatalf("unexpected additional fields: %#v", decoded.AdditionalFields)
}
}
func TestAutoDeleteModeResolution(t *testing.T) {
tests := []struct {
name string
cfg AutoDeleteConfig
want string
}{
{name: "default", cfg: AutoDeleteConfig{}, want: "none"},
{name: "legacy all", cfg: AutoDeleteConfig{Sessions: true}, want: "all"},
{name: "single", cfg: AutoDeleteConfig{Mode: "single"}, want: "single"},
{name: "all", cfg: AutoDeleteConfig{Mode: "all"}, want: "all"},
{name: "none", cfg: AutoDeleteConfig{Mode: "none"}, want: "none"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
store := &Store{cfg: Config{AutoDelete: tc.cfg}}
if got := store.AutoDeleteMode(); got != tc.want {
t.Fatalf("AutoDeleteMode()=%q want=%q", got, tc.want)
}
})
}
}
func TestConfigUnmarshalJSONPreservesUnknownFields(t *testing.T) {
raw := `{"keys":["k1"],"accounts":[],"my_custom_field":"hello","number_field":42}`
var cfg Config
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
t.Fatalf("unmarshal error: %v", err)
}
if cfg.AdditionalFields["my_custom_field"] != "hello" {
t.Fatalf("expected custom field preserved, got %#v", cfg.AdditionalFields)
}
// number_field should also be preserved
if cfg.AdditionalFields["number_field"] != float64(42) {
t.Fatalf("expected number field preserved, got %#v", cfg.AdditionalFields["number_field"])
}
}
// ─── Config.Clone ────────────────────────────────────────────────────
func TestConfigCloneIsDeepCopy(t *testing.T) {
falseVal := false
trueVal := true
turns := 2
cfg := Config{
Keys: []string{"key1"},
Accounts: []Account{{Email: "user@test.com", Token: "token"}},
ClaudeMapping: map[string]string{
"fast": "deepseek-chat",
},
Compat: CompatConfig{
StripReferenceMarkers: &falseVal,
},
HistorySplit: HistorySplitConfig{
Enabled: &trueVal,
TriggerAfterTurns: &turns,
},
AdditionalFields: map[string]any{"custom": "value"},
}
cloned := cfg.Clone()
// Modify original
cfg.Keys[0] = "modified"
cfg.Accounts[0].Email = "modified@test.com"
cfg.ClaudeMapping["fast"] = "modified-model"
if cfg.Compat.StripReferenceMarkers != nil {
*cfg.Compat.StripReferenceMarkers = true
}
if cfg.HistorySplit.Enabled != nil {
*cfg.HistorySplit.Enabled = false
}
if cfg.HistorySplit.TriggerAfterTurns != nil {
*cfg.HistorySplit.TriggerAfterTurns = 5
}
// Cloned should not be affected
if cloned.Keys[0] != "key1" {
t.Fatalf("clone keys was affected by original change: %#v", cloned.Keys)
}
if cloned.Accounts[0].Email != "user@test.com" {
t.Fatalf("clone accounts was affected: %#v", cloned.Accounts)
}
if cloned.ClaudeMapping["fast"] != "deepseek-chat" {
t.Fatalf("clone claude mapping was affected: %#v", cloned.ClaudeMapping)
}
if cloned.Compat.StripReferenceMarkers == nil || *cloned.Compat.StripReferenceMarkers {
t.Fatalf("clone compat was affected: %#v", cloned.Compat.StripReferenceMarkers)
}
if cloned.HistorySplit.Enabled == nil || !*cloned.HistorySplit.Enabled {
t.Fatalf("clone history split enabled was affected: %#v", cloned.HistorySplit.Enabled)
}
if cloned.HistorySplit.TriggerAfterTurns == nil || *cloned.HistorySplit.TriggerAfterTurns != 2 {
t.Fatalf("clone history split trigger was affected: %#v", cloned.HistorySplit.TriggerAfterTurns)
}
}
func TestConfigCloneNilMaps(t *testing.T) {
cfg := Config{
Keys: []string{"k"},
Accounts: nil,
}
cloned := cfg.Clone()
if len(cloned.Keys) != 1 {
t.Fatalf("unexpected keys length: %d", len(cloned.Keys))
}
if cloned.Accounts != nil {
t.Fatalf("expected nil accounts in clone, got %#v", cloned.Accounts)
}
}
// ─── Account.Identifier edge cases ───────────────────────────────────
func TestAccountIdentifierPreferenceMobileOverToken(t *testing.T) {
acc := Account{Mobile: "13800138000", Token: "tok"}
if acc.Identifier() != "+8613800138000" {
t.Fatalf("expected mobile identifier, got %q", acc.Identifier())
}
}
func TestAccountIdentifierPreferenceEmailOverMobile(t *testing.T) {
acc := Account{Email: "user@test.com", Mobile: "13800138000"}
if acc.Identifier() != "user@test.com" {
t.Fatalf("expected email identifier, got %q", acc.Identifier())
}
}
func TestAccountIdentifierEmptyAccount(t *testing.T) {
acc := Account{}
if acc.Identifier() != "" {
t.Fatalf("expected empty identifier for empty account, got %q", acc.Identifier())
}
}
// ─── normalizeConfigInput ────────────────────────────────────────────
func TestNormalizeConfigInputStripsQuotes(t *testing.T) {
got := normalizeConfigInput(`"base64:abc"`)
if strings.HasPrefix(got, `"`) || strings.HasSuffix(got, `"`) {
t.Fatalf("expected quotes stripped, got %q", got)
}
}
func TestNormalizeConfigInputStripsSingleQuotes(t *testing.T) {
got := normalizeConfigInput("'some-value'")
if strings.HasPrefix(got, "'") || strings.HasSuffix(got, "'") {
t.Fatalf("expected single quotes stripped, got %q", got)
}
}
func TestNormalizeConfigInputTrimsWhitespace(t *testing.T) {
got := normalizeConfigInput(" hello ")
if got != "hello" {
t.Fatalf("expected trimmed, got %q", got)
}
}
// ─── parseConfigString edge cases ────────────────────────────────────
func TestParseConfigStringPlainJSON(t *testing.T) {
cfg, err := parseConfigString(`{"keys":["k1"],"accounts":[]}`)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(cfg.Keys) != 1 || cfg.Keys[0] != "k1" {
t.Fatalf("unexpected keys: %#v", cfg.Keys)
}
}
func TestParseConfigStringBase64Prefix(t *testing.T) {
rawJSON := `{"keys":["base64-key"],"accounts":[]}`
b64 := base64.StdEncoding.EncodeToString([]byte(rawJSON))
cfg, err := parseConfigString("base64:" + b64)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(cfg.Keys) != 1 || cfg.Keys[0] != "base64-key" {
t.Fatalf("unexpected keys: %#v", cfg.Keys)
}
}
func TestParseConfigStringInvalidBase64(t *testing.T) {
_, err := parseConfigString("base64:!!!invalid!!!")
if err == nil {
t.Fatal("expected error for invalid base64")
}
}
func TestParseConfigStringEmptyString(t *testing.T) {
_, err := parseConfigString("")
if err == nil {
t.Fatal("expected error for empty string")
}
}
// ─── Store methods ───────────────────────────────────────────────────
func TestStoreSnapshotReturnsClone(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[{"email":"u@test.com","token":"t1"}]}`)
store := LoadStore()
snap := store.Snapshot()
snap.Keys[0] = "modified"
if store.Keys()[0] != "k1" {
t.Fatal("snapshot modification should not affect store")
}
}
func TestStoreHasAPIKeyMultipleKeys(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["key1","key2","key3"],"accounts":[]}`)
store := LoadStore()
if !store.HasAPIKey("key1") {
t.Fatal("expected key1 found")
}
if !store.HasAPIKey("key2") {
t.Fatal("expected key2 found")
}
if !store.HasAPIKey("key3") {
t.Fatal("expected key3 found")
}
if store.HasAPIKey("nonexistent") {
t.Fatal("expected nonexistent key not found")
}
}
func TestStoreFindAccountNotFound(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[{"email":"u@test.com"}]}`)
store := LoadStore()
_, ok := store.FindAccount("nonexistent@test.com")
if ok {
t.Fatal("expected account not found")
}
}
func TestStoreCompatWideInputStrictOutputDefaultTrue(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[]}`)
store := LoadStore()
if !store.CompatWideInputStrictOutput() {
t.Fatal("expected default wide_input_strict_output=true when unset")
}
}
func TestStoreCompatWideInputStrictOutputCanDisable(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[],"compat":{"wide_input_strict_output":false}}`)
store := LoadStore()
if store.CompatWideInputStrictOutput() {
t.Fatal("expected wide_input_strict_output=false when explicitly configured")
}
snap := store.Snapshot()
data, err := snap.MarshalJSON()
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
t.Fatalf("decode failed: %v", err)
}
rawCompat, ok := out["compat"].(map[string]any)
if !ok {
t.Fatalf("expected compat in marshaled output, got %#v", out)
}
if rawCompat["wide_input_strict_output"] != false {
t.Fatalf("expected explicit false in compat, got %#v", rawCompat)
}
}
func TestStoreCompatStripReferenceMarkersDefaultTrue(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[]}`)
store := LoadStore()
if !store.CompatStripReferenceMarkers() {
t.Fatal("expected default strip_reference_markers=true when unset")
}
}
func TestStoreCompatStripReferenceMarkersCanDisable(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[],"compat":{"strip_reference_markers":false}}`)
store := LoadStore()
if store.CompatStripReferenceMarkers() {
t.Fatal("expected strip_reference_markers=false when explicitly configured")
}
snap := store.Snapshot()
data, err := snap.MarshalJSON()
if err != nil {
t.Fatalf("marshal failed: %v", err)
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
t.Fatalf("decode failed: %v", err)
}
rawCompat, ok := out["compat"].(map[string]any)
if !ok {
t.Fatalf("expected compat in marshaled output, got %#v", out)
}
if rawCompat["strip_reference_markers"] != false {
t.Fatalf("expected explicit false in compat, got %#v", rawCompat)
}
}
func TestStoreIsEnvBacked(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[]}`)
store := LoadStore()
if !store.IsEnvBacked() {
t.Fatal("expected env-backed store")
}
}
func TestStoreReplace(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[]}`)
store := LoadStore()
newCfg := Config{
Keys: []string{"new-key"},
Accounts: []Account{{Email: "new@test.com"}},
}
if err := store.Replace(newCfg); err != nil {
t.Fatalf("replace error: %v", err)
}
if !store.HasAPIKey("new-key") {
t.Fatal("expected new key after replace")
}
if store.HasAPIKey("k1") {
t.Fatal("expected old key removed after replace")
}
}
func TestStoreUpdate(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["k1"],"accounts":[]}`)
store := LoadStore()
err := store.Update(func(cfg *Config) error {
cfg.Keys = append(cfg.Keys, "k2")
return nil
})
if err != nil {
t.Fatalf("update error: %v", err)
}
if !store.HasAPIKey("k2") {
t.Fatal("expected k2 after update")
}
}
func TestStoreUpdateReconcilesAPIKeyMutations(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{
"keys":["k1"],
"api_keys":[{"key":"k1","name":"primary","remark":"prod"}],
"accounts":[]
}`)
store := LoadStore()
if err := store.Update(func(cfg *Config) error {
cfg.APIKeys = append(cfg.APIKeys, APIKey{Key: "k2", Name: "secondary", Remark: "staging"})
return nil
}); err != nil {
t.Fatalf("add api key failed: %v", err)
}
snap := store.Snapshot()
if len(snap.Keys) != 2 || snap.Keys[0] != "k1" || snap.Keys[1] != "k2" {
t.Fatalf("unexpected keys after api key add: %#v", snap.Keys)
}
if len(snap.APIKeys) != 2 {
t.Fatalf("unexpected api keys length after add: %#v", snap.APIKeys)
}
if snap.APIKeys[0].Name != "primary" || snap.APIKeys[0].Remark != "prod" {
t.Fatalf("metadata for existing key was lost: %#v", snap.APIKeys[0])
}
if snap.APIKeys[1].Name != "secondary" || snap.APIKeys[1].Remark != "staging" {
t.Fatalf("metadata for new key was lost: %#v", snap.APIKeys[1])
}
if err := store.Update(func(cfg *Config) error {
cfg.APIKeys = append([]APIKey(nil), cfg.APIKeys[1:]...)
return nil
}); err != nil {
t.Fatalf("delete api key failed: %v", err)
}
snap = store.Snapshot()
if len(snap.Keys) != 1 || snap.Keys[0] != "k2" {
t.Fatalf("unexpected keys after api key delete: %#v", snap.Keys)
}
if len(snap.APIKeys) != 1 || snap.APIKeys[0].Key != "k2" {
t.Fatalf("unexpected api keys after delete: %#v", snap.APIKeys)
}
}
func TestStoreUpdateReconcilesLegacyKeyMutations(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{
"keys":["k1"],
"api_keys":[{"key":"k1","name":"primary","remark":"prod"}],
"accounts":[]
}`)
store := LoadStore()
if err := store.Update(func(cfg *Config) error {
cfg.Keys = append(cfg.Keys, "k2")
return nil
}); err != nil {
t.Fatalf("legacy key update failed: %v", err)
}
snap := store.Snapshot()
if len(snap.Keys) != 2 || snap.Keys[0] != "k1" || snap.Keys[1] != "k2" {
t.Fatalf("unexpected keys after legacy update: %#v", snap.Keys)
}
if len(snap.APIKeys) != 2 {
t.Fatalf("unexpected api keys after legacy update: %#v", snap.APIKeys)
}
if snap.APIKeys[0].Name != "primary" || snap.APIKeys[0].Remark != "prod" {
t.Fatalf("metadata for preserved key was lost: %#v", snap.APIKeys[0])
}
if snap.APIKeys[1].Key != "k2" || snap.APIKeys[1].Name != "" || snap.APIKeys[1].Remark != "" {
t.Fatalf("new legacy key should stay metadata-free: %#v", snap.APIKeys[1])
}
}
func TestNormalizeCredentialsPrefersStructuredAPIKeys(t *testing.T) {
cfg := Config{
Keys: []string{"legacy-key"},
APIKeys: []APIKey{
{Key: "structured-key", Name: "primary", Remark: "prod"},
},
}
cfg.NormalizeCredentials()
if len(cfg.Keys) != 1 || cfg.Keys[0] != "structured-key" {
t.Fatalf("unexpected normalized keys: %#v", cfg.Keys)
}
if len(cfg.APIKeys) != 1 {
t.Fatalf("unexpected normalized api keys: %#v", cfg.APIKeys)
}
if cfg.APIKeys[0].Key != "structured-key" || cfg.APIKeys[0].Name != "primary" || cfg.APIKeys[0].Remark != "prod" {
t.Fatalf("unexpected structured api key metadata: %#v", cfg.APIKeys[0])
}
}
func TestStoreClaudeMapping(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":[],"accounts":[],"claude_mapping":{"fast":"deepseek-chat","slow":"deepseek-reasoner"}}`)
store := LoadStore()
mapping := store.ClaudeMapping()
if mapping["fast"] != "deepseek-chat" {
t.Fatalf("unexpected fast mapping: %q", mapping["fast"])
}
if mapping["slow"] != "deepseek-reasoner" {
t.Fatalf("unexpected slow mapping: %q", mapping["slow"])
}
}
func TestStoreClaudeMappingEmpty(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":[],"accounts":[]}`)
store := LoadStore()
mapping := store.ClaudeMapping()
// Even without config mapping, there are defaults
if mapping == nil {
t.Fatal("expected non-nil mapping (may contain defaults)")
}
}
func TestStoreSetVercelSync(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":[],"accounts":[]}`)
store := LoadStore()
if err := store.SetVercelSync("hash123", 1234567890); err != nil {
t.Fatalf("setVercelSync error: %v", err)
}
snap := store.Snapshot()
if snap.VercelSyncHash != "hash123" || snap.VercelSyncTime != 1234567890 {
t.Fatalf("unexpected vercel sync: hash=%q time=%d", snap.VercelSyncHash, snap.VercelSyncTime)
}
}
func TestStoreExportJSONAndBase64(t *testing.T) {
t.Setenv("DS2API_CONFIG_JSON", `{"keys":["export-key"],"accounts":[]}`)
store := LoadStore()
jsonStr, b64Str, err := store.ExportJSONAndBase64()
if err != nil {
t.Fatalf("export error: %v", err)
}
if !strings.Contains(jsonStr, "export-key") {
t.Fatalf("expected JSON to contain key: %q", jsonStr)
}
decoded, err := base64.StdEncoding.DecodeString(b64Str)
if err != nil {
t.Fatalf("base64 decode error: %v", err)
}
if !strings.Contains(string(decoded), "export-key") {
t.Fatalf("expected base64-decoded to contain key: %q", string(decoded))
}
}
// ─── OpenAIModelsResponse / ClaudeModelsResponse ─────────────────────
func TestOpenAIModelsResponse(t *testing.T) {
resp := OpenAIModelsResponse()
if resp["object"] != "list" {
t.Fatalf("unexpected object: %v", resp["object"])
}
data, ok := resp["data"].([]ModelInfo)
if !ok {
t.Fatalf("unexpected data type: %T", resp["data"])
}
if len(data) == 0 {
t.Fatal("expected non-empty models list")
}
expected := map[string]bool{
"deepseek-chat": false,
"deepseek-reasoner": false,
"deepseek-chat-search": false,
"deepseek-reasoner-search": false,
"deepseek-expert-chat": false,
"deepseek-expert-reasoner": false,
"deepseek-expert-chat-search": false,
"deepseek-expert-reasoner-search": false,
"deepseek-vision-chat": false,
"deepseek-vision-reasoner": false,
"deepseek-vision-chat-search": false,
"deepseek-vision-reasoner-search": false,
}
for _, model := range data {
if _, ok := expected[model.ID]; ok {
expected[model.ID] = true
}
}
for id, seen := range expected {
if !seen {
t.Fatalf("expected OpenAI model list to include %s", id)
}
}
}
func TestClaudeModelsResponse(t *testing.T) {
resp := ClaudeModelsResponse()
if resp["object"] != "list" {
t.Fatalf("unexpected object: %v", resp["object"])
}
data, ok := resp["data"].([]ModelInfo)
if !ok {
t.Fatalf("unexpected data type: %T", resp["data"])
}
if len(data) == 0 {
t.Fatal("expected non-empty models list")
}
}