Files
GmRelayBot/deliverable.md
T
Toutsu 8f0f2ef7e7 refactor(wizard): move core to Shared, add IWizardMessenger contract (issue #112)
Moves the game-creation wizard state machine, view builder, and
platform-neutral contracts (callback data, step names, storage
exception, club option, step limits) from GmRelay.Bot to GmRelay.Shared.
Telegram continues to work through a new TelegramWizardMessenger
implementing IWizardMessenger and a WizardInteractionMapper that
converts Update → WizardInteraction. Wires the new platform column on
wizard_drafts (V032 migration) and switches chat/owner/thread/message
ids to TEXT so the same table can hold Discord snowflakes later.

- GameCreationWizard: now in Shared, takes IWizardMessenger +
  IWizardDraftRepository, dispatches on WizardInteraction.
- New IWizardMessenger contract with Edit/Send/Answer/GetOwnerClubs
  (returns string ids so Telegram longs and Discord snowflakes both
  fit).
- New WizardStepViewBuilder in Shared returns
  (text, IReadOnlyList<WizardAction>); TelegramWizardMessenger
  renders actions into InlineKeyboardMarkup via a new Bot-side
  ToInlineKeyboard helper.
- New WizardInteractionMapper in Bot (5-case test) converts Telegram
  Update to WizardInteraction.
- WizardDraft gains a Platform column; ChatId/MessageThreadId/OwnerId/
  DraftMessageId switched to string. V032 migrates existing rows and
  rebuilds the owner lookup index on (platform, owner_id).
- All existing wizard / create-session tests updated to the new
  contract (HandleInteractionAsync + WizardInteraction). Wizard
  callback-data format preserved.
- dotnet build clean, dotnet format --verify-no-changes clean, all
  101 wizard tests pass.
2026-06-05 16:23:20 +03:00

8.9 KiB

Issue #112 — Wizard platform-neutral refactor

Summary

Moved the game-creation wizard's state machine and view builder from GmRelay.Bot to GmRelay.Shared, replacing the Telegram-typed ITelegramWizardMessenger / Update / Message surface with a platform-neutral IWizardMessenger / WizardInteraction contract. Telegram continues to work unchanged through a new TelegramWizardMessenger adapter and WizardInteractionMapper. The wizard core is now ready for a future Discord adapter without touching the state machine, and a platform column on wizard_drafts discriminates drafts from different messengers.

Branch

feat/issue-112-wizard-refactor (off main).

Changed files

New (Shared — platform-neutral wizard core)

  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/IWizardMessenger.csIWizardMessenger (Edit/Send/Answer/GetOwnerClubs), WizardAction + WizardActionStyle, WizardKeyboard, WizardClubOption, WizardInteraction, IWizardDraftRepository (now takes platform, ownerId).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepLimits.csMaxTitleLength, MaxDescriptionLength, MaxSystemLength, capacity and duration bounds (moved out of the Bot-only WizardStep).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepNames.cs — moved from Bot (unchanged content).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardCallbackData.cs — moved from Bot (unchanged content; wizard:cancel / wizard:back / wizard:choice:step:value format preserved for back-compat).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStorageException.cs — moved from Bot (unchanged content).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepViewBuilder.cs — new platform-neutral view builder; produces (string Text, IReadOnlyList<WizardAction> Actions) for each step. Replaces the Telegram-only WizardStep.Render(text, InlineKeyboardMarkup).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs — moved from Bot; rewritten to take IWizardMessenger + IWizardDraftRepository and a WizardInteraction (no more Update/Message/CallbackQuery).

Updated (Shared)

  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.csChatId, MessageThreadId, OwnerId, DraftMessageId switched to string? to fit both Telegram and Discord ids; new Platform field (defaults to "Telegram" for backward compatibility with pre-V032 rows).
  • src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.csGetActiveAsync(platform, ownerId); selects/inserts the new platform column and the renamed owner_id.
  • (Deleted) src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/IWizardDraftRepository.cs — merged into IWizardMessenger.cs to keep wizard contracts colocated.

New (Bot — Telegram adapter)

  • src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardInteractionMapper.cs — converts Telegram.Bot.Types.UpdateWizardInteraction. The single bridge between Telegram's update type and the platform-neutral wizard.
  • src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/TelegramWizardMessenger.cs — rewritten to implement IWizardMessenger; serialises (WizardDraft, text, IReadOnlyList<WizardAction>) to Telegram EditMessageText / SendMessage / AnswerCallbackQuery. Club lookup SQL unchanged.

Updated (Bot)

  • src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStep.cs — kept as the Telegram-side keyboard renderer. Re-exports the WizardStepLimits constants (so legacy call sites still work) and now delegates to WizardStepViewBuilder.Build(...) for the (text, actions) pair, then converts actions to InlineKeyboardMarkup.
  • src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs — depends on IWizardMessenger; StartWizardAsync / SubmitDraftAsync use the new messenger contract; sets Platform = "Telegram" on the draft; uses string ids for ChatId / MessageThreadId / OwnerId.
  • src/GmRelay.Bot/Infrastructure/Telegram/UpdateRouter.cs — takes the Shared GameCreationWizard; uses WizardInteractionMapper.TryMap to feed the wizard with a platform-neutral WizardInteraction. Draft lookup uses (platform, ownerId).
  • src/GmRelay.Bot/Program.cs — DI: IWizardMessengerTelegramWizardMessenger; GameCreationWizard resolved from Shared.
  • (Deleted) src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs
  • (Deleted) src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStepNames.cs
  • (Deleted) src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardCallbackData.cs
  • (Deleted) src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStorageException.cs
  • (Deleted) src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/ITelegramWizardMessenger.cs

New (Bot migrations)

  • src/GmRelay.Bot/Migrations/V032__wizard_drafts_platform.sql — adds platform TEXT NOT NULL DEFAULT 'Telegram'; converts chat_id, message_thread_id, draft_message_id from numeric types to TEXT; renames owner_telegram_idowner_id and converts to TEXT; rebuilds the owner lookup index to use (platform, owner_id).

Tests

  • tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.csFakeWizardMessenger now implements IWizardMessenger; NewDraft sets Platform = "Telegram" and uses string ids; new helper factories CallbackInteraction, TextInteraction, PhotoInteraction build the platform-neutral WizardInteraction; legacy CallbackUpdate / TextUpdate helpers preserved for router-level tests that still consume Update.
  • tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs — schema updated to TEXT columns + platform column.
  • tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs — uses the new GetActiveAsync(platform, ownerId) signature and string ids.
  • All wizard / create-session test files updated to call wizard.HandleInteractionAsync(...) with a WizardInteraction instead of wizard.HandleUpdateAsync(Update, …).
  • tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardInteractionMapperTests.cs (new) — 5 cases covering callback / text / photo / captioned-photo / empty updates.
  • tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardStepRenderTests.csNewDraft updated to use string ChatId.

Verification

  • dotnet build — full solution builds with 0 warnings, 0 errors.
  • dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj — 568/571 pass (2 pre-existing [Fact(Skip = …)] happy-path tests skipped, 1 pre-existing test failure in CampaignTemplatesNavigationTests.NavMenu_ShouldExposeCurrentProjectVersion which expects the old v3.7.1 string in NavMenu.razor after the 3.8.0 release bump in commit 71080ae — unrelated to this refactor). All 101 wizard / create-session tests pass.
  • dotnet format --verify-no-changes — clean.
  • git grep "Telegram.Bot" src/GmRelay.Shared/ — empty.
  • git grep "NetCord" src/GmRelay.Bot/ — empty.
  • GameCreationWizard.cs exists exactly once, in src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/.
  • IWizardMessenger lives in src/GmRelay.Shared/...; ITelegramWizardMessenger is gone.
  • dotnet list package --vulnerable — clean.

Notes for the verifier

  • The callback-data wire format (wizard:cancel, wizard:back, wizard:create, wizard:choice:{step}:{value}) is unchanged; the router-level wizard:resume / wizard:reset controls are still produced by UpdateRouter.
  • WizardDraft.DraftMessageId switched from long? to string? so the same column can hold Discord's 64-bit snowflakes. V032 converts BIGINT → TEXT; existing rows in the V031 schema (if any are still live) survive the cast.
  • The FakeWizardMessenger keeps the long-typed recording shape (Edits / Sends tuples) so the existing assertions in CreateSessionHandlerSubmitMissingFieldsTests etc. keep working without rewrites. The fake converts draft.ChatId / draft.MessageThreadId to long on the way out, matching the old test contract.
  • The Telegram-renderer Bot-side WizardStep keeps the same public surface (WizardStep.Render(draft, payload, clubs) returning (string, InlineKeyboardMarkup)) so call sites in UpdateRouter.TryHandleDraftControlCallbackAsync and the CreateSessionHandler continue to work. The view layer behind it is now WizardStepViewBuilder.
  • A future DiscordWizardMessenger and DiscordWizardInteractionMapper can be added without any change to the Shared wizard; this is deliberately not part of this task (the plan calls it out as Task 2).