feat(report): improve daily intelligence UX and price tracking
Some checks failed
CI / go-test (push) Has been cancelled
CI / scripts-regression (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / docker-build (push) Has been cancelled

This commit is contained in:
phamnazage-jpg
2026-05-27 17:23:08 +08:00
parent f274621013
commit f5b373caf4
29 changed files with 4257 additions and 801 deletions

View File

@@ -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")
}
}