feat(report): improve daily intelligence UX and price tracking
This commit is contained in:
@@ -9,6 +9,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"llm-intelligence/internal/retry"
|
||||
)
|
||||
|
||||
// Test 1: parseModels 正确解析 name、context_length、capabilities、pricing input/prompt 和 output/completion
|
||||
@@ -48,6 +50,10 @@ func TestParseModels(t *testing.T) {
|
||||
if m.Pricing.Output != 10.0 {
|
||||
t.Errorf("Pricing.Output 错误: %f", m.Pricing.Output)
|
||||
}
|
||||
if modality := deriveModality(m); modality != "multimodal" {
|
||||
t.Errorf("deriveModality = %q, want %q", modality, "multimodal")
|
||||
|
||||
}
|
||||
|
||||
// 第二条:pricing 用 prompt/completion 别名回退
|
||||
m2 := models[1]
|
||||
@@ -65,6 +71,68 @@ func TestParseModels(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveModality(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
capabilities []string
|
||||
want string
|
||||
}{
|
||||
{name: "vision first", capabilities: []string{"vision", "json_mode"}, want: "multimodal"},
|
||||
{name: "audio", capabilities: []string{"audio_generation"}, want: "audio"},
|
||||
{name: "code", capabilities: []string{"code_interpreter"}, want: "code"},
|
||||
{name: "text fallback", capabilities: []string{"function_calling"}, want: "text"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := deriveModality(ModelInfo{Capabilities: tt.capabilities}); got != tt.want {
|
||||
t.Fatalf("deriveModality() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveModalityInfersFromModelIdentityWithoutCapabilities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
model ModelInfo
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "omni id maps to multimodal",
|
||||
model: ModelInfo{
|
||||
ID: "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free",
|
||||
Description: "accepts text, image, video, and audio inputs",
|
||||
},
|
||||
want: "multimodal",
|
||||
},
|
||||
{
|
||||
name: "audio id maps to audio",
|
||||
model: ModelInfo{
|
||||
ID: "openai/gpt-audio",
|
||||
Description: "audio model for natural sounding voices",
|
||||
},
|
||||
want: "audio",
|
||||
},
|
||||
{
|
||||
name: "vl id maps to multimodal",
|
||||
model: ModelInfo{
|
||||
ID: "qwen/qwen3-vl-32b-instruct",
|
||||
Description: "vision-language model for text, images, and video",
|
||||
},
|
||||
want: "multimodal",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := deriveModality(tt.model); got != tt.want {
|
||||
t.Fatalf("deriveModality(%+v) = %q, want %q", tt.model, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: run 无 API Key 时写入临时文件,JSON 含 total 和 models 字段
|
||||
func TestRunNoAPIKey(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
@@ -108,6 +176,60 @@ func TestFetchModelsFailsInStrictRealModeWithoutAPIKey(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchModelsDoesNotRetryPermanentHTTPErrors(t *testing.T) {
|
||||
attempts := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempts++
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, err := fetchModels(Config{
|
||||
APIKey: "test-key",
|
||||
APIURL: server.URL,
|
||||
MaxRetries: 3,
|
||||
TimeoutSec: 1,
|
||||
StrictReal: true,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected fetchModels to fail on 403")
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Fatalf("expected 1 attempt for permanent HTTP error, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchModelsRetriesServerErrors(t *testing.T) {
|
||||
attempts := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
http.Error(w, "temporary", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":"openai/gpt-4o","name":"GPT-4o","context_length":128000,"pricing":{"input":2.5,"output":10.0}}]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
models, err := fetchModels(Config{
|
||||
APIKey: "test-key",
|
||||
APIURL: server.URL,
|
||||
MaxRetries: 3,
|
||||
TimeoutSec: 1,
|
||||
StrictReal: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected retry success, got %v", err)
|
||||
}
|
||||
if len(models) != 1 {
|
||||
t.Fatalf("expected 1 model, got %d", len(models))
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Fatalf("expected 3 attempts for temporary server error, got %d", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFailsInStrictRealModeWhenDBWriteFails(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
outPath := filepath.Join(tmpDir, "models.json")
|
||||
@@ -130,3 +252,12 @@ func TestRunFailsInStrictRealModeWhenDBWriteFails(t *testing.T) {
|
||||
t.Fatal("strict real mode should fail when database write fails")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryHTTPStatusErrorClassification(t *testing.T) {
|
||||
if retry.IsRetryable(retry.HTTPStatusError{StatusCode: http.StatusForbidden}) {
|
||||
t.Fatal("403 should not be retryable")
|
||||
}
|
||||
if !retry.IsRetryable(retry.HTTPStatusError{StatusCode: http.StatusBadGateway}) {
|
||||
t.Fatal("502 should be retryable")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user