package recognize_test import ( "context" "io" "log/slog" "os" "strconv" "testing" "time" "git.vakhrushev.me/av/jellybit/internal/llm" "git.vakhrushev.me/av/jellybit/internal/recognize" ) func derefInt(p *int) string { if p == nil { return "nil" } return strconv.Itoa(*p) } // TestIntegration_RecognizeSeries гоняет полный конвейер против реального // LLM на настоящих (русских) именах файлов раздачи. По умолчанию // пропускается; включается так же, как llm-интеграция: // // JELLYBIT_LLM_BASE_URL=https://bothub.chat/api/v2/openai/v1 \ // JELLYBIT_LLM_API_KEY=... JELLYBIT_LLM_MODEL=deepseek-v4-flash \ // go test ./internal/recognize/ -run Integration -v func TestIntegration_RecognizeSeries(t *testing.T) { base := os.Getenv("JELLYBIT_LLM_BASE_URL") key := os.Getenv("JELLYBIT_LLM_API_KEY") model := os.Getenv("JELLYBIT_LLM_MODEL") if base == "" || model == "" { t.Skip("set JELLYBIT_LLM_BASE_URL and JELLYBIT_LLM_MODEL to run") } provider, err := llm.New(llm.Config{ Type: "openai-compat", BaseURL: base, APIKey: key, Model: model, Timeout: 90 * time.Second, }) if err != nil { t.Fatalf("llm.New: %v", err) } log := slog.New(slog.NewTextHandler(io.Discard, nil)) r := recognize.New(provider, recognize.Config{MaxRetries: 2}, log) const dir = "Аватар Легенда об Аанге.Книга 2.Земля(Avatar The Last Airbender The book 2.Earth)/" in := recognize.Input{ Name: "Аватар Легенда об Аанге.Книга 2.Земля", Context: "Аватар: Легенда об Аанге / Книга 2: Земля [2006, США, DVDRip-AVC]", Files: []recognize.File{ {Path: dir + "1.Состояние Аватара (The Avatar State).mkv", Size: 215_000_000}, {Path: dir + "6.Слепой бандит (Blind bandit).mkv", Size: 215_910_977}, {Path: dir + "8.Погоня (The Chase).mkv", Size: 216_587_695}, {Path: dir + "12.Змеиный перевал (The Serpent's Pass).mkv", Size: 216_330_940}, {Path: dir + "20.Перекрестки судьбы (The Crossroads of Destiny).mkv", Size: 215_934_285}, }, } ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) defer cancel() res, err := r.Recognize(ctx, in) if err != nil { t.Fatalf("Recognize: %v", err) } t.Logf("type=%s title=%q year=%d files=%d attempts=%d\nreasons=%v\nnotes=%s", res.Plan.Type, res.Plan.Title, res.Plan.Year, len(res.Plan.Files), res.Attempts, res.Decision.Reasons, res.Plan.Notes) for _, f := range res.Plan.Files { t.Logf(" %s -> role=%s season=%s episode=%s", f.Src, f.Role, derefInt(f.Season), derefInt(f.Episode)) } if res.Plan.Type != recognize.MediaSeries { t.Errorf("type = %q, want series", res.Plan.Type) } if res.Decision.Auto { t.Error("Ф2 must not auto-resolve") } episodes := 0 for _, f := range res.Plan.Files { if f.Role == recognize.RoleEpisode { episodes++ } } if episodes != len(in.Files) { t.Errorf("recognized %d episodes, want %d", episodes, len(in.Files)) } }