From 15040eb954e94c3fcf692d5493c75b8f2f348435 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Mon, 8 Jun 2026 18:17:01 +0300 Subject: [PATCH 1/3] fix(bot,discord): allow 'no player limit' option in /newsession wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../CreateSession/CreateSessionHandler.cs | 6 +- .../Sessions/Wizard/DiscordWizardStep.cs | 10 ++- .../Sessions/Wizard/DiscordWizardSubmitter.cs | 6 +- .../Properties/AssemblyInfo.cs | 3 + .../Wizard/GameCreationWizard.cs | 3 +- .../Wizard/WizardStepViewBuilder.cs | 3 +- .../DiscordWizardStepCapacityRenderTests.cs | 64 ++++++++++++++ ...DiscordWizardSubmitterBuildCommandTests.cs | 85 +++++++++++++++++++ .../CreateSessionHandlerBuildCommandTests.cs | 85 +++++++++++++++++++ .../GameCreationWizardStepTransitionsTests.cs | 27 ++++++ .../Wizard/WizardStepRenderTests.cs | 1 + 11 files changed, 283 insertions(+), 10 deletions(-) create mode 100644 src/GmRelay.DiscordBot/Properties/AssemblyInfo.cs create mode 100644 tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardStepCapacityRenderTests.cs create mode 100644 tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardSubmitterBuildCommandTests.cs create mode 100644 tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/CreateSessionHandlerBuildCommandTests.cs diff --git a/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs b/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs index 29eba1a..d16db2f 100644 --- a/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs @@ -170,7 +170,7 @@ public sealed class CreateSessionHandler draft, p, new[] { p.Single?.ScheduledAt ?? default }, - p.Single?.MaxPlayers ?? 0, + p.Single?.MaxPlayers, isOneShot: true), }; } @@ -178,11 +178,11 @@ public sealed class CreateSessionHandler private static int MaxPlayersForPool(WizardPoolInput pool) => pool.Slots.Count == 0 ? 0 : pool.Slots.Max(s => s.MaxPlayers); - private static CreateSessionCommand BuildCommand( + internal static CreateSessionCommand BuildCommand( WizardDraft draft, WizardPayload p, IReadOnlyList scheduledTimes, - int maxPlayers, + int? maxPlayers, bool isOneShot) { var user = new PlatformUser( diff --git a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardStep.cs b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardStep.cs index b9aa1cb..ff3a596 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardStep.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardStep.cs @@ -252,11 +252,12 @@ public static class DiscordWizardStep private static DiscordWizardRender RenderCapacity() => new( "👥 Лимит мест", - "Введите лимит (1..50) и выберите waitlist.", + "Введите лимит (1..50), выберите waitlist или сразу «♾ Без лимита».", new[] { Row(ChoiceBtn("✅ Waitlist вкл", WizardStepNames.Capacity, "waitlist:on", ButtonStyle.Success), ChoiceBtn("❌ Без waitlist", WizardStepNames.Capacity, "waitlist:off", ButtonStyle.Danger)), + Row(ChoiceBtn("♾ Без лимита", WizardStepNames.Capacity, "no_limit", ButtonStyle.Primary)), Row(ControlBtn("⬅️ Назад", "back"), ControlBtn("❌ Отмена", "cancel", ButtonStyle.Danger)), }, @@ -371,11 +372,12 @@ public static class DiscordWizardStep private static DiscordWizardRender RenderPoolSlotCapacity() => new( "👥 Лимит слотов", - "Введите лимит (1..50) и выберите waitlist.", + "Введите лимит (1..50), выберите waitlist или сразу «♾ Без лимита».", new[] { Row(ChoiceBtn("✅ Waitlist вкл", WizardStepNames.PoolSlotCapacity, "waitlist:on", ButtonStyle.Success), ChoiceBtn("❌ Без waitlist", WizardStepNames.PoolSlotCapacity, "waitlist:off", ButtonStyle.Danger)), + Row(ChoiceBtn("♾ Без лимита", WizardStepNames.PoolSlotCapacity, "no_limit", ButtonStyle.Primary)), Row(ControlBtn("⬅️ Назад", "back"), ControlBtn("❌ Отмена", "cancel", ButtonStyle.Danger)), }, @@ -417,6 +419,10 @@ public static class DiscordWizardStep { sb.Append("👥 Мест: ").Append(mp).Append(", waitlist ").Append(p.Waitlist == true ? "вкл" : "выкл").AppendLine(); } + else if (p.Type == WizardCreationType.Single) + { + sb.Append("👥 Без лимита, waitlist ").Append(p.Waitlist == true ? "вкл" : "выкл").AppendLine(); + } sb.Append("🔒 Видимость: ").AppendLine(RenderVisibilityText(p.Visibility)); return sb.ToString(); } diff --git a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs index 5ab039c..8e067b2 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs @@ -134,7 +134,7 @@ public sealed class DiscordWizardSubmitter draft, p, new[] { p.Single?.ScheduledAt ?? default }, - p.Single?.MaxPlayers ?? 0, + p.Single?.MaxPlayers, isOneShot: true), }; } @@ -142,11 +142,11 @@ public sealed class DiscordWizardSubmitter private static int MaxPlayersForPool(WizardPoolInput pool) => pool.Slots.Count == 0 ? 0 : pool.Slots.Max(s => s.MaxPlayers); - private static CreateSessionCommand BuildCommand( + internal static CreateSessionCommand BuildCommand( WizardDraft draft, WizardPayload p, IReadOnlyList scheduledTimes, - int maxPlayers, + int? maxPlayers, bool isOneShot) { var user = new PlatformUser( diff --git a/src/GmRelay.DiscordBot/Properties/AssemblyInfo.cs b/src/GmRelay.DiscordBot/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..6620bd8 --- /dev/null +++ b/src/GmRelay.DiscordBot/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("GmRelay.Bot.Tests")] diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs index 294a675..2b523de 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs @@ -302,6 +302,7 @@ public sealed class GameCreationWizard { "waitlist:on" => (WizardStepNames.Visibility, SetWaitlist(p, true)), "waitlist:off" => (WizardStepNames.Visibility, SetWaitlist(p, false)), + "no_limit" => (WizardStepNames.Visibility, SetMaxPlayers(p, null)), _ => (null, "Неизвестный выбор"), }; @@ -416,7 +417,7 @@ public sealed class GameCreationWizard private static string? SetDurationMinutes(WizardPayload p, int? v) { p.DurationMinutes = v; return null; } private static string? SetScheduledAt(WizardPayload p, DateTimeOffset v) { p.Single ??= new WizardSingleInput(); p.Single.ScheduledAt = v; return null; } - private static string? SetMaxPlayers(WizardPayload p, int v) + private static string? SetMaxPlayers(WizardPayload p, int? v) { p.Single ??= new WizardSingleInput(); p.Single.MaxPlayers = v; return null; } private static string? SetWaitlist(WizardPayload p, bool v) { p.Waitlist = v; return null; } private static string? SetVisibility(WizardPayload p, WizardVisibility? v) { p.Visibility = v; return null; } diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepViewBuilder.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepViewBuilder.cs index fac6fae..8bff399 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepViewBuilder.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepViewBuilder.cs @@ -97,11 +97,12 @@ public static class WizardStepViewBuilder BackCancel()); private static (string, IReadOnlyList) BuildCapacity() => ( - "👥 Введите лимит мест (1..50) одним числом.\nЗатем нажмите кнопку waitlist.", + "👥 Введите лимит мест (1..50) одним числом.\nЗатем нажмите кнопку waitlist. Или сразу «♾ Без лимита».", new List { new("✅ Waitlist вкл", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:on"), WizardActionStyle.Success), new("❌ Без waitlist", WizardCallbackData.Choice(WizardStepNames.Capacity, "waitlist:off"), WizardActionStyle.Danger), + new("♾ Без лимита", WizardCallbackData.Choice(WizardStepNames.Capacity, "no_limit"), WizardActionStyle.Primary), }); private static (string, IReadOnlyList) BuildVisibility() => ( diff --git a/tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardStepCapacityRenderTests.cs b/tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardStepCapacityRenderTests.cs new file mode 100644 index 0000000..bbdd0c2 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardStepCapacityRenderTests.cs @@ -0,0 +1,64 @@ +using GmRelay.DiscordBot.Features.Sessions.Wizard; +using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; +using NetCord.Rest; +using Xunit; + +namespace GmRelay.Bot.Tests.Discord.Wizard; + +/// +/// Renderer tests for the Discord wizard's Capacity / PoolSlotCapacity steps. +/// Locks in the presence of the "♾ Без лимита" button so the user can pick +/// a session with no player cap (null in sessions.max_players), the +/// same affordance the Telegram wizard provides. +/// +public sealed class DiscordWizardStepCapacityRenderTests +{ + [Fact] + public void RenderCapacity_ContainsNoLimitButton() + { + var draft = new WizardDraft { Step = WizardStepNames.Capacity }; + var render = DiscordWizardStep.Render(draft, new WizardPayload()); + + var labels = ExtractButtonLabels(render); + Assert.Contains(labels, l => l.Contains("Без лимита", System.StringComparison.Ordinal)); + } + + [Fact] + public void RenderPoolSlotCapacity_ContainsNoLimitButton() + { + var draft = new WizardDraft { Step = WizardStepNames.PoolSlotCapacity }; + var render = DiscordWizardStep.Render(draft, new WizardPayload()); + + var labels = ExtractButtonLabels(render); + Assert.Contains(labels, l => l.Contains("Без лимита", System.StringComparison.Ordinal)); + } + + [Fact] + public void RenderCapacity_NoLimitButton_HasChoiceCustomIdForNoLimit() + { + var draft = new WizardDraft { Step = WizardStepNames.Capacity }; + var render = DiscordWizardStep.Render(draft, new WizardPayload()); + + var buttons = ExtractButtons(render); + var noLimit = buttons.SingleOrDefault(b => b.Label?.Contains("Без лимита", System.StringComparison.Ordinal) == true); + Assert.NotNull(noLimit); + Assert.StartsWith("wizard:btn:choice:Capacity:no_limit", noLimit!.CustomId); + } + + private static System.Collections.Generic.List ExtractButtonLabels( + DiscordWizardStep.DiscordWizardRender render) => + render.Components + .OfType() + .SelectMany(r => r.Components) + .OfType() + .Select(b => b.Label ?? string.Empty) + .ToList(); + + private static System.Collections.Generic.List ExtractButtons( + DiscordWizardStep.DiscordWizardRender render) => + render.Components + .OfType() + .SelectMany(r => r.Components) + .OfType() + .ToList(); +} diff --git a/tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardSubmitterBuildCommandTests.cs b/tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardSubmitterBuildCommandTests.cs new file mode 100644 index 0000000..d0d87ed --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Discord/Wizard/DiscordWizardSubmitterBuildCommandTests.cs @@ -0,0 +1,85 @@ +using GmRelay.DiscordBot.Features.Sessions.Wizard; +using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; +using Xunit; + +namespace GmRelay.Bot.Tests.Discord.Wizard; + +/// +/// Regression coverage for 's +/// BuildCommand: when the wizard payload carries no player limit +/// (user picked «♾ Без лимита» on the Capacity step), the resulting +/// CreateSessionCommand.MaxPlayers must be null — never +/// 0. 0 would violate the DB CHECK +/// ck_sessions_max_players in V006 and the contract that +/// null means "no limit". +/// +public sealed class DiscordWizardSubmitterBuildCommandTests +{ + [Fact] + public void BuildCommand_WhenSingleMaxPlayersIsNull_PropagatesNull() + { + var draft = new WizardDraft + { + Id = Guid.NewGuid(), + ChatId = "42", + OwnerId = "100", + Step = "confirm", + }; + var payload = new WizardPayload + { + Type = WizardCreationType.Single, + Title = "T", + System = "Dnd5e", + DurationMinutes = 240, + Visibility = WizardVisibility.Public, + Single = new WizardSingleInput + { + ScheduledAt = DateTimeOffset.UtcNow.AddDays(1), + MaxPlayers = null, + }, + }; + + var cmd = DiscordWizardSubmitter.BuildCommand( + draft, + payload, + new[] { payload.Single!.ScheduledAt!.Value }, + payload.Single.MaxPlayers, + isOneShot: true); + + Assert.Null(cmd.MaxPlayers); + } + + [Fact] + public void BuildCommand_WhenSingleMaxPlayersIsSet_PropagatesValue() + { + var draft = new WizardDraft + { + Id = Guid.NewGuid(), + ChatId = "42", + OwnerId = "100", + Step = "confirm", + }; + var payload = new WizardPayload + { + Type = WizardCreationType.Single, + Title = "T", + System = "Dnd5e", + DurationMinutes = 240, + Visibility = WizardVisibility.Public, + Single = new WizardSingleInput + { + ScheduledAt = DateTimeOffset.UtcNow.AddDays(1), + MaxPlayers = 5, + }, + }; + + var cmd = DiscordWizardSubmitter.BuildCommand( + draft, + payload, + new[] { payload.Single!.ScheduledAt!.Value }, + payload.Single.MaxPlayers, + isOneShot: true); + + Assert.Equal(5, cmd.MaxPlayers); + } +} diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/CreateSessionHandlerBuildCommandTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/CreateSessionHandlerBuildCommandTests.cs new file mode 100644 index 0000000..885d457 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/CreateSessionHandlerBuildCommandTests.cs @@ -0,0 +1,85 @@ +using GmRelay.Bot.Features.Sessions.CreateSession; +using GmRelay.Shared.Features.Sessions.CreateSession.Wizard; +using Xunit; + +namespace GmRelay.Bot.Tests.Features.Sessions.CreateSession.Wizard; + +/// +/// Regression coverage for : +/// when the wizard payload carries no player limit (e.g. user picked +/// «♾ Без лимита» on the Capacity step), the resulting +/// CreateSessionCommand.MaxPlayers must be null — never 0. +/// 0 would violate the DB CHECK constraint +/// (ck_sessions_max_players in V006) by inserting 0 instead of +/// NULL, which is the wire-level representation of "no limit". +/// +public sealed class CreateSessionHandlerBuildCommandTests +{ + [Fact] + public void BuildCommand_WhenSingleMaxPlayersIsNull_PropagatesNull() + { + var draft = new WizardDraft + { + Id = Guid.NewGuid(), + ChatId = "42", + OwnerId = "100", + Step = "confirm", + }; + var payload = new WizardPayload + { + Type = WizardCreationType.Single, + Title = "T", + System = "Dnd5e", + DurationMinutes = 240, + Visibility = WizardVisibility.Public, + Single = new WizardSingleInput + { + ScheduledAt = DateTimeOffset.UtcNow.AddDays(1), + MaxPlayers = null, + }, + }; + + var cmd = CreateSessionHandler.BuildCommand( + draft, + payload, + new[] { payload.Single!.ScheduledAt!.Value }, + payload.Single.MaxPlayers, + isOneShot: true); + + Assert.Null(cmd.MaxPlayers); + } + + [Fact] + public void BuildCommand_WhenSingleMaxPlayersIsSet_PropagatesValue() + { + var draft = new WizardDraft + { + Id = Guid.NewGuid(), + ChatId = "42", + OwnerId = "100", + Step = "confirm", + }; + var payload = new WizardPayload + { + Type = WizardCreationType.Single, + Title = "T", + System = "Dnd5e", + DurationMinutes = 240, + Visibility = WizardVisibility.Public, + Single = new WizardSingleInput + { + ScheduledAt = DateTimeOffset.UtcNow.AddDays(1), + MaxPlayers = 5, + }, + }; + + var cmd = CreateSessionHandler.BuildCommand( + draft, + payload, + new[] { payload.Single!.ScheduledAt!.Value }, + payload.Single.MaxPlayers, + isOneShot: true); + + Assert.Equal(5, cmd.MaxPlayers); + } +} diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs index b9c1ef5..6064da5 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs @@ -23,6 +23,7 @@ public sealed class GameCreationWizardStepTransitionsTests // Capacity → Visibility [InlineData(WizardStepNames.Capacity, "waitlist:on", WizardStepNames.Visibility)] [InlineData(WizardStepNames.Capacity, "waitlist:off", WizardStepNames.Visibility)] + [InlineData(WizardStepNames.Capacity, "no_limit", WizardStepNames.Visibility)] // Visibility → Publish (public, no club) [InlineData(WizardStepNames.Visibility, "public", WizardStepNames.Publish)] // Visibility → PickClub @@ -70,6 +71,32 @@ public sealed class GameCreationWizardStepTransitionsTests Assert.Equal(240, dur.GetInt32()); } + [Fact] + public async Task NoLimitCapacityButton_AdvancesToVisibility_AndLeavesMaxPlayersNull() + { + var wizard = BuildWizard(out var drafts, out _); + var draft = NewDraft(WizardStepNames.Capacity, PayloadForStep(WizardStepNames.Capacity)); + drafts.Seed(draft); + + var data = WizardCallbackData.Choice(WizardStepNames.Capacity, "no_limit"); + await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None); + + Assert.Equal(WizardStepNames.Visibility, draft.Step); + using var doc = JsonDocument.Parse(draft.PayloadJson); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("single", out var single)); + // WizardPayloadJsonContext имеет DefaultIgnoreCondition=WhenWritingNull, + // поэтому null-MaxPlayers просто не пишется. Оба варианта + // (отсутствует / JsonValueKind.Null) десериализуются обратно в null + // и уйдут в БД как NULL — то есть «без лимита». + if (single.TryGetProperty("maxPlayers", out var maxPlayers)) + { + Assert.True( + maxPlayers.ValueKind == JsonValueKind.Null, + $"expected maxPlayers to be null (no limit), got {maxPlayers.ValueKind}"); + } + } + [Fact] public async Task ChoiceCallback_FromMismatchedStep_AdvancesBasedOnCallbackStep() { diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardStepRenderTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardStepRenderTests.cs index 99d2993..9367b10 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardStepRenderTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardStepRenderTests.cs @@ -76,6 +76,7 @@ public sealed class WizardStepRenderTests var labels = ButtonLabels(kb); Assert.Contains(labels, l => l.Contains("Waitlist вкл", StringComparison.Ordinal)); Assert.Contains(labels, l => l.Contains("Без waitlist", StringComparison.Ordinal)); + Assert.Contains(labels, l => l.Contains("Без лимита", StringComparison.Ordinal)); } [Fact] From 5c0397a5e676959b2eb8aec0de3d32acbe98be78 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Mon, 8 Jun 2026 18:17:26 +0300 Subject: [PATCH 2/3] chore: bump version 3.9.2 -> 3.9.3 Sync the four canonical version sources for the patch release: - Directory.Build.props - compose.yaml (bot, discord, web image tags) - .gitea/workflows/deploy.yml (VERSION env) - src/GmRelay.Web/Components/Layout/NavMenu.razor (visible nav-version) --- .gitea/workflows/deploy.yml | 2 +- Directory.Build.props | 2 +- compose.yaml | 6 +++--- src/GmRelay.Web/Components/Layout/NavMenu.razor | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index d941558..60991b7 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 3.9.2 + VERSION: 3.9.3 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index dc23532..e5db704 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 3.9.2 + 3.9.3 net10.0 preview enable diff --git a/compose.yaml b/compose.yaml index 4934bf3..c8b359d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -49,7 +49,7 @@ services: crond -f bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.2 + image: git.codeanddice.ru/toutsu/gmrelay-bot:3.9.3 restart: always depends_on: db: @@ -67,7 +67,7 @@ services: retries: 3 discord: - image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.2 + image: git.codeanddice.ru/toutsu/gmrelay-discord-bot:3.9.3 restart: always depends_on: db: @@ -86,7 +86,7 @@ services: retries: 3 web: - image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.2 + image: git.codeanddice.ru/toutsu/gmrelay-web:3.9.3 restart: always depends_on: db: diff --git a/src/GmRelay.Web/Components/Layout/NavMenu.razor b/src/GmRelay.Web/Components/Layout/NavMenu.razor index 2af1975..2bcfee1 100644 --- a/src/GmRelay.Web/Components/Layout/NavMenu.razor +++ b/src/GmRelay.Web/Components/Layout/NavMenu.razor @@ -82,7 +82,7 @@ - + From 3c3ef8db5a7f4802968ea0de764f06dbbdc1130a Mon Sep 17 00:00:00 2001 From: Toutsu Date: Mon, 8 Jun 2026 18:31:17 +0300 Subject: [PATCH 3/3] test(web): update NavMenu_ShouldExposeCurrentProjectVersion to 3.9.3 The version-bump commit changed the visible version in NavMenu.razor from 3.9.2 to 3.9.3 but this test hard-coded the old literal. Update the assertion to match the new release tag. --- tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs b/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs index ed9195d..9018686 100644 --- a/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs +++ b/tests/GmRelay.Bot.Tests/Web/CampaignTemplatesNavigationTests.cs @@ -15,7 +15,7 @@ public sealed class CampaignTemplatesNavigationTests public async Task NavMenu_ShouldExposeCurrentProjectVersion() { var navMenu = await File.ReadAllTextAsync(FindRepositoryFile("src/GmRelay.Web/Components/Layout/NavMenu.razor")); - Assert.Contains("v3.9.2", navMenu, StringComparison.Ordinal); + Assert.Contains("v3.9.3", navMenu, StringComparison.Ordinal); } [Fact]