diff --git a/internal/tgbot/bot_test.go b/internal/tgbot/bot_test.go
index d57a737..8d5ab63 100644
--- a/internal/tgbot/bot_test.go
+++ b/internal/tgbot/bot_test.go
@@ -413,6 +413,39 @@ func TestBot_NotifyFailedEscapesExternalText(t *testing.T) {
}
}
+// Карточка ревью: спецсимволы во внешнем пути раскладки и в причинах
+// распознавания экранируются (esc после tailPath/join), сырая разметка не
+// просачивается. Закрывает сценарий «Внешний путь и причины экранируются».
+func TestReviewCard_EscapesPathAndReasons(t *testing.T) {
+ b, _, _, _ := newTestBot(t, []int64{7})
+ rd := reviewData(store.StateReview)
+ rd.Recognition.Reasons = `["Tom & Jerry x"]`
+ rd.Preview[0].Dst = "/srv/media/series/A & B /file .mkv"
+ text, _ := b.renderCard(rd)
+
+ if !strings.Contains(text, "Tom & Jerry <b>x</b>") {
+ t.Errorf("причины не экранированы:\n%s", text)
+ }
+ if !strings.Contains(text, "A & B <x>") || !strings.Contains(text, "file <y>.mkv") {
+ t.Errorf("путь раскладки не экранирован:\n%s", text)
+ }
+ if strings.Contains(text, "") || strings.Contains(text, "") {
+ t.Errorf("сырая разметка просочилась:\n%s", text)
+ }
+}
+
+// opErr: при наличии download id он уходит моноширинным (tap-to-copy) под
+// ключом download_id=; без id — дружелюбный текст без ключа. Закрывает сценарий
+// «Id в сообщении об ошибке копируется».
+func TestOpErr_IDMonospace(t *testing.T) {
+ if got := opErr("Не удалось выполнить действие", tid); !strings.Contains(got, "download_id="+idCode(tid)) {
+ t.Errorf("id не моноширинным под ключом download_id: %q", got)
+ }
+ if got := opErr("Готово", ""); strings.Contains(got, "download_id") {
+ t.Errorf("без id ключа download_id быть не должно: %q", got)
+ }
+}
+
func TestBot_CallbackRetry(t *testing.T) {
b, _, _, rev := newTestBot(t, []int64{7})
rev.data = reviewData(store.StateFailed)