Files
CLIProxyAPI/internal/thinking/suffix_test.go
2026-01-15 13:06:39 +08:00

314 lines
12 KiB
Go

// Package thinking provides unified thinking configuration processing.
package thinking
import (
"strings"
"testing"
)
// TestParseSuffix tests the ParseSuffix function.
//
// ParseSuffix extracts thinking suffix from model name.
// Format: model-name(value) where value is the raw suffix content.
// This function only extracts; interpretation is done by other functions.
func TestParseSuffix(t *testing.T) {
tests := []struct {
name string
model string
wantModel string
wantSuffix bool
wantRaw string
}{
{"no suffix", "claude-sonnet-4-5", "claude-sonnet-4-5", false, ""},
{"numeric suffix", "model(1000)", "model", true, "1000"},
{"level suffix", "gpt-5(high)", "gpt-5", true, "high"},
{"auto suffix", "gemini-2.5-pro(auto)", "gemini-2.5-pro", true, "auto"},
{"none suffix", "model(none)", "model", true, "none"},
{"complex model name", "gemini-2.5-flash-lite(8192)", "gemini-2.5-flash-lite", true, "8192"},
{"alias with suffix", "g25p(1000)", "g25p", true, "1000"},
{"empty suffix", "model()", "model", true, ""},
{"nested parens", "model(a(b))", "model(a", true, "b)"},
{"no model name", "(1000)", "", true, "1000"},
{"unmatched open", "model(", "model(", false, ""},
{"unmatched close", "model)", "model)", false, ""},
{"paren not at end", "model(1000)extra", "model(1000)extra", false, ""},
{"empty string", "", "", false, ""},
{"large budget", "claude-opus(128000)", "claude-opus", true, "128000"},
{"xhigh level", "gpt-5.2(xhigh)", "gpt-5.2", true, "xhigh"},
{"minimal level", "model(minimal)", "model", true, "minimal"},
{"medium level", "model(medium)", "model", true, "medium"},
{"low level", "model(low)", "model", true, "low"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ParseSuffix(tt.model)
if got.ModelName != tt.wantModel {
t.Errorf("ModelName = %q, want %q", got.ModelName, tt.wantModel)
}
if got.HasSuffix != tt.wantSuffix {
t.Errorf("HasSuffix = %v, want %v", got.HasSuffix, tt.wantSuffix)
}
if got.RawSuffix != tt.wantRaw {
t.Errorf("RawSuffix = %q, want %q", got.RawSuffix, tt.wantRaw)
}
})
}
}
// TestParseSuffixWithError tests invalid suffix error reporting.
func TestParseSuffixWithError(t *testing.T) {
tests := []struct {
name string
model string
wantHasSuffix bool
}{
{"missing close paren", "model(abc", false},
{"unmatched close paren", "model)", false},
{"paren not at end", "model(1000)extra", false},
{"no suffix", "gpt-5", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseSuffixWithError(tt.model)
if tt.name == "no suffix" {
if err != nil {
t.Fatalf("ParseSuffixWithError(%q) error = %v, want nil", tt.model, err)
}
if got.HasSuffix != tt.wantHasSuffix {
t.Errorf("HasSuffix = %v, want %v", got.HasSuffix, tt.wantHasSuffix)
}
return
}
if err == nil {
t.Fatalf("ParseSuffixWithError(%q) error = nil, want error", tt.model)
}
thinkingErr, ok := err.(*ThinkingError)
if !ok {
t.Fatalf("ParseSuffixWithError(%q) error type = %T, want *ThinkingError", tt.model, err)
}
if thinkingErr.Code != ErrInvalidSuffix {
t.Errorf("error code = %v, want %v", thinkingErr.Code, ErrInvalidSuffix)
}
if !strings.Contains(thinkingErr.Message, tt.model) {
t.Errorf("message %q does not include input %q", thinkingErr.Message, tt.model)
}
if got.HasSuffix != tt.wantHasSuffix {
t.Errorf("HasSuffix = %v, want %v", got.HasSuffix, tt.wantHasSuffix)
}
})
}
}
// TestParseSuffixNumeric tests numeric suffix parsing.
//
// ParseNumericSuffix parses raw suffix content as integer budget.
// Only non-negative integers are valid. Negative numbers return ok=false.
func TestParseSuffixNumeric(t *testing.T) {
tests := []struct {
name string
rawSuffix string
wantBudget int
wantOK bool
}{
{"small budget", "512", 512, true},
{"standard budget", "8192", 8192, true},
{"large budget", "100000", 100000, true},
{"max int32", "2147483647", 2147483647, true},
{"max int64", "9223372036854775807", 9223372036854775807, true},
{"zero", "0", 0, true},
{"negative one", "-1", 0, false},
{"negative", "-100", 0, false},
{"int64 overflow", "9223372036854775808", 0, false},
{"large overflow", "99999999999999999999", 0, false},
{"not a number", "abc", 0, false},
{"level string", "high", 0, false},
{"float", "1.5", 0, false},
{"empty", "", 0, false},
{"leading zero", "08192", 8192, true},
{"whitespace", " 8192 ", 0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
budget, ok := ParseNumericSuffix(tt.rawSuffix)
if budget != tt.wantBudget {
t.Errorf("budget = %d, want %d", budget, tt.wantBudget)
}
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
})
}
}
// TestParseSuffixLevel tests level suffix parsing.
//
// ParseLevelSuffix parses raw suffix content as discrete thinking level.
// Only effort levels (minimal, low, medium, high, xhigh) are valid.
// Special values (none, auto) return ok=false - use ParseSpecialSuffix instead.
func TestParseSuffixLevel(t *testing.T) {
tests := []struct {
name string
rawSuffix string
wantLevel ThinkingLevel
wantOK bool
}{
{"minimal", "minimal", LevelMinimal, true},
{"low", "low", LevelLow, true},
{"medium", "medium", LevelMedium, true},
{"high", "high", LevelHigh, true},
{"xhigh", "xhigh", LevelXHigh, true},
{"case HIGH", "HIGH", LevelHigh, true},
{"case High", "High", LevelHigh, true},
{"case hIgH", "hIgH", LevelHigh, true},
{"case MINIMAL", "MINIMAL", LevelMinimal, true},
{"case XHigh", "XHigh", LevelXHigh, true},
{"none special", "none", "", false},
{"auto special", "auto", "", false},
{"unknown ultra", "ultra", "", false},
{"unknown maximum", "maximum", "", false},
{"unknown invalid", "invalid", "", false},
{"numeric", "8192", "", false},
{"numeric zero", "0", "", false},
{"empty", "", "", false},
{"whitespace", " high ", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
level, ok := ParseLevelSuffix(tt.rawSuffix)
if level != tt.wantLevel {
t.Errorf("level = %q, want %q", level, tt.wantLevel)
}
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
})
}
}
// TestParseSuffixSpecialValues tests special value suffix parsing.
//
// Depends on: Epic 3 Story 3-4 (special value suffix parsing)
func TestParseSuffixSpecialValues(t *testing.T) {
tests := []struct {
name string
rawSuffix string
wantMode ThinkingMode
wantOK bool
}{
{"none", "none", ModeNone, true},
{"auto", "auto", ModeAuto, true},
{"negative one", "-1", ModeAuto, true},
{"case NONE", "NONE", ModeNone, true},
{"case Auto", "Auto", ModeAuto, true},
{"case aUtO", "aUtO", ModeAuto, true},
{"case NoNe", "NoNe", ModeNone, true},
{"empty", "", ModeBudget, false},
{"level high", "high", ModeBudget, false},
{"numeric", "8192", ModeBudget, false},
{"negative other", "-2", ModeBudget, false},
{"whitespace", " none ", ModeBudget, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mode, ok := ParseSpecialSuffix(tt.rawSuffix)
if mode != tt.wantMode {
t.Errorf("mode = %q, want %q", mode, tt.wantMode)
}
if ok != tt.wantOK {
t.Errorf("ok = %v, want %v", ok, tt.wantOK)
}
})
}
}
// TestParseSuffixAliasFormats tests alias model suffix parsing.
//
// This test validates that short model aliases (e.g., g25p, cs45) work correctly
// with all suffix types. Alias-to-canonical-model mapping is caller's responsibility.
func TestParseSuffixAliasFormats(t *testing.T) {
tests := []struct {
name string // test case description
model string // input model string with optional suffix
wantName string // expected ModelName after parsing
wantSuffix bool // expected HasSuffix value
wantRaw string // expected RawSuffix value
checkBudget bool // if true, verify ParseNumericSuffix result
wantBudget int // expected budget (only when checkBudget=true)
checkLevel bool // if true, verify ParseLevelSuffix result
wantLevel ThinkingLevel // expected level (only when checkLevel=true)
checkMode bool // if true, verify ParseSpecialSuffix result
wantMode ThinkingMode // expected mode (only when checkMode=true)
}{
// Alias + numeric suffix
{"alias numeric g25p", "g25p(1000)", "g25p", true, "1000", true, 1000, false, "", false, 0},
{"alias numeric cs45", "cs45(16384)", "cs45", true, "16384", true, 16384, false, "", false, 0},
{"alias numeric g3f", "g3f(8192)", "g3f", true, "8192", true, 8192, false, "", false, 0},
// Alias + level suffix
{"alias level gpt52", "gpt52(high)", "gpt52", true, "high", false, 0, true, LevelHigh, false, 0},
{"alias level g25f", "g25f(medium)", "g25f", true, "medium", false, 0, true, LevelMedium, false, 0},
{"alias level cs4", "cs4(low)", "cs4", true, "low", false, 0, true, LevelLow, false, 0},
// Alias + special suffix
{"alias auto g3f", "g3f(auto)", "g3f", true, "auto", false, 0, false, "", true, ModeAuto},
{"alias none claude", "claude(none)", "claude", true, "none", false, 0, false, "", true, ModeNone},
{"alias -1 g25p", "g25p(-1)", "g25p", true, "-1", false, 0, false, "", true, ModeAuto},
// Single char alias
{"single char c", "c(1024)", "c", true, "1024", true, 1024, false, "", false, 0},
{"single char g", "g(high)", "g", true, "high", false, 0, true, LevelHigh, false, 0},
// Alias containing numbers
{"alias with num gpt5", "gpt5(medium)", "gpt5", true, "medium", false, 0, true, LevelMedium, false, 0},
{"alias with num g25", "g25(1000)", "g25", true, "1000", true, 1000, false, "", false, 0},
// Edge cases
{"no suffix", "g25p", "g25p", false, "", false, 0, false, "", false, 0},
{"empty alias", "(1000)", "", true, "1000", true, 1000, false, "", false, 0},
{"hyphen alias", "g-25-p(1000)", "g-25-p", true, "1000", true, 1000, false, "", false, 0},
{"underscore alias", "g_25_p(high)", "g_25_p", true, "high", false, 0, true, LevelHigh, false, 0},
{"nested parens", "g25p(test)(1000)", "g25p(test)", true, "1000", true, 1000, false, "", false, 0},
}
// ParseSuffix only extracts alias and suffix; mapping to canonical model is caller responsibility.
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseSuffix(tt.model)
if result.ModelName != tt.wantName {
t.Errorf("ParseSuffix(%q).ModelName = %q, want %q", tt.model, result.ModelName, tt.wantName)
}
if result.HasSuffix != tt.wantSuffix {
t.Errorf("ParseSuffix(%q).HasSuffix = %v, want %v", tt.model, result.HasSuffix, tt.wantSuffix)
}
if result.RawSuffix != tt.wantRaw {
t.Errorf("ParseSuffix(%q).RawSuffix = %q, want %q", tt.model, result.RawSuffix, tt.wantRaw)
}
if result.HasSuffix {
if tt.checkBudget {
budget, ok := ParseNumericSuffix(result.RawSuffix)
if !ok || budget != tt.wantBudget {
t.Errorf("ParseNumericSuffix(%q) = (%d, %v), want (%d, true)",
result.RawSuffix, budget, ok, tt.wantBudget)
}
}
if tt.checkLevel {
level, ok := ParseLevelSuffix(result.RawSuffix)
if !ok || level != tt.wantLevel {
t.Errorf("ParseLevelSuffix(%q) = (%q, %v), want (%q, true)",
result.RawSuffix, level, ok, tt.wantLevel)
}
}
if tt.checkMode {
mode, ok := ParseSpecialSuffix(result.RawSuffix)
if !ok || mode != tt.wantMode {
t.Errorf("ParseSpecialSuffix(%q) = (%v, %v), want (%v, true)",
result.RawSuffix, mode, ok, tt.wantMode)
}
}
}
})
}
}