In the session creation wizard (Telegram + Discord), the Capacity step
only exposed waitlist on/off buttons. The 'no waitlist' button silently
advanced to the next step without setting MaxPlayers, so users who tried
to create a session with no player cap were blocked with
'Не заполнены поля: лимит мест'.
The DB contract and CreateSessionCommand already supported null
MaxPlayers (int?, ck_sessions_max_players check in V006), and the web
form already exposes 'Без лимита' as an empty InputNumber — only the
wizard flow was broken.
Changes:
- Add '♾ Без лимита' choice button to Capacity in shared
WizardStepViewBuilder.BuildCapacity (Telegram) and to
RenderCapacity / RenderPoolSlotCapacity in DiscordWizardStep (Discord).
- Add 'no_limit' branch to GameCreationWizard.ApplyCapacityChoice that
sets MaxPlayers to null and advances to Visibility.
- Change GameCreationWizard.SetMaxPlayers signature from int to int? so
the 'no limit' branch compiles.
- Change CreateSessionCommand builder in both Telegram and Discord
submitters to take int? maxPlayers and drop the '?? 0' that would
have turned null into 0 (violating the DB CHECK and the 'no limit'
contract).
- In Discord BuildConfirmDescription, render '👥 Без лимита, waitlist
вкл/выкл' when MaxPlayers is null (the previous code silently
omitted the line).
- Expose BuildCommand as internal in both submitters and add
InternalsVisibleTo('GmRelay.Bot.Tests') to the DiscordBot assembly
for unit-test access.
Tests (9 new):
- WizardStepRenderTests.CapacityStep_HasWaitlistButtons — asserts the
'Без лимита' button is present.
- GameCreationWizardStepTransitionsTests.NoLimitCapacityButton_… —
asserts the choice advances to Visibility and leaves MaxPlayers null
in the JSON draft.
- GameCreationWizardStepTransitionsTests.ChoiceCallback_AdvancesToExpectedStep —
new Theory row for Capacity/no_limit.
- CreateSessionHandlerBuildCommandTests (new) — null/value propagation
through the Telegram submitter's BuildCommand.
- DiscordWizardStepCapacityRenderTests (new) — 'Без лимита' button is
rendered for both Capacity and PoolSlotCapacity, with the expected
custom-id shape.
- DiscordWizardSubmitterBuildCommandTests (new) — null/value
propagation through the Discord submitter's BuildCommand.
Closes#123
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.
- Extract IWizardDraftRepository interface for testability (NSubstitute cannot
mock sealed classes; the codebase uses fake-style doubles instead).
- Add step-transition, pool-slot, validation, cancel/back, and render-shape tests
using FakeWizardDraftRepository and FakeWizardMessenger.
- Fix wizard payload persistence bug: HandleCallbackAsync and HandleTextAsync
now call SavePayload after ApplyChoice/ApplyText mutations, so subsequent
LoadPayload calls see the user's progress. Previously, local WizardPayload
mutations were discarded and the wizard reset on every step.
- CommitCurrentPoolSlot now auto-creates a slot via EnsureCurrentPoolSlot when
one is missing, so the PoolSlotCapacity → waitlist click is recoverable
even if the user lands on the step without a slot.