8f0f2ef7e7
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.
8.9 KiB
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.cs—IWizardMessenger(Edit/Send/Answer/GetOwnerClubs),WizardAction+WizardActionStyle,WizardKeyboard,WizardClubOption,WizardInteraction,IWizardDraftRepository(now takesplatform, ownerId).src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepLimits.cs—MaxTitleLength,MaxDescriptionLength,MaxSystemLength, capacity and duration bounds (moved out of the Bot-onlyWizardStep).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:valueformat 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-onlyWizardStep.Render(text, InlineKeyboardMarkup).src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs— moved from Bot; rewritten to takeIWizardMessenger+IWizardDraftRepositoryand aWizardInteraction(no moreUpdate/Message/CallbackQuery).
Updated (Shared)
src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs—ChatId,MessageThreadId,OwnerId,DraftMessageIdswitched tostring?to fit both Telegram and Discord ids; newPlatformfield (defaults to"Telegram"for backward compatibility with pre-V032 rows).src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs—GetActiveAsync(platform, ownerId); selects/inserts the newplatformcolumn and the renamedowner_id.- (Deleted)
src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/IWizardDraftRepository.cs— merged intoIWizardMessenger.csto keep wizard contracts colocated.
New (Bot — Telegram adapter)
src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardInteractionMapper.cs— convertsTelegram.Bot.Types.Update→WizardInteraction. The single bridge between Telegram's update type and the platform-neutral wizard.src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/TelegramWizardMessenger.cs— rewritten to implementIWizardMessenger; serialises(WizardDraft, text, IReadOnlyList<WizardAction>)to TelegramEditMessageText/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 theWizardStepLimitsconstants (so legacy call sites still work) and now delegates toWizardStepViewBuilder.Build(...)for the (text, actions) pair, then converts actions toInlineKeyboardMarkup.src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs— depends onIWizardMessenger;StartWizardAsync/SubmitDraftAsyncuse the new messenger contract; setsPlatform = "Telegram"on the draft; uses string ids forChatId/MessageThreadId/OwnerId.src/GmRelay.Bot/Infrastructure/Telegram/UpdateRouter.cs— takes the SharedGameCreationWizard; usesWizardInteractionMapper.TryMapto feed the wizard with a platform-neutralWizardInteraction. Draft lookup uses(platform, ownerId).src/GmRelay.Bot/Program.cs— DI:IWizardMessenger→TelegramWizardMessenger;GameCreationWizardresolved 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— addsplatform TEXT NOT NULL DEFAULT 'Telegram'; convertschat_id,message_thread_id,draft_message_idfrom numeric types toTEXT; renamesowner_telegram_id→owner_idand converts toTEXT; rebuilds the owner lookup index to use(platform, owner_id).
Tests
tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.cs—FakeWizardMessengernow implementsIWizardMessenger;NewDraftsetsPlatform = "Telegram"and uses string ids; new helper factoriesCallbackInteraction,TextInteraction,PhotoInteractionbuild the platform-neutralWizardInteraction; legacyCallbackUpdate/TextUpdatehelpers preserved for router-level tests that still consumeUpdate.tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs— schema updated to TEXT columns +platformcolumn.tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs— uses the newGetActiveAsync(platform, ownerId)signature and string ids.- All wizard / create-session test files updated to call
wizard.HandleInteractionAsync(...)with aWizardInteractioninstead ofwizard.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.cs—NewDraftupdated to use stringChatId.
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 inCampaignTemplatesNavigationTests.NavMenu_ShouldExposeCurrentProjectVersionwhich expects the oldv3.7.1string inNavMenu.razorafter the 3.8.0 release bump in commit71080ae— 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.csexists exactly once, insrc/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/.IWizardMessengerlives insrc/GmRelay.Shared/...;ITelegramWizardMessengeris 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-levelwizard:resume/wizard:resetcontrols are still produced byUpdateRouter. WizardDraft.DraftMessageIdswitched fromlong?tostring?so the same column can hold Discord's 64-bit snowflakes. V032 convertsBIGINT → TEXT; existing rows in the V031 schema (if any are still live) survive the cast.- The
FakeWizardMessengerkeeps the long-typed recording shape (Edits/Sendstuples) so the existing assertions inCreateSessionHandlerSubmitMissingFieldsTestsetc. keep working without rewrites. The fake convertsdraft.ChatId/draft.MessageThreadIdto long on the way out, matching the old test contract. - The Telegram-renderer Bot-side
WizardStepkeeps the same public surface (WizardStep.Render(draft, payload, clubs)returning(string, InlineKeyboardMarkup)) so call sites inUpdateRouter.TryHandleDraftControlCallbackAsyncand theCreateSessionHandlercontinue to work. The view layer behind it is nowWizardStepViewBuilder. - A future
DiscordWizardMessengerandDiscordWizardInteractionMappercan 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).