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.
This commit is contained in:
+157
@@ -0,0 +1,157 @@
|
||||
# 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 takes `platform, ownerId`).
|
||||
- `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepLimits.cs`
|
||||
— `MaxTitleLength`, `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.cs`
|
||||
— `ChatId`, `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.cs`
|
||||
— `GetActiveAsync(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.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 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: `IWizardMessenger` →
|
||||
`TelegramWizardMessenger`; `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_id` → `owner_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.cs`
|
||||
— `FakeWizardMessenger` 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.cs`
|
||||
— `NewDraft` 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).
|
||||
Reference in New Issue
Block a user