fix(bot): IsComplete must not flag null MaxPlayers as missing (no-limit)
PR Checks / test-and-build (pull_request) Successful in 12m33s

After 3.9.6 fixed long-polling, the bot finally reaches the final
' Создать' step. Users pressing '♾ Без лимита' on the Capacity step
get a valid payload where Single.MaxPlayers = null (the legitimate
no-limit choice from GameCreationWizard.ApplyCapacityChoice 'no_limit'),
but CreateSessionHandler.IsComplete then reports 'лимит мест' as
missing, blocking session creation.

This regression existed since 3.9.3 (when 'no_limit' was added) but
stayed invisible because 3.9.4 and 3.9.5 never reached SubmitDraft
(libgssapi-krb5 missing → long-polling hung). Once 3.9.6 restored
polling, the bug surfaced immediately.

Fix: drop the null-MaxPlayers check from IsComplete for Single type.
Null is a valid 'no limit' state and must pass through to BuildCommands
→ shared handler, which already accepts null MaxPlayers correctly.

Closes #131.

Bump version 3.9.6 -> 3.9.7
This commit is contained in:
2026-06-09 13:15:15 +03:00
parent 4424d8faad
commit 320ec18ab0
6 changed files with 54 additions and 7 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ on:
- main - main
env: env:
VERSION: 3.9.6 VERSION: 3.9.7
jobs: jobs:
# ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами)
+1 -1
View File
@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>3.9.6</Version> <Version>3.9.7</Version>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<LangVersion>preview</LangVersion> <LangVersion>preview</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
+3 -3
View File
@@ -49,7 +49,7 @@ services:
crond -f crond -f
bot: bot:
image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.6 image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.7
restart: always restart: always
depends_on: depends_on:
db: db:
@@ -67,7 +67,7 @@ services:
retries: 3 retries: 3
discord: discord:
image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.6 image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.7
restart: always restart: always
depends_on: depends_on:
db: db:
@@ -86,7 +86,7 @@ services:
retries: 3 retries: 3
web: web:
image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.6 image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.7
restart: always restart: always
depends_on: depends_on:
db: db:
@@ -229,7 +229,11 @@ public sealed class CreateSessionHandler
if (p.Type == WizardCreationType.Single) if (p.Type == WizardCreationType.Single)
{ {
if (p.Single?.ScheduledAt is null) missingFields.Add("дата/время"); if (p.Single?.ScheduledAt is null) missingFields.Add("дата/время");
if (p.Single?.MaxPlayers is null) missingFields.Add("лимит мест"); // MaxPlayers = null is a valid "♾ Без лимита" choice
// (see GameCreationWizard.ApplyCapacityChoice "no_limit"). Only a
// missing wizard step on the numeric-input path is a real defect;
// a non-null 0 also blocks (zero players is never what the user
// asked for). Treat null as "no limit" and accept it.
} }
else else
{ {
@@ -82,7 +82,7 @@
</button> </button>
</form> </form>
<div class="nav-version">v3.9.6</div> <div class="nav-version">v3.9.7</div>
</div> </div>
</Authorized> </Authorized>
<NotAuthorized> <NotAuthorized>
@@ -146,4 +146,47 @@ public sealed class CreateSessionHandlerSubmitValidationTests
Assert.Single(messenger.Edits); Assert.Single(messenger.Edits);
Assert.Contains("слоты", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase); Assert.Contains("слоты", messenger.Edits[0].Text, StringComparison.OrdinalIgnoreCase);
} }
[Fact]
public async Task SubmitDraftAsync_SingleWithNoLimit_DoesNotReportMaxPlayersAsMissing()
{
// Regression for #131: pressing "♾ Без лимита" sets MaxPlayers = null.
// IsComplete must NOT flag that as a missing field; null means
// "no player limit" and is a valid final state.
var drafts = new FakeWizardDraftRepository();
var messenger = new FakeWizardMessenger();
var sut = new CreateSessionHandler(
drafts,
shared: null!,
messenger,
NullLogger<CreateSessionHandler>.Instance);
var payload = new WizardPayload
{
Type = WizardCreationType.Single,
Title = "T",
System = "Dnd5e",
DurationMinutes = 240,
Visibility = WizardVisibility.Public,
Single = new WizardSingleInput
{
ScheduledAt = DateTimeOffset.UtcNow.AddDays(7),
MaxPlayers = null,
},
};
var draft = NewDraft(WizardStepNames.Confirm, payload);
drafts.Seed(draft);
await sut.SubmitDraftAsync(draft, CancellationToken.None);
// Validation must let the no-limit payload through. The shared
// handler is null, so anything that reached the database call would
// throw a NullReferenceException — that is caught by the retry
// path and reported as a "💥 Ошибка:" edit, not a missing-fields
// edit. Therefore we assert that NO edit mentions a missing field.
Assert.NotEmpty(messenger.Edits);
var lastEdit = messenger.Edits[^1].Text;
Assert.DoesNotContain("Не заполнены", lastEdit, StringComparison.OrdinalIgnoreCase);
}
} }