package recognize import ( "bytes" "log/slog" "strings" "testing" ) func TestFoldHomoglyphs(t *testing.T) { cases := map[string]string{ // Latin-доминантный токен: кириллический двойник 'а' (U+0430) → 'a'. "Hаrold and the Purple Crayon": "Harold and the Purple Crayon", // Честная кириллица — не трогаем. "Тёмный рыцарь": "Тёмный рыцарь", // Честная латиница — не трогаем. "The Dark Knight": "The Dark Knight", // Cyrillic-доминантный токен (М р з) с латинскими 'o' (U+006F) → 'о'. "Мoрoз": "Мороз", // Равенство скриптов в токене (кир. О U+041E + лат. k) — не трогаем. "Оk": "Оk", // Двуязычные, но одно-скриптовые токены — каждый нетронут. "Fargo Фарго": "Fargo Фарго", // Буква-меньшинство без двойника в таблице (кир. 'б' U+0431) остаётся. "Harбld": "Harбld", // Пустая строка. "": "", } for in, want := range cases { if got := foldHomoglyphs(in); got != want { t.Errorf("foldHomoglyphs(%q) = %q, want %q", in, got, want) } } } func TestSanitizeTitle(t *testing.T) { cases := map[string]string{ // Zero-width (U+200B) внутри слова удаляется, слово склеивается. "a\u200bb": "ab", // Zero-width + двойник + лишние пробелы → strip + fold + collapse + trim. " Hаrold\u200b Crayon ": "Harold Crayon", // Табы/переводы строк как пробелы, сведены к одиночным. "Harold\tand the": "Harold and the", // Управляющий символ (BEL U+0007) удаляется. "Harold\u0007": "Harold", // BOM (U+FEFF) удаляется. "\ufeffFargo": "Fargo", // Уже чистое — без изменений. "The Matrix": "The Matrix", } for in, want := range cases { if got := sanitizeTitle(in); got != want { t.Errorf("sanitizeTitle(%q) = %q, want %q", in, got, want) } } } func TestSanitizePlan_FieldsCleanedSrcUntouched(t *testing.T) { // src намеренно содержит кириллический двойник — sanitizePlan его не трогает. dirtySrc := "Hаrold.mkv" p := Plan{ Title: "Hаrold", OriginalTitle: " spaced ", ProviderHint: "clean\u200b", Files: []PlanFile{{Src: dirtySrc, Role: RoleMain}}, } sanitizePlan(&p, nil) if p.Title != "Harold" { t.Errorf("Title = %q, want %q", p.Title, "Harold") } if p.OriginalTitle != "spaced" { t.Errorf("OriginalTitle = %q, want %q", p.OriginalTitle, "spaced") } if p.ProviderHint != "clean" { t.Errorf("ProviderHint = %q, want %q", p.ProviderHint, "clean") } if p.Files[0].Src != dirtySrc { t.Errorf("files[].src изменён санитайзингом: %q != %q", p.Files[0].Src, dirtySrc) } } func TestSanitizePlan_LogsOnChange(t *testing.T) { var buf bytes.Buffer log := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) p := Plan{Title: "Hаrold"} // 'а' — кириллица, санитайзинг перепишет sanitizePlan(&p, log) out := buf.String() if !strings.Contains(out, "recognition plan field sanitized") || !strings.Contains(out, "field=title") { t.Errorf("ожидался Debug-лог о санитайзинге title, got: %q", out) } } func TestSanitizePlan_NoLogWhenClean(t *testing.T) { var buf bytes.Buffer log := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})) p := Plan{Title: "Harold", OriginalTitle: "The Matrix"} sanitizePlan(&p, log) if buf.Len() != 0 { t.Errorf("для чистых полей лога быть не должно, got: %q", buf.String()) } }