acdream/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatCommandRouterTests.cs
Erik d3cab1ab10 fix(chat): / and @ are equivalent command prefixes (retail parity)
Retail treats / and @ interchangeably for commands, but two client-side
layers broke the / spelling for server commands:

- ChatCommandRouter refused ANY unknown /verb with a local "Unknown
  command" guess — so /tele, /ci, /acehelp never reached ACE even
  though ACE supports them. The guard is gone; the server is now the
  single authority on what's a valid command (ACE replies "Unknown
  command: x" itself).
- ChatInputParser passed unknown /verbs through as literal speech.
  ACE's GameActionTalk only intercepts the @ form on the wire (the /
  acceptance in CommandManager is the server CONSOLE path), so the
  parser now rewrites unknown /xyz -> @xyz.

Both command pass-throughs (@ and rewritten /) now also force the Say
channel: GameActionTalk (0x0015) is the only wire action ACE parses
commands on — previously a command typed with a chat channel active
would broadcast as channel speech.

Phase J Tier 4 ("/-text must never broadcast as speech") still holds:
letter-verbed input goes out as an @command (never speech), and
command-shaped-but-verbless input ("/", "//shrug") is refused locally
by a narrow router guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:46:50 +02:00

92 lines
3.2 KiB
C#

using AcDream.Core.Chat;
using AcDream.UI.Abstractions;
using AcDream.UI.Abstractions.Panels.Chat;
using Xunit;
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
public class ChatCommandRouterTests
{
private sealed class CaptureBus : ICommandBus
{
public SendChatCmd? Last;
public void Publish<T>(T command) where T : notnull
{
if (command is SendChatCmd c) Last = c;
}
}
private static (ChatVM vm, ChatLog log, CaptureBus bus) Fixture()
{
var log = new ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
return (vm, log, new CaptureBus());
}
[Fact]
public void PlainText_PublishesOnDefaultChannel()
{
var (vm, _, bus) = Fixture();
var outcome = ChatCommandRouter.Submit("hello there", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.Sent, outcome);
Assert.NotNull(bus.Last);
Assert.Equal(ChatChannelKind.Say, bus.Last!.Channel);
Assert.Equal("hello there", bus.Last.Text);
}
[Fact]
public void DefaultChannel_IsHonored()
{
var (vm, _, bus) = Fixture();
ChatCommandRouter.Submit("hi", vm, bus, ChatChannelKind.Fellowship);
Assert.Equal(ChatChannelKind.Fellowship, bus.Last!.Channel);
}
[Fact]
public void ClearCommand_DrainsLog_DoesNotPublish()
{
var (vm, log, bus) = Fixture();
log.OnSystemMessage("x", chatType: 0);
var outcome = ChatCommandRouter.Submit("/clear", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
Assert.Null(bus.Last);
Assert.Empty(log.Snapshot());
}
[Fact]
public void UnknownSlashVerb_RoutesToServerInAtForm()
{
// Retail / ≡ @: unknown verbs are the server's to judge. The
// parser rewrites /-form to @-form (ACE's GameActionTalk only
// intercepts @ on the wire); ACE answers "Unknown command: x"
// for verbs it doesn't know — no local guess.
var (vm, log, bus) = Fixture();
var outcome = ChatCommandRouter.Submit("/notacommand", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.Sent, outcome);
Assert.NotNull(bus.Last);
Assert.Equal("@notacommand", bus.Last!.Text);
Assert.DoesNotContain(log.Snapshot(), e => e.Text.Contains("Unknown command"));
}
[Fact]
public void ServerCommandWithArgs_PublishesAtFormAsSay_EvenOnChannelDefault()
{
// Commands resolve before channel routing (retail behavior):
// even with Fellowship as the active input channel, a command
// goes out as Say/Talk — the only wire action ACE parses @ on.
var (vm, _, bus) = Fixture();
var outcome = ChatCommandRouter.Submit("/ci 629 5", vm, bus, ChatChannelKind.Fellowship);
Assert.Equal(SubmitOutcome.Sent, outcome);
Assert.Equal(ChatChannelKind.Say, bus.Last!.Channel);
Assert.Equal("@ci 629 5", bus.Last.Text);
}
[Fact]
public void EmptyInput_DoesNothing()
{
var (vm, _, bus) = Fixture();
var outcome = ChatCommandRouter.Submit(" ", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.Empty, outcome);
Assert.Null(bus.Last);
}
}