332 lines
15 KiB
C#
332 lines
15 KiB
C#
using AcDream.UI.Abstractions;
|
|
using AcDream.UI.Abstractions.Panels.Chat;
|
|
|
|
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
|
|
|
|
public sealed class RetailClientCommandCatalogTests
|
|
{
|
|
[Theory]
|
|
[InlineData("/lifestone")]
|
|
[InlineData("@lifestone")]
|
|
[InlineData("/lif")]
|
|
[InlineData("@LIF")]
|
|
[InlineData("/ls")]
|
|
[InlineData("@LS")]
|
|
public void RetailAliases_ResolveCaseInsensitively(string input)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(ClientCommandId.LifestoneRecall, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
Assert.Empty(match.Arguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/marketplace", ClientCommandId.MarketplaceRecall)]
|
|
[InlineData("@MAR", ClientCommandId.MarketplaceRecall)]
|
|
[InlineData("/mp", ClientCommandId.MarketplaceRecall)]
|
|
[InlineData("/pkarena", ClientCommandId.PkArenaRecall)]
|
|
[InlineData("/pka", ClientCommandId.PkArenaRecall)]
|
|
[InlineData("/pklarena", ClientCommandId.PkLiteArenaRecall)]
|
|
[InlineData("/pla", ClientCommandId.PkLiteArenaRecall)]
|
|
[InlineData("/pklite", ClientCommandId.EnterPkLite)]
|
|
[InlineData("@pklite", ClientCommandId.EnterPkLite)]
|
|
[InlineData("/hor", ClientCommandId.HouseRecall)]
|
|
[InlineData("/hr", ClientCommandId.HouseRecall)]
|
|
[InlineData("/hom", ClientCommandId.MansionRecall)]
|
|
[InlineData("/hoa", ClientCommandId.MansionRecall)]
|
|
[InlineData("/age", ClientCommandId.QueryAge)]
|
|
[InlineData("/birth", ClientCommandId.QueryBirth)]
|
|
[InlineData("/framerate", ClientCommandId.ToggleFrameRate)]
|
|
[InlineData("/lockui", ClientCommandId.ToggleUiLock)]
|
|
[InlineData("/version", ClientCommandId.ShowVersion)]
|
|
[InlineData("/loc", ClientCommandId.ShowLocation)]
|
|
[InlineData("/corpse", ClientCommandId.ShowLastCorpseLocation)]
|
|
[InlineData("/cor", ClientCommandId.ShowLastCorpseLocation)]
|
|
public void AdditionalRetailAliases_Resolve(string input, ClientCommandId expected)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/clear all", ClientCommandId.ClearChat)]
|
|
[InlineData("/saveui hunt", ClientCommandId.SaveUi)]
|
|
[InlineData("/loadui hunt", ClientCommandId.LoadUi)]
|
|
[InlineData("/saveautoui", ClientCommandId.SaveAutoUi)]
|
|
[InlineData("/loadautoui", ClientCommandId.LoadAutoUi)]
|
|
[InlineData("/afk msg lunch", ClientCommandId.Away)]
|
|
[InlineData("/consent who", ClientCommandId.Consent)]
|
|
[InlineData("/e waves", ClientCommandId.Emote)]
|
|
[InlineData("/em waves", ClientCommandId.Emote)]
|
|
[InlineData("/emote waves", ClientCommandId.Emote)]
|
|
[InlineData("/me waves", ClientCommandId.Emote)]
|
|
[InlineData("/emotes", ClientCommandId.ListEmotes)]
|
|
[InlineData("/friends online", ClientCommandId.Friends)]
|
|
[InlineData("/friends_add Alice", ClientCommandId.FriendsAdd)]
|
|
[InlineData("/friends_remove Alice", ClientCommandId.FriendsRemove)]
|
|
[InlineData("/squelch -tell Alice", ClientCommandId.Squelch)]
|
|
[InlineData("/unsquelch Alice", ClientCommandId.Unsquelch)]
|
|
[InlineData("/filter -combat", ClientCommandId.Filter)]
|
|
[InlineData("/unfilter -combat", ClientCommandId.Unfilter)]
|
|
[InlineData("/messagetypes", ClientCommandId.ListMessageTypes)]
|
|
[InlineData("/fillcomps scarabs 500", ClientCommandId.FillComponents)]
|
|
[InlineData("/day", ClientCommandId.TogglePersistentDaylight)]
|
|
[InlineData("@day ignored", ClientCommandId.TogglePersistentDaylight)]
|
|
[InlineData("/render radius 12", ClientCommandId.RenderOption)]
|
|
[InlineData("@render usage", ClientCommandId.RenderOption)]
|
|
public void CommandFamilies_ResolveToTypedClientCommands(
|
|
string input, ClientCommandId expected)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/house recall", ClientCommandId.HouseRecall)]
|
|
[InlineData("@house mansion_recall", ClientCommandId.MansionRecall)]
|
|
[InlineData("/house alleg_recall", ClientCommandId.MansionRecall)]
|
|
public void HouseRecallSubcommands_Resolve(string input, ClientCommandId expected)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/house open", ClientCommandId.HouseOpenStatus, "open")]
|
|
[InlineData("/house close ignored", ClientCommandId.HouseOpenStatus, "close")]
|
|
[InlineData("/house guest add Bob", ClientCommandId.HouseGuests, "add Bob")]
|
|
[InlineData("/house storage add Lord Bob", ClientCommandId.HouseStorage, "add Lord Bob")]
|
|
[InlineData("/house remove Bob", ClientCommandId.HouseBoot, "Bob")]
|
|
[InlineData("/house boot -all", ClientCommandId.HouseBoot, "-all")]
|
|
[InlineData("/house boot_all ignored", ClientCommandId.HouseBootAll, "ignored")]
|
|
[InlineData("/house remove_all", ClientCommandId.HouseBootAll, "")]
|
|
[InlineData("/house available Cottage", ClientCommandId.HouseAvailableList, "Cottage")]
|
|
[InlineData("/house hooks off", ClientCommandId.HouseHooks, "off")]
|
|
public void HouseManagementSubcommands_ResolveLocally(
|
|
string input,
|
|
ClientCommandId expected,
|
|
string expectedArguments)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.True(match.HasValidArguments);
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.Equal(expectedArguments, match.Arguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/house")]
|
|
[InlineData("/house nope")]
|
|
public void HouseUnknownOrMissingSubcommand_ShowsRetailRefusalLocally(string input)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(ClientCommandId.HouseUnrecognizedSubcommand, match.Command);
|
|
Assert.False(match.HasValidArguments);
|
|
Assert.Equal(
|
|
"Please see @help House for more information on how to use this command.",
|
|
match.InvalidArgumentsText);
|
|
}
|
|
|
|
[Fact]
|
|
public void HouseAvailableWithoutType_UsesHslistArgumentValidation()
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch("/house available", out var match));
|
|
Assert.Equal(ClientCommandId.HouseAvailableList, match.Command);
|
|
Assert.False(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/house abandon", ClientCommandId.HouseAbandon)]
|
|
[InlineData("/house re", ClientCommandId.HouseRecall)]
|
|
[InlineData("/house ma", ClientCommandId.MansionRecall)]
|
|
[InlineData("/hou recall", ClientCommandId.HouseRecall)]
|
|
public void HouseAliasesAndShortcuts_Resolve(string input, ClientCommandId expected)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/allegiance boot Bob", ClientCommandId.AllegianceBoot, "Bob")]
|
|
[InlineData("/allegiance ban add Bob", ClientCommandId.AllegianceBan, "add Bob")]
|
|
[InlineData("/all chat gag Bob", ClientCommandId.AllegianceChat, "gag Bob")]
|
|
[InlineData("/all ch off", ClientCommandId.AllegianceChat, "off")]
|
|
[InlineData("/allegiance broadcast hello", ClientCommandId.AllegianceBroadcast, "hello")]
|
|
[InlineData("/allegiance br hello", ClientCommandId.AllegianceBroadcast, "hello")]
|
|
[InlineData("/all officer add 2 Bob", ClientCommandId.AllegianceOfficer, "add 2 Bob")]
|
|
[InlineData("/all title set 2 Regent", ClientCommandId.AllegianceOfficerTitle, "set 2 Regent")]
|
|
[InlineData("/allegiance motd", ClientCommandId.AllegianceMotd, "")]
|
|
[InlineData("/allegiance name set A Name", ClientCommandId.AllegianceName, "set A Name")]
|
|
[InlineData("/allegiance lock bypass Bob", ClientCommandId.AllegianceLock, "bypass Bob")]
|
|
[InlineData("/allegiance house guest open", ClientCommandId.AllegianceHouse, "guest open")]
|
|
[InlineData("/motd set Welcome", ClientCommandId.AllegianceMotd, "set Welcome")]
|
|
public void AllegianceManagementSubcommands_ResolveLocally(
|
|
string input,
|
|
ClientCommandId expected,
|
|
string expectedArguments)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.True(match.HasValidArguments);
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.Equal(expectedArguments, match.Arguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/allegiance")]
|
|
[InlineData("/allegiance nope")]
|
|
[InlineData("/all nonsense")]
|
|
public void UnknownAllegianceSubcommand_ShowsRetailRefusal_ClientSide(string input)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(ClientCommandId.AllegianceUnrecognizedSubcommand, match.Command);
|
|
Assert.False(match.HasValidArguments);
|
|
Assert.Equal(
|
|
"Please see @help Allegiance for more information on how to use this command.",
|
|
match.InvalidArgumentsText);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/allegiance hometown", ClientCommandId.AllegianceHometown, "")]
|
|
[InlineData("/allegiance ho", ClientCommandId.AllegianceHometown, "")]
|
|
[InlineData("/alh", ClientCommandId.AllegianceHometown, "")]
|
|
[InlineData("/ah", ClientCommandId.AllegianceHometown, "")]
|
|
[InlineData("/allegiance info", ClientCommandId.AllegianceInfo, "")]
|
|
[InlineData("/allegiance info Bob", ClientCommandId.AllegianceInfo, "Bob")]
|
|
[InlineData("/all info Bob", ClientCommandId.AllegianceInfo, "Bob")]
|
|
public void AllegianceSimpleSubcommands_Resolve(
|
|
string input, ClientCommandId expected, string expectedArguments)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
Assert.Equal(expectedArguments, match.Arguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/pkl", ClientCommandId.EnterPkLite)]
|
|
[InlineData("/message_types", ClientCommandId.ListMessageTypes)]
|
|
[InlineData("/msgtypes", ClientCommandId.ListMessageTypes)]
|
|
[InlineData("/msg_types", ClientCommandId.ListMessageTypes)]
|
|
[InlineData("/endurance", ClientCommandId.Endurance)]
|
|
[InlineData("/speaker", ClientCommandId.Speaker)]
|
|
[InlineData("/index", ClientCommandId.IndexChannels)]
|
|
// CH4 REJECT-review nit 14 (2026-08-09): DoChannelIndex ignores argc —
|
|
// "@index foo" sends the same request as bare "@index".
|
|
[InlineData("/index foo", ClientCommandId.IndexChannels)]
|
|
public void MissingAliasesSweep_Resolve(string input, ClientCommandId expected)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expected, match.Command);
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/chat on")]
|
|
[InlineData("/chat off")]
|
|
[InlineData("/notell on")]
|
|
[InlineData("/notell off")]
|
|
public void ChatNoTellToggle_ValidArguments_Resolve(string input)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void ChatToggle_InvalidArgument_IsRejected()
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch("/chat maybe", out var match));
|
|
Assert.False(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/join allegiance")]
|
|
[InlineData("/join general")]
|
|
[InlineData("/leave society")]
|
|
[InlineData("/leave soc")]
|
|
public void JoinLeave_ValidTags_Resolve(string input)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.True(match.HasValidArguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void JoinLeave_InvalidTag_IsRejected()
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch("/join nonsense", out var match));
|
|
Assert.False(match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/permit add Bob", true)]
|
|
[InlineData("/permit remove Bob", true)]
|
|
// CH4 REJECT-review SHOULD-FIX 5 (2026-08-09): retail's DoPermit joins
|
|
// every token after the mode word into the name (JoinArgsAsName), so a
|
|
// multi-word character name is a VALID shape, not a rejected one.
|
|
[InlineData("/permit add Aunt Agatha", true)]
|
|
[InlineData("/permit remove Lord Gnarly Beard", true)]
|
|
[InlineData("/permit add", false)]
|
|
[InlineData("/permit maybe Bob", false)]
|
|
public void Permit_ArgumentShape(string input, bool expectedValid)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expectedValid, match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/hslist Cottage", true)]
|
|
[InlineData("/hslist mansion", true)]
|
|
[InlineData("/hslist nonsense", false)]
|
|
public void HouseAvailableList_ArgumentShape(string input, bool expectedValid)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expectedValid, match.HasValidArguments);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/clist fellowship", true)]
|
|
[InlineData("/on admin", true)]
|
|
// CH4 REJECT-review SHOULD-FIX 6 (2026-08-09): the catalog only
|
|
// validates argument SHAPE (retail's argc != 1 check) — a resolved-but-
|
|
// UNKNOWN single-token tag is now a VALID shape that reaches
|
|
// ClientCommandController, which raises WeenieError 0x422 ("That
|
|
// channel doesn't exist.") instead of the catalog silently rejecting
|
|
// it with the wrong "Please specify the channel name." usage line.
|
|
[InlineData("/off nonsense", true)]
|
|
[InlineData("/clist", false)]
|
|
[InlineData("/on fellowship extra", false)]
|
|
public void ChannelArgumentCommands_RequireExactlyOneToken(string input, bool expectedValid)
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch(input, out var match));
|
|
Assert.Equal(expectedValid, match.HasValidArguments);
|
|
}
|
|
|
|
[Fact]
|
|
public void MrPr_AreNeverExecutable()
|
|
{
|
|
// Retail registers @mr/@pr with a NULL function pointer — they
|
|
// must never resolve as client-owned commands (Tier B.9).
|
|
Assert.False(RetailClientCommandCatalog.TryMatch("/mr", out _));
|
|
Assert.False(RetailClientCommandCatalog.TryMatch("/pr", out _));
|
|
Assert.False(RetailClientCommandCatalog.TryMatch("/mr hello", out _));
|
|
Assert.False(RetailClientCommandCatalog.TryMatch("/pr hello", out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void LifestoneArgument_IsRecognizedButInvalid()
|
|
{
|
|
Assert.True(RetailClientCommandCatalog.TryMatch("/ls now", out var match));
|
|
Assert.Equal("now", match.Arguments);
|
|
Assert.False(match.HasValidArguments);
|
|
Assert.Equal("/lifestone", match.Usage);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("/ci 629")]
|
|
[InlineData("@acehelp")]
|
|
[InlineData("ordinary speech")]
|
|
public void NonClientCommands_DoNotMatch(string input)
|
|
=> Assert.False(RetailClientCommandCatalog.TryMatch(input, out _));
|
|
}
|