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]