Files
GmRelayBot/.hermes/plans/gmrelay-issue-19.md
root 14b9bf15f2 refactor(#22): разделить SessionBatchRenderer на neutral view и Telegram renderer
- SessionBatchViewBuilder в Shared собирает нейтральную view model
- TelegramSessionBatchRenderer в Bot/Web рендерит HTML + InlineKeyboardMarkup
- DiscordSessionBatchRenderer заглушка подготовлена
- BatchMessageEditor перенесён из Shared в Bot/Web
- Удалён SessionBatchRenderer, убран Telegram.Bot из Shared.csproj
- Обновлены все вызовы (7 handler-ов + Web SessionService + smoke tests)
- Новые тесты на builder и Telegram renderer
2026-05-06 08:28:25 +00:00

9.0 KiB
Raw Permalink Blame History

Issue #19: Выровнять /newsession с batch-сценарием лендинга

Goal: Сделать /newsession атомарным: либо весь batch создаётся целиком, либо ничего — с понятным сообщением об ошибках. Убрать создание частичных сессий при наличии любых ошибок ввода.

Architecture: Изменить NewSessionParseResult.IsValid так, чтобы он учитывал ЛЮБЫЕ parse-ошибки (некорректные даты, лимиты, повторы, прошедшие даты). Обновить CreateSessionHandler, чтобы при !IsValid отправлялось одно детальное сообщение с перечислением ошибок и help-шаблоном, и creation прерывался до транзакции БД.

Tech Stack: C#, .NET, Dapper, Telegram.Bot, xUnit


Task 1: Добавить HasErrors и ужесточить IsValid

Objective: Запретить частичное создание: IsValid == false, если есть хоть одна ошибка парсинга.

Files:

  • Modify: src/GmRelay.Bot/Features/Sessions/CreateSession/NewSessionCommandParser.cs
  • Test: tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs

Step 1: RED — написать failing test

Добавить в конец NewSessionCommandParserTests.cs:

[Fact]
public void Parse_ShouldBeInvalid_WhenAnyErrorsPresent()
{
    var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
    var text = """
        Название: Delta Green
        Время: 25.04.2026 19:30
        Время: 31.04.2026 19:30
        Ссылка: https://example.test/dg
        """;

    var result = NewSessionCommandParser.Parse(text, nowUtc);

    Assert.True(result.HasErrors);
    Assert.False(result.IsValid);
}

Step 2: Run test and verify RED

cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~NewSessionCommandParserTests" -v n

Expected: HasErrors property not found → compile error or FAIL.

Step 3: GREEN — minimal implementation

В NewSessionCommandParser.cs, в NewSessionParseResult, заменить IsValid на:

public bool HasErrors =>
    PastTimeInputs.Count > 0 ||
    InvalidTimeInputs.Count > 0 ||
    InvalidSeatLimitInputs.Count > 0 ||
    InvalidRecurringInputs.Count > 0;

public bool IsValid =>
    !string.IsNullOrWhiteSpace(Title) &&
    !string.IsNullOrWhiteSpace(Link) &&
    ScheduledTimes.Count > 0 &&
    !HasErrors;

Step 4: Run test and verify GREEN

cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~NewSessionCommandParserTests" -v n

Expected: 7 passed (включая новый).

Step 5: Commit

git add src/GmRelay.Bot/Features/Sessions/CreateSession/NewSessionCommandParser.cs
git add tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs
git commit -m "feat(#19): add HasErrors to prevent partial batch creation"

Task 2: Обновить существующий тест на Past/Invalid times

Objective: Старый тест ожидал IsValid == true при partial invalid — теперь это баг, нужно обновить assertion.

Files:

  • Modify: tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs:114

Step 1: Write failing expectation first

Заменить в методе Parse_ShouldCollectPastAndInvalidTimes (примерно строка 114):

// Было:
Assert.True(result.IsValid);
// Стало:
Assert.False(result.IsValid);

Step 2: Run test and verify RED

cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "Parse_ShouldCollectPastAndInvalidTimes" -v n

Expected: FAIL — expected False, actual True.

Step 3: Fix already done in Task 1

Task 1 уже изменил IsValid. Перезапустить тест.

Step 4: Run test and verify GREEN

cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "Parse_ShouldCollectPastAndInvalidTimes" -v n

Expected: PASS.

Step 5: Commit

git add tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs
git commit -m "test(#19): update assertion - partial invalid means invalid"

Task 3: Обновить CreateSessionHandler для атомарной валидации и единого сообщения об ошибках

Objective: Убрать разбросанные warning-сообщения. При любых ошибках — одно сообщение с перечнем ошибок + help-шаблон. Не создавать сессии.

Files:

  • Modify: src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs

Step 1: RED — напишем интеграционный поведенческий тест (опционально, landing test уже покрывает happy path)

Просто запустим landing promise smoke test и убедимся, что он ещё green (happy path не сломан).

cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "TelegramLandingPromisesSmokeTests" -v n

Expected: PASS.

Step 2: GREEN — minimal change in handler

В CreateSessionHandler.cs, метод HandleAsync, заменить начало (строки 19–59) на:

var parseResult = NewSessionCommandParser.Parse(message.Text ?? message.Caption, DateTimeOffset.UtcNow);

var errorMessages = new List<string>();

foreach (var timeInput in parseResult.PastTimeInputs)
    errorMessages.Add($"⚠️ Дата {timeInput} находится в прошлом и будет пропущена.");

foreach (var timeInput in parseResult.InvalidTimeInputs)
    errorMessages.Add($"⚠️ Некорректный формат времени '{timeInput}'. Пропущено.");

foreach (var seatLimitInput in parseResult.InvalidSeatLimitInputs)
    errorMessages.Add($"⚠️ Некорректный лимит мест '{seatLimitInput}'. Укажите целое число больше 0.");

foreach (var recurringInput in parseResult.InvalidRecurringInputs)
    errorMessages.Add($"⚠️ Некорректный повтор расписания '{recurringInput}'. Укажите число игр 1–52 и шаг 1–365 дней.");

if (!parseResult.IsValid)
{
    var helpText = """
         Не удалось распознать формат. Пожалуйста, используйте шаблон:

        /newsession
        Название: My Game
        Время: 15.05.2026 19:30
        Время: 22.05.2026 19:30
        Мест: 4
        Ссылка: https://link
        Картинка: https://cover

        Для повтора можно указать одну дату и строки:
        Игр: 4
        Интервал: 7
        """;

    if (errorMessages.Count > 0)
    {
        helpText = string.Join('\n', errorMessages) + "\n\n" + helpText;
    }

    await botClient.SendMessage(
        chatId: message.Chat.Id,
        text: helpText,
        cancellationToken: cancellationToken);
    return;
}

Step 3: Build and verify

cd /repo && dotnet build src/GmRelay.Bot

Expected: Build succeeded.

Step 4: Run all relevant tests

cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~CreateSession|FullyQualifiedName~Landing" -v n

Expected: All passed.

Step 5: Commit

git add src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs
git commit -m "feat(#19): atomic validation with detailed error message in /newsession"

Task 4: Проверить полный test suite на регрессии

Objective: Убедиться, что изменения не сломали существующие тесты.

Files:

  • Test: все тесты проекта GmRelay.Bot.Tests

Step 1: Run full test suite

cd /repo && dotnet test tests/GmRelay.Bot.Tests -v n

Expected: All passed.

Step 2: Commit (if any test baseline updated)

Если всё зелёное — commit message:

git commit --allow-empty -m "test(#19): verify full suite green after atomic validation"

Verification Checklist

  • NewSessionCommandParser.Parse возвращает IsValid = false при любых ошибках (past/invalid times, seat limits, recurring).
  • HasErrors true ↔ есть хотя бы одна ошибка.
  • CreateSessionHandler не открывает БД-транзакцию, если !IsValid.
  • Пользователь получает одно сообщение с перечислением ошибок + help.
  • Landing promise smoke test проходит (happy path не сломан).
  • Полный test suite зелёный.