layout: непомещающееся целевое имя уводит задачу в review вместо failed
- предел длины компонента (255 байт) проверяется в BuildLinks до первой операции с ФС: ни каталога, ни ссылки при отказе не создаётся - причина пустого предпросмотра считается на показе (ReviewData.PreviewError) и печатается в панели действий и в карточке Telegram: у задачи без записанной причины взять её больше неоткуда
This commit is contained in:
@@ -400,6 +400,15 @@ func (b *Bot) handleCallback(ctx context.Context, cq *tgbotapi.CallbackQuery) {
|
||||
b.send(chatID, opErr("Торрент ещё качается — дождитесь докачки", id), nil)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, layout.ErrNameTooLong) {
|
||||
// Имя не помещается в файловую систему: задача штатно ушла в review,
|
||||
// где название правится подсказкой. Карточку обновляем — в ней теперь
|
||||
// причина.
|
||||
b.answer(cq.ID, "Имя не помещается")
|
||||
b.send(chatID, opErr("Целевое имя слишком длинное — задача отправлена в ревью", id), nil)
|
||||
b.refreshCard(ctx, chatID, msgID, id)
|
||||
return
|
||||
}
|
||||
if errors.Is(err, layout.ErrCollision) {
|
||||
// Коллизия цели: задача штатно ушла в review с причиной — показываем
|
||||
// конкретно и обновляем карточку (в ней теперь причина коллизии).
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -78,6 +79,7 @@ type fakeReviewer struct {
|
||||
deleted []string
|
||||
dismissed []string
|
||||
chosen map[string]string // downloadID → выбранный candidateID
|
||||
applyErr error // чем отвечает Apply (nil — успехом)
|
||||
}
|
||||
|
||||
func (f *fakeReviewer) ReviewData(context.Context, string) (*worker.ReviewData, error) {
|
||||
@@ -85,7 +87,7 @@ func (f *fakeReviewer) ReviewData(context.Context, string) (*worker.ReviewData,
|
||||
}
|
||||
func (f *fakeReviewer) Apply(_ context.Context, id string) error {
|
||||
f.applied = append(f.applied, id)
|
||||
return nil
|
||||
return f.applyErr
|
||||
}
|
||||
func (f *fakeReviewer) Refine(_ context.Context, id string, hint string) error {
|
||||
if f.refined == nil {
|
||||
@@ -614,3 +616,23 @@ func TestBot_IngestErrorPromisesNoDownloadID(t *testing.T) {
|
||||
t.Errorf("в отказе обещан идентификатор загрузки: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Непомещающееся целевое имя — штатный отказ, чинимый на ревью: бот отвечает
|
||||
// конкретно и обновляет карточку (в ней теперь причина), как для коллизии, а не
|
||||
// падает в общее «Не удалось выполнить действие».
|
||||
func TestBot_CallbackApplyNameTooLong(t *testing.T) {
|
||||
b, api, _, rev := newTestBot(t, []int64{7})
|
||||
rev.applyErr = fmt.Errorf("apply: %w", layout.ErrNameTooLong)
|
||||
|
||||
b.handleCallback(context.Background(), cbFrom(7, "apply:"+tid))
|
||||
|
||||
if len(api.answers) != 1 || !strings.Contains(api.answers[0], "не помещается") {
|
||||
t.Errorf("answers = %+v, ожидался конкретный ответ", api.answers)
|
||||
}
|
||||
if len(api.sent) != 1 || !strings.Contains(api.sent[0].text, "слишком длинное") {
|
||||
t.Errorf("sent = %+v, ожидалось конкретное сообщение", api.sent)
|
||||
}
|
||||
if len(api.edits) != 1 {
|
||||
t.Errorf("edits = %v, карточка должна обновиться причиной", api.edits)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,11 +89,26 @@ func (b *Bot) reviewCard(rd *worker.ReviewData) (string, *tgbotapi.InlineKeyboar
|
||||
}
|
||||
if n := len(rd.Preview); n > 0 {
|
||||
fmt.Fprintf(&sb, "План: %d файлов → %s", n, esc(tailPath(rd.Preview[0].Dst)))
|
||||
} else if why := previewProblem(rd); why != "" {
|
||||
// Плана нет — без этой строки карточка молчит о том, почему нет и «Применить».
|
||||
// Причина показа (PreviewError) старше записанной: она про текущий план, а
|
||||
// error_msg остался от последнего перехода.
|
||||
fmt.Fprintf(&sb, "Не удаётся построить план: %s", esc(shorten(why, 120)))
|
||||
}
|
||||
|
||||
return strings.TrimRight(sb.String(), "\n"), b.reviewKeyboard(rd)
|
||||
}
|
||||
|
||||
// previewProblem — почему у задачи в review нет плана: причина, посчитанная на
|
||||
// показе, либо записанная при последнем переходе. Пусто — объяснять нечего
|
||||
// (источник ещё не подтверждён).
|
||||
func previewProblem(rd *worker.ReviewData) string {
|
||||
if rd.PreviewError != "" {
|
||||
return rd.PreviewError
|
||||
}
|
||||
return rd.Download.ErrorMsg.String
|
||||
}
|
||||
|
||||
func (b *Bot) reviewKeyboard(rd *worker.ReviewData) *tgbotapi.InlineKeyboardMarkup {
|
||||
id := rd.Download.ID
|
||||
sid := id
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package tgbot
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
)
|
||||
|
||||
// Карточка ревью без плана называет причину: иначе владелец видит «Нужно
|
||||
// подтверждение» без плана и без кнопки «Применить» и не знает, что чинить.
|
||||
// Причина показа старше записанной — она про текущий план.
|
||||
func TestReviewCard_NamesPreviewProblem(t *testing.T) {
|
||||
t.Run("причина показа", func(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
rd.Preview = nil
|
||||
rd.PreviewError = `layout: имя не помещается: "ЫЫЫ…ЫЫЫ" — 400 байт при пределе 255`
|
||||
rd.Download.ErrorMsg = store.NullString("устаревшая причина прошлого перехода")
|
||||
|
||||
text, _ := b.renderCard(rd)
|
||||
if !strings.Contains(text, "не помещается") {
|
||||
t.Errorf("карточка не назвала причину:\n%s", text)
|
||||
}
|
||||
if strings.Contains(text, "устаревшая") {
|
||||
t.Errorf("записанная причина не должна перебивать посчитанную на показе:\n%s", text)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("записанная причина, когда посчитанной нет", func(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
rd.Preview = nil
|
||||
rd.Download.ErrorMsg = store.NullString("целевой файл уже существует")
|
||||
|
||||
text, _ := b.renderCard(rd)
|
||||
if !strings.Contains(text, "уже существует") {
|
||||
t.Errorf("карточка не назвала записанную причину:\n%s", text)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("причины нет — строки нет", func(t *testing.T) {
|
||||
b, _, _, _ := newTestBot(t, []int64{7})
|
||||
rd := reviewData(store.StateReview)
|
||||
rd.Preview = nil
|
||||
|
||||
text, _ := b.renderCard(rd)
|
||||
if strings.Contains(text, "Не удаётся построить план") {
|
||||
t.Errorf("без причины строки быть не должно:\n%s", text)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user