| package util |
|
|
| import ( |
| "encoding/json" |
| "net/http/httptest" |
| "strings" |
| "testing" |
|
|
| "ds2api/internal/config" |
| ) |
|
|
| |
|
|
| func TestEstimateTokensEmpty(t *testing.T) { |
| if got := EstimateTokens(""); got != 0 { |
| t.Fatalf("expected 0 for empty string, got %d", got) |
| } |
| } |
|
|
| func TestEstimateTokensShortASCII(t *testing.T) { |
| got := EstimateTokens("ab") |
| if got != 1 { |
| t.Fatalf("expected 1 for 2 ascii chars, got %d", got) |
| } |
| } |
|
|
| func TestEstimateTokensLongASCII(t *testing.T) { |
| got := EstimateTokens(strings.Repeat("x", 100)) |
| if got != 25 { |
| t.Fatalf("expected 25 for 100 ascii chars, got %d", got) |
| } |
| } |
|
|
| func TestEstimateTokensChinese(t *testing.T) { |
| got := EstimateTokens("δ½ ε₯½δΈη") |
| if got < 1 { |
| t.Fatalf("expected at least 1 token for Chinese text, got %d", got) |
| } |
| } |
|
|
| func TestEstimateTokensMixed(t *testing.T) { |
| got := EstimateTokens("Hello δ½ ε₯½δΈη") |
| if got < 2 { |
| t.Fatalf("expected at least 2 tokens for mixed text, got %d", got) |
| } |
| } |
|
|
| func TestEstimateTokensSingleByte(t *testing.T) { |
| got := EstimateTokens("x") |
| if got != 1 { |
| t.Fatalf("expected 1 for single char (minimum), got %d", got) |
| } |
| } |
|
|
| func TestEstimateTokensSingleChinese(t *testing.T) { |
| got := EstimateTokens("δ½ ") |
| if got != 1 { |
| t.Fatalf("expected 1 for single Chinese char, got %d", got) |
| } |
| } |
|
|
| |
|
|
| func TestToBoolTrue(t *testing.T) { |
| if !ToBool(true) { |
| t.Fatal("expected true") |
| } |
| } |
|
|
| func TestToBoolFalse(t *testing.T) { |
| if ToBool(false) { |
| t.Fatal("expected false") |
| } |
| } |
|
|
| func TestToBoolNonBool(t *testing.T) { |
| if ToBool("true") { |
| t.Fatal("expected false for string 'true'") |
| } |
| if ToBool(1) { |
| t.Fatal("expected false for int 1") |
| } |
| if ToBool(nil) { |
| t.Fatal("expected false for nil") |
| } |
| } |
|
|
| |
|
|
| func TestIntFromFloat64(t *testing.T) { |
| if got := IntFrom(float64(42.5)); got != 42 { |
| t.Fatalf("expected 42 for float64(42.5), got %d", got) |
| } |
| } |
|
|
| func TestIntFromInt(t *testing.T) { |
| if got := IntFrom(int(42)); got != 42 { |
| t.Fatalf("expected 42, got %d", got) |
| } |
| } |
|
|
| func TestIntFromInt64(t *testing.T) { |
| if got := IntFrom(int64(42)); got != 42 { |
| t.Fatalf("expected 42, got %d", got) |
| } |
| } |
|
|
| func TestIntFromString(t *testing.T) { |
| if got := IntFrom("42"); got != 0 { |
| t.Fatalf("expected 0 for string, got %d", got) |
| } |
| } |
|
|
| func TestIntFromNil(t *testing.T) { |
| if got := IntFrom(nil); got != 0 { |
| t.Fatalf("expected 0 for nil, got %d", got) |
| } |
| } |
|
|
| |
|
|
| func TestWriteJSON(t *testing.T) { |
| rec := httptest.NewRecorder() |
| WriteJSON(rec, 200, map[string]any{"key": "value"}) |
| if rec.Code != 200 { |
| t.Fatalf("expected 200, got %d", rec.Code) |
| } |
| if ct := rec.Header().Get("Content-Type"); ct != "application/json" { |
| t.Fatalf("expected application/json content type, got %q", ct) |
| } |
| var body map[string]any |
| if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil { |
| t.Fatalf("decode error: %v", err) |
| } |
| if body["key"] != "value" { |
| t.Fatalf("unexpected body: %#v", body) |
| } |
| } |
|
|
| func TestWriteJSONStatusCodes(t *testing.T) { |
| for _, code := range []int{200, 201, 400, 404, 500} { |
| rec := httptest.NewRecorder() |
| WriteJSON(rec, code, map[string]any{"status": code}) |
| if rec.Code != code { |
| t.Fatalf("expected %d, got %d", code, rec.Code) |
| } |
| } |
| } |
|
|
| |
|
|
| func TestMessagesPrepareEmpty(t *testing.T) { |
| got := MessagesPrepare(nil) |
| if got != "" { |
| t.Fatalf("expected empty for nil messages, got %q", got) |
| } |
| } |
|
|
| func TestMessagesPrepareMergesConsecutiveSameRole(t *testing.T) { |
| messages := []map[string]any{ |
| {"role": "user", "content": "Hello"}, |
| {"role": "user", "content": "World"}, |
| } |
| got := MessagesPrepare(messages) |
| if !strings.HasPrefix(got, "<ο½beginβofβsentenceο½>") { |
| t.Fatalf("expected user marker at the start, got %q", got) |
| } |
| if !strings.Contains(got, "Hello") || !strings.Contains(got, "World") { |
| t.Fatalf("expected both messages, got %q", got) |
| } |
| |
| count := strings.Count(got, "<ο½Userο½>") |
| if count != 1 { |
| t.Fatalf("expected one User marker for the merged pair, got %d occurrences", count) |
| } |
| |
| |
| if count := strings.Count(got, "<ο½endβofβsentenceο½>"); count != 0 { |
| t.Fatalf("expected zero sentence terminators for user-only merge, got %d occurrences", count) |
| } |
| } |
|
|
| func TestMessagesPrepareAssistantMarkers(t *testing.T) { |
| messages := []map[string]any{ |
| {"role": "user", "content": "Hi"}, |
| {"role": "assistant", "content": "Hello!"}, |
| } |
| got := MessagesPrepare(messages) |
| if !strings.Contains(got, "<ο½Assistantο½>") { |
| t.Fatalf("expected assistant marker, got %q", got) |
| } |
| if !strings.Contains(got, "<ο½endβofβsentenceο½>") { |
| t.Fatalf("expected end of sentence marker, got %q", got) |
| } |
| if strings.Count(got, "<ο½endβofβsentenceο½>") != 1 { |
| t.Fatalf("expected one end_of_sentence (assistant only), got %q", got) |
| } |
| if !strings.Contains(got, "<ο½Assistantο½>Hello!<ο½endβofβsentenceο½>") { |
| t.Fatalf("expected assistant EOS suffix, got %q", got) |
| } |
| if strings.Contains(got, "<think>") || strings.Contains(got, "</think>") { |
| t.Fatalf("did not expect think tags in prompt, got %q", got) |
| } |
| if strings.Contains(got, "<system_instructions>") { |
| t.Fatalf("did not expect legacy system marker, got %q", got) |
| } |
| } |
|
|
| func TestMessagesPrepareUnknownRole(t *testing.T) { |
| messages := []map[string]any{ |
| {"role": "user", "content": "Hello"}, |
| {"role": "unknown_role", "content": "Unknown"}, |
| } |
| got := MessagesPrepare(messages) |
| if !strings.Contains(got, "Unknown") { |
| t.Fatalf("expected unknown role content, got %q", got) |
| } |
| } |
|
|
| func TestMessagesPrepareMarkdownImageReplaced(t *testing.T) { |
| messages := []map[string]any{ |
| {"role": "user", "content": "Look at this: "}, |
| } |
| got := MessagesPrepare(messages) |
| if strings.Contains(got, "![alt]") { |
| t.Fatalf("expected markdown image to be replaced, got %q", got) |
| } |
| } |
|
|
| func TestMessagesPrepareNilContent(t *testing.T) { |
| messages := []map[string]any{ |
| {"role": "user", "content": nil}, |
| } |
| got := MessagesPrepare(messages) |
| if got != "null" { |
| t.Logf("nil content handled as: %q", got) |
| } |
| } |
|
|
| |
|
|
| func TestNormalizeContentString(t *testing.T) { |
| got := normalizeContent("hello") |
| if got != "hello" { |
| t.Fatalf("expected 'hello', got %q", got) |
| } |
| } |
|
|
| func TestNormalizeContentArray(t *testing.T) { |
| got := normalizeContent([]any{ |
| map[string]any{"type": "text", "text": "line1"}, |
| map[string]any{"type": "text", "text": "line2"}, |
| }) |
| if got != "line1\nline2" { |
| t.Fatalf("expected 'line1\\nline2', got %q", got) |
| } |
| } |
|
|
| func TestNormalizeContentArrayWithContentField(t *testing.T) { |
| got := normalizeContent([]any{ |
| map[string]any{"type": "text", "content": "from-content"}, |
| }) |
| if got != "from-content" { |
| t.Fatalf("expected 'from-content', got %q", got) |
| } |
| } |
|
|
| func TestNormalizeContentArraySkipsImage(t *testing.T) { |
| got := normalizeContent([]any{ |
| map[string]any{"type": "image_url", "image_url": "https://example.com/img.png"}, |
| map[string]any{"type": "text", "text": "caption"}, |
| }) |
| if strings.Contains(got, "image") { |
| t.Fatalf("expected image skipped, got %q", got) |
| } |
| if got != "caption" { |
| t.Fatalf("expected 'caption', got %q", got) |
| } |
| } |
|
|
| func TestNormalizeContentArrayNonMapItems(t *testing.T) { |
| got := normalizeContent([]any{"string item", 42}) |
| if got != "" { |
| t.Fatalf("expected empty for non-map items, got %q", got) |
| } |
| } |
|
|
| func TestNormalizeContentJSON(t *testing.T) { |
| got := normalizeContent(map[string]any{"key": "value"}) |
| if !strings.Contains(got, `"key":"value"`) { |
| t.Fatalf("expected JSON serialized, got %q", got) |
| } |
| } |
|
|
| |
|
|
| func TestConvertClaudeToDeepSeekDefaultModel(t *testing.T) { |
| store := config.LoadStore() |
| req := map[string]any{ |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| if out["model"] == "" { |
| t.Fatal("expected default model") |
| } |
| } |
|
|
| func TestConvertClaudeToDeepSeekWithStopSequences(t *testing.T) { |
| store := config.LoadStore() |
| req := map[string]any{ |
| "model": "claude-sonnet-4-5", |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| "stop_sequences": []any{"\n\n"}, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| if out["stop"] == nil { |
| t.Fatal("expected stop field from stop_sequences") |
| } |
| } |
|
|
| func TestConvertClaudeToDeepSeekWithTemperature(t *testing.T) { |
| store := config.LoadStore() |
| req := map[string]any{ |
| "model": "claude-sonnet-4-5", |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| "temperature": 0.7, |
| "top_p": 0.9, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| if out["temperature"] != 0.7 { |
| t.Fatalf("expected temperature 0.7, got %v", out["temperature"]) |
| } |
| if out["top_p"] != 0.9 { |
| t.Fatalf("expected top_p 0.9, got %v", out["top_p"]) |
| } |
| } |
|
|
| func TestConvertClaudeToDeepSeekNoSystem(t *testing.T) { |
| store := config.LoadStore() |
| req := map[string]any{ |
| "model": "claude-sonnet-4-5", |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| msgs, _ := out["messages"].([]any) |
| if len(msgs) != 1 { |
| t.Fatalf("expected 1 message without system, got %d", len(msgs)) |
| } |
| } |
|
|
| func TestConvertClaudeToDeepSeekOpusUsesGlobalAlias(t *testing.T) { |
| store := config.LoadStore() |
| req := map[string]any{ |
| "model": "claude-opus-4-6", |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| if out["model"] != "deepseek-v4-pro" { |
| t.Fatalf("expected opus to use global alias, got %q", out["model"]) |
| } |
| } |
|
|
| func TestConvertClaudeToDeepSeekUsesExplicitModelAlias(t *testing.T) { |
| t.Setenv("DS2API_CONFIG_JSON", `{"keys":[],"accounts":[],"model_aliases":{"claude-sonnet-4-6":"deepseek-v4-pro-search"}}`) |
| store := config.LoadStore() |
| req := map[string]any{ |
| "model": "claude-sonnet-4-6", |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| if out["model"] != "deepseek-v4-pro-search" { |
| t.Fatalf("expected explicit alias override, got %q", out["model"]) |
| } |
| } |
|
|
| func TestConvertClaudeToDeepSeekUsesExplicitNoThinkingModelAlias(t *testing.T) { |
| t.Setenv("DS2API_CONFIG_JSON", `{"keys":[],"accounts":[],"model_aliases":{"claude-sonnet-4-6":"deepseek-v4-pro-search"}}`) |
| store := config.LoadStore() |
| req := map[string]any{ |
| "model": "claude-sonnet-4-6-nothinking", |
| "messages": []any{map[string]any{"role": "user", "content": "Hi"}}, |
| } |
| out := ConvertClaudeToDeepSeek(req, store) |
| if out["model"] != "deepseek-v4-pro-search-nothinking" { |
| t.Fatalf("expected explicit alias override with nothinking suffix, got %q", out["model"]) |
| } |
| } |
|
|