Files
ds2api/internal/util/util_edge_test.go
2026-04-27 02:09:11 +08:00

388 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package util
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"
"ds2api/internal/config"
)
// ─── EstimateTokens edge cases ───────────────────────────────────────
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)
}
}
// ─── ToBool edge cases ───────────────────────────────────────────────
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")
}
}
// ─── IntFrom edge cases ─────────────────────────────────────────────
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)
}
}
// ─── WriteJSON ───────────────────────────────────────────────────────
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)
}
}
}
// ─── MessagesPrepare edge cases ──────────────────────────────────────
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)
}
// Should be merged into a single user turn with one marker at the start.
count := strings.Count(got, "<User>")
if count != 1 {
t.Fatalf("expected one User marker for the merged pair, got %d occurrences", count)
}
// User messages no longer have end_of_sentence markers in the official format.
// The merged pair should have zero end_of_sentence markers (user turn only).
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: ![alt](https://example.com/img.png)"},
}
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)
}
}
// ─── normalizeContent edge cases ─────────────────────────────────────
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)
}
}
// ─── ConvertClaudeToDeepSeek edge cases ──────────────────────────────
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"])
}
}