acdream/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/ChatPanelInputTests.cs
Erik 3501194083 fix(chat): /help client-side handler + System dedup + ThatIsNotAValidCommand template
Phase J follow-up after a 2026-04-25 trace where typing /help
produced two identical "Unknown command: help" lines (ACE fires the
text via both GameMessageSystemChat 0xF7E0 and a paired
CommunicationTransientString 0x02EB), and the server's WeenieError
0x0026 trailer rendered cryptically as "WeenieError 0x0026".

Three small changes:

1. WeenieErrorMessages: add 0x0026 ThatIsNotAValidCommand ->
   "That is not a valid command." Plus 0x0414 / 0x050F that Phase J
   already added are now covered by tests too.

2. ChatLog.OnSystemMessage dedup. Track last system text + arrival
   time; if a second identical text shows up within 1 second,
   suppress. ACE's two-path send (gag warnings, command errors,
   etc.) collapses to a single chat line. Long bursts of repeated
   text still skip the duplicates without resetting the timer.

3. Client-side /help and /clear in ChatPanel. Intercepted BEFORE
   the parser passes to the server bus:
   - /help, /?, /h (case-insensitive) -> render local cheat-sheet
     listing acdream's slash prefixes via ChatLog.OnSystemMessage.
     Avoids the round-trip to ACE that produced the duplicate
     "Unknown command: help" lines AND gives users discoverability.
   - /clear, /cls -> drains the chat log so the panel starts empty.

   New ChatVM.ShowSystemMessage() + ChatVM.Clear() expose the
   minimum surface the panel needs to dispatch client-only feedback
   without coupling the panel to ChatLog directly.

12 new tests:
- 3 WeenieErrorMessages template adds (0x0026 / 0x0414 / 0x050F).
- 4 ChatLog dedup cases (immediate dup, different text, triplet,
  bookended-by-different-text).
- 5 ChatPanel client-command cases (/help, 3 alias variants,
  /clear).

Solution total: 1033 green (243 Core.Net + 130 UI + 660 Core),
0 warnings.

Acceptance: type /help in chat -> local help banner appears, no
server round-trip, no "Unknown command: help" duplicates. Type
/clear -> chat tail empty. Welcome banner + WeenieError-templated
"You are not in an allegiance!" / "You do not belong to a
Fellowship." continue rendering once each.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:22:07 +02:00

208 lines
6.5 KiB
C#

using AcDream.Core.Chat;
using AcDream.UI.Abstractions.Panels.Chat;
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
/// <summary>
/// Phase I.4: when the user submits text via the chat input field, the
/// panel must publish a <see cref="SendChatCmd"/> to the command bus.
/// We exercise the full Render path with the <see cref="FakePanelRenderer"/>
/// pre-loading a "submitted" string and a recording bus capturing the
/// resulting command.
/// </summary>
public sealed class ChatPanelInputTests
{
private sealed class RecordingBus : ICommandBus
{
public List<object> Published { get; } = new();
public void Publish<T>(T command) where T : notnull => Published.Add(command);
}
[Fact]
public void Submit_HelpCommand_RendersLocalHelpAndDoesNotPublish()
{
// Phase J follow-up: client-side commands (/help, /?, /h) are
// intercepted before the parser. They render a local cheat-sheet
// via ChatLog.OnSystemMessage and do NOT round-trip the server
// — that's what prevented the "Unknown command: help" duplicate
// ACE was firing back.
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = "/help",
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
Assert.Empty(bus.Published);
var entry = Assert.Single(log.Snapshot());
Assert.Equal(ChatKind.System, entry.Kind);
Assert.Contains("acdream chat commands:", entry.Text);
Assert.Contains("/tell", entry.Text);
}
[Theory]
[InlineData("/?")]
[InlineData("/h")]
[InlineData("/HELP")]
public void Submit_HelpAliases_AlsoRenderLocalHelp(string raw)
{
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = raw,
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
Assert.Empty(bus.Published);
Assert.Single(log.Snapshot());
}
[Fact]
public void Submit_ClearCommand_DrainsLog_AndDoesNotPublish()
{
var log = new ChatLog();
log.OnSystemMessage("seed line", chatType: 0);
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = "/clear",
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
Assert.Empty(bus.Published);
Assert.Empty(log.Snapshot());
}
[Fact]
public void Submit_PlainText_PublishesSayCommand()
{
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = "hello world",
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
var cmd = Assert.Single(bus.Published);
var sendCmd = Assert.IsType<SendChatCmd>(cmd);
Assert.Equal(ChatChannelKind.Say, sendCmd.Channel);
Assert.Null(sendCmd.TargetName);
Assert.Equal("hello world", sendCmd.Text);
}
[Fact]
public void Submit_TellSlashCommand_PublishesTellCommand()
{
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = "/t Bestie ping",
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
var sendCmd = Assert.IsType<SendChatCmd>(Assert.Single(bus.Published));
Assert.Equal(ChatChannelKind.Tell, sendCmd.Channel);
Assert.Equal("Bestie", sendCmd.TargetName);
Assert.Equal("ping", sendCmd.Text);
}
[Fact]
public void Submit_ReplySlashCommand_UsesLastIncomingTellSender()
{
var log = new ChatLog();
var vm = new ChatVM(log);
log.OnTellReceived("Bestie", "ping", senderGuid: 0x5000_00AAu);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = "/r back at you",
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
var sendCmd = Assert.IsType<SendChatCmd>(Assert.Single(bus.Published));
Assert.Equal(ChatChannelKind.Tell, sendCmd.Channel);
Assert.Equal("Bestie", sendCmd.TargetName);
Assert.Equal("back at you", sendCmd.Text);
}
[Fact]
public void Submit_EmptyOrWhitespace_PublishesNothing()
{
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = " ",
InputTextSubmitNextBufferAfter = "",
};
panel.Render(new PanelContext(0.016f, bus), renderer);
Assert.Empty(bus.Published);
}
[Fact]
public void NoSubmit_PublishesNothing()
{
// Most frames: user is typing or idle; submitted == null.
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = null,
};
panel.Render(new PanelContext(0.016f, bus), renderer);
Assert.Empty(bus.Published);
}
[Fact]
public void Render_AlwaysCallsInputTextSubmit_ToShowTheField()
{
var log = new ChatLog();
var vm = new ChatVM(log);
var panel = new ChatPanel(vm);
var bus = new RecordingBus();
var renderer = new FakePanelRenderer
{
InputTextSubmitNextSubmitted = null,
};
panel.Render(new PanelContext(0.016f, bus), renderer);
Assert.Contains(renderer.Calls, c => c.Method == "InputTextSubmit");
}
}