merge: plugin text and unknown-command refusals go to the chat window (owner-directed, AD-124)
Owner 2026-09-07: plugin output and 'Unknown command' land in the chat scroll, not the SpewBox. Plugin text = Decal/VTank-faithful; the unknown-command re-route is recorded as AD-124 (retail types it 0x1A). Bad-argument refusals of real retail commands stay SpewBox-only (AP-183). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
66160741f0
11 changed files with 311 additions and 65 deletions
|
|
@ -146,6 +146,31 @@ public sealed class AppAutomationSurfaceTests
|
|||
Assert.Equal(0, second.CommunicationOwner.SubscriberCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owner-directed override 2026-09-07 (register row AD-124): plugin
|
||||
/// output ("Unknown commands like /vt or stuff from plugins ... should
|
||||
/// go to the chatbox") must land in the chat log, never the transient
|
||||
/// SpewBox overlay retail's own ClientLocal (0x1A) typing used to send
|
||||
/// it to — the same VTank-faithful destination Decal's own
|
||||
/// <c>AddChatText</c> uses.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PostSystemMessage_RoutesToChatLog_NeverSpewBox()
|
||||
{
|
||||
using var runtime = GameRuntimeTestFactory.Create();
|
||||
using var surface = new AppAutomationSurface();
|
||||
surface.Bind(runtime, runtime.CharacterOwner, runtime.ActionOwner.SpellCast);
|
||||
|
||||
surface.PostSystemMessage("MossTank: buffs applied.");
|
||||
|
||||
var entry = Assert.Single(runtime.CommunicationOwner.Chat.Snapshot());
|
||||
Assert.Equal("MossTank: buffs applied.", entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
|
||||
runtime.CommunicationOwner.SpewBox.Tick(0d);
|
||||
Assert.Equal(0, runtime.CommunicationOwner.SpewBox.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryCompletionProjectsTheCanonicalRequestReceipt()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,111 @@
|
|||
using AcDream.Core.Chat;
|
||||
using AcDream.Runtime.Chat;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Owner direction 2026-09-07 (verbatim): "Unknown commands like /vt or
|
||||
/// stuff from plugins shall now go to the SpewBox. They should go to the
|
||||
/// chatbox." Register row AD-124 records the deviation from retail's own
|
||||
/// ClientLocal (0x1A) typing for exactly these two families. This file pins
|
||||
/// the <see cref="ChatCommandRouter"/> half of that change at the Runtime
|
||||
/// layer — <see cref="RuntimeChatCommandFeedback"/> bound to a real
|
||||
/// <see cref="RuntimeCommunicationState"/> — since the existing router
|
||||
/// coverage in <c>AcDream.UI.Abstractions.Tests</c> only exercises the
|
||||
/// <c>ChatVM</c> feedback implementation. The plugin-text half is pinned at
|
||||
/// the App layer (<c>AppAutomationSurfaceTests.PostSystemMessage_RoutesToChatLog_NeverSpewBox</c>),
|
||||
/// since <c>AppAutomationSurface</c> is the App-layer production
|
||||
/// implementation of <c>IPluginChat</c>.
|
||||
/// </summary>
|
||||
public sealed class ChatCommandRouterFeedbackRoutingTests
|
||||
{
|
||||
[Fact]
|
||||
public void DegeneratePrefix_UnknownCommandRefusal_RoutesToChatLog_NeverSpewBox()
|
||||
{
|
||||
// "/" alone (no letter verb) is the degenerate-prefix guard's
|
||||
// "Unknown command: {verb}." refusal — retail itself types this
|
||||
// 0x1A (ClientLocal / SpewBox-only); the owner override moves it to
|
||||
// the chat scroll (Default/0x00) instead.
|
||||
using var communication = new RuntimeCommunicationState();
|
||||
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||
|
||||
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||
"/", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||
|
||||
Assert.Equal(SubmitOutcome.UnknownCommand, outcome);
|
||||
ChatEntry entry = Assert.Single(communication.Chat.Snapshot());
|
||||
Assert.Contains("Unknown command:", entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
|
||||
communication.SpewBox.Tick(0d);
|
||||
Assert.Equal(0, communication.SpewBox.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HelpUnresolvedVerb_UnknownCommandText_RoutesToChatLog_NeverSpewBox()
|
||||
{
|
||||
// "/help nonsenseverb" hits EmitVerbHelp's final unresolved-verb
|
||||
// fallback (RetailCommandHelpTable.UnknownCommand), the exact
|
||||
// existing retail-swept text — only the destination changes.
|
||||
using var communication = new RuntimeCommunicationState();
|
||||
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||
|
||||
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||
"/help nonsenseverb", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||
|
||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||
ChatEntry entry = Assert.Single(communication.Chat.Snapshot());
|
||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
|
||||
communication.SpewBox.Tick(0d);
|
||||
Assert.Equal(0, communication.SpewBox.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HelpConfirmedNullVerb_UnknownCommandText_RoutesToChatLog_NeverSpewBox()
|
||||
{
|
||||
// "index" is one of the four catalog verbs retail registers with a
|
||||
// genuinely NULL help pointer (RetailCommandHelpTable.
|
||||
// CatalogVerbsWithNoRetailHelp) — EmitVerbHelp's OTHER "Unknown
|
||||
// command" call site, distinct from the unresolved-verb fallback
|
||||
// above.
|
||||
using var communication = new RuntimeCommunicationState();
|
||||
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||
|
||||
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||
"/help index", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||
|
||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||
ChatEntry entry = Assert.Single(communication.Chat.Snapshot());
|
||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
|
||||
communication.SpewBox.Tick(0d);
|
||||
Assert.Equal(0, communication.SpewBox.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RealCommandBadArguments_StillRoutesToSpewBox_NeverChatLog()
|
||||
{
|
||||
// Boundary pin: AP-183's bad-argument refusals of REAL retail
|
||||
// commands are UNCHANGED by the owner's 2026-09-07 direction, which
|
||||
// named only unknown commands and plugin text. "/ls now" (Lifestone
|
||||
// with bad args) must still land in the SpewBox exclusively.
|
||||
using var communication = new RuntimeCommunicationState();
|
||||
var feedback = new RuntimeChatCommandFeedback(communication);
|
||||
|
||||
SubmitOutcome outcome = ChatCommandRouter.Submit(
|
||||
"/ls now", feedback, NullCommandBus.Instance, ChatChannelKind.Say);
|
||||
|
||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||
Assert.Empty(communication.Chat.Snapshot());
|
||||
|
||||
communication.SpewBox.Tick(0d);
|
||||
Assert.Equal(1, communication.SpewBox.Count);
|
||||
Assert.Equal(
|
||||
"Please see @help lifestone for more information on how to use this command.",
|
||||
communication.SpewBox.Snapshot()[0].Text);
|
||||
}
|
||||
}
|
||||
|
|
@ -194,4 +194,36 @@ public sealed class ChatVMTests
|
|||
// The stored body never carries the stamp in either state.
|
||||
Assert.Equal("hi", log.Snapshot()[0].Text);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Owner-directed override 2026-09-07 (register row AD-124): plugin
|
||||
/// output (<c>AppAutomationSurface.PostSystemMessage</c>, the
|
||||
/// production implementation of <c>IPluginChat.PostSystemMessage</c>)
|
||||
/// now funnels into <c>RuntimeCommunicationState.AddText(text,
|
||||
/// RetailLogTextType.Default)</c>, which calls
|
||||
/// <c>Chat.OnSystemMessage(text, (uint)Default)</c> — the exact call
|
||||
/// this test performs directly on the shared <see cref="ChatLog"/>,
|
||||
/// matching Decal's own <c>AddChatText</c> behavior for plugin text.
|
||||
/// Any <see cref="ChatVM"/> bound to that log (the production chat
|
||||
/// window) must show the line; it must never depend on the
|
||||
/// <see cref="ChatVM.OnInterfaceText"/> SpewBox seam, which this call
|
||||
/// never touches.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void RecentLines_ShowsPluginSystemMessage_TaggedDefault()
|
||||
{
|
||||
var log = new ChatLog();
|
||||
var vm = new ChatVM(log, displayLimit: 50);
|
||||
|
||||
log.OnSystemMessage(
|
||||
"MossTank: buffs applied.",
|
||||
chatType: (uint)RetailLogTextType.Default);
|
||||
|
||||
Assert.Equal(
|
||||
"MossTank: buffs applied.",
|
||||
Assert.Single(vm.RecentLines()));
|
||||
Assert.Equal(
|
||||
(uint)RetailLogTextType.Default,
|
||||
Assert.Single(log.Snapshot()).LogTextType);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,28 +406,35 @@ public class ChatCommandRouterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void HelpVerb_UnknownVerb_ShowsRetailUnknownCommandText_ViaInterfaceTextSeam()
|
||||
public void HelpVerb_UnknownVerb_ShowsRetailUnknownCommandText_InChatLog_TaggedDefault()
|
||||
{
|
||||
// Campaign CH user-gate round 3 (2026-08-10): retail's own DoHelp
|
||||
// fallback text is "Unknown command" (swept verbatim), not an
|
||||
// acdream-invented "No help available" message. Retail types this
|
||||
// 0x1A (ClientLocal / SpewBox-only). Issue #363/#367: now routed
|
||||
// through the interface-text seam as ONE entry (no HelpPrefixNote
|
||||
// wrapper — DoHelp's fallback bypasses the two-entry shape
|
||||
// entirely), not the chat scroll.
|
||||
// acdream-invented "No help available" message. Retail itself types
|
||||
// this 0x1A (ClientLocal / SpewBox-only). Owner-directed override
|
||||
// 2026-09-07 (register row AD-124): "Unknown command" refusals now
|
||||
// route to the CHAT SCROLL (ShowSystemMessage, Default/0x00) instead
|
||||
// of the interface-text/SpewBox seam — the seam stays empty.
|
||||
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||
|
||||
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
||||
|
||||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||
Assert.Empty(bus.Published);
|
||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, Assert.Single(interfaceTexts));
|
||||
Assert.Empty(log.Snapshot());
|
||||
Assert.Empty(interfaceTexts);
|
||||
var entry = Assert.Single(log.Snapshot());
|
||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HelpVerb_UnknownVerb_NoInterfaceSinkWired_FallsBackToChatLog_TaggedClientLocal()
|
||||
public void HelpVerb_UnknownVerb_NoInterfaceSinkWired_StillRoutesToChatLog_TaggedDefault()
|
||||
{
|
||||
// Owner-directed override 2026-09-07 (register row AD-124):
|
||||
// ShowSystemMessage never depended on OnInterfaceText wiring in the
|
||||
// first place, so headless / no-window hosts see the identical
|
||||
// chat-log entry whether or not a sink is wired — unlike the old
|
||||
// ShowInterfaceText null-fallback this test used to pin.
|
||||
var (vm, log, bus) = Fixture();
|
||||
|
||||
var outcome = ChatCommandRouter.Submit("/help nonsenseverb", vm, bus, ChatChannelKind.Say);
|
||||
|
|
@ -435,7 +442,7 @@ public class ChatCommandRouterTests
|
|||
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
|
||||
var entry = Assert.Single(log.Snapshot());
|
||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -588,19 +595,23 @@ public class ChatCommandRouterTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void DegeneratePrefix_UnknownCommand_ShowsRefusal_ViaInterfaceTextSeam()
|
||||
public void DegeneratePrefix_UnknownCommand_ShowsRefusal_InChatLog_TaggedDefault()
|
||||
{
|
||||
// "/" alone (no letter verb) — the pre-existing "Unknown command:
|
||||
// {verb}." refusal, now also routed through the interface-text
|
||||
// seam (issue #367).
|
||||
// {verb}." refusal. Owner-directed override 2026-09-07 (register
|
||||
// row AD-124): routed to the chat scroll (ShowSystemMessage,
|
||||
// Default/0x00), NOT the interface-text/SpewBox seam issue #367
|
||||
// originally moved it to.
|
||||
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
|
||||
|
||||
var outcome = ChatCommandRouter.Submit("/", vm, bus, ChatChannelKind.Say);
|
||||
|
||||
Assert.Equal(SubmitOutcome.UnknownCommand, outcome);
|
||||
Assert.Empty(bus.Published);
|
||||
Assert.Contains("Unknown command:", Assert.Single(interfaceTexts));
|
||||
Assert.Empty(log.Snapshot());
|
||||
Assert.Empty(interfaceTexts);
|
||||
var entry = Assert.Single(log.Snapshot());
|
||||
Assert.Contains("Unknown command:", entry.Text);
|
||||
Assert.Equal((uint)RetailLogTextType.Default, entry.LogTextType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -395,10 +395,13 @@ public sealed class RetailCommandHelpTableTests
|
|||
// assignment, unlike every extracted verb above -- confirming a
|
||||
// genuinely NULL help function pointer. Retail's own DoHelp skips
|
||||
// its help-callback branch entirely for these and falls to the
|
||||
// SAME "Unknown command" 0x1A text an unregistered verb gets, even
|
||||
// though the verb dispatches fine for ordinary (non-help) use.
|
||||
// Showing the catalog's own invented summary here would be
|
||||
// retail-inaccurate.
|
||||
// SAME "Unknown command" text an unregistered verb gets (retail
|
||||
// itself types it 0x1A), even though the verb dispatches fine for
|
||||
// ordinary (non-help) use. Showing the catalog's own invented
|
||||
// summary here would be retail-inaccurate. Owner-directed override
|
||||
// 2026-09-07 (register row AD-124): acdream routes this "Unknown
|
||||
// command" text to the CHAT SCROLL (Default/0x00) rather than
|
||||
// retail's own SpewBox-only 0x1A typing.
|
||||
var log = new AcDream.Core.Chat.ChatLog();
|
||||
var vm = new ChatVM(log, displayLimit: 50);
|
||||
var bus = new RecordingCommandBus();
|
||||
|
|
@ -411,7 +414,7 @@ public sealed class RetailCommandHelpTableTests
|
|||
Assert.Single(entries);
|
||||
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
|
||||
Assert.Equal(
|
||||
(uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal,
|
||||
(uint)AcDream.Core.Chat.RetailLogTextType.Default,
|
||||
entries[0].LogTextType);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue