fix(chat): #363 — retail 0x1A typing for command refusals via the interface-text seam

ChatVM gains an OnInterfaceText hook + ShowInterfaceText(text), the
App-layer composition wires it to RuntimeCommunicationState.AddText,
and ChatCommandRouter routes every retail-0x1A command refusal through
it instead of the chat log's 0x00 sink. UI.Abstractions still never
references Runtime directly; unwired hosts (headless, tests) fall back
to the chat log tagged ClientLocal so no text is ever silently lost.

Reclassified per register row AP-183 (DoChannelList/On/Off, DoAllegiance,
DoHouseAvailableList — the last also corrected to retail's own bad-house-
type string instead of a synthesized "Usage:" line) and newly wired two
sites that previously showed nothing at all (DoStupidChannelHack's bare
legacy-channel-verb refusal, DoReply's message-but-no-last-teller
refusal). The generic bad-args fallback now resolves WeenieErrorMessages
0x026 ("That is not a valid command.", retail's HandleFailureEvent(0x26))
instead of synthesizing "Usage: {Usage}". DoSpeaker/DoEndurance/DoTitle
are untouched — already correct at 0x00.

Also closes #367 (DoHelp's "Unknown command" fallback and the degenerate-
prefix refusal now reach the SpewBox too) and retires register row
AP-186, whose own filing proposed exactly this seam shape.

Full Release suite: 12,542 passed / 4 skipped / 0 failed (baseline
12,466/4/0 at ff2784ea).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 15:20:57 +02:00
parent ff2784eaa4
commit 09453ecae8
14 changed files with 702 additions and 106 deletions

View file

@ -423,4 +423,95 @@ public sealed class ChatInputParserTests
Assert.Null(parsed.Value.TargetName);
Assert.Equal(text, parsed.Value.Text);
}
// ── Issue #363: IsBareRegisteredChannelVerb / IsReplyMissingLastTeller ──
[Theory]
[InlineData("/g")]
[InlineData("/f")]
[InlineData("/fellow")]
[InlineData("/fellows")]
[InlineData("/fellowship")]
[InlineData("/group")]
[InlineData("/party")]
[InlineData("/a")]
[InlineData("/guild")]
[InlineData("/gu")]
[InlineData("/ab")]
[InlineData("/m")]
[InlineData("/monarch")]
[InlineData("/p")]
[InlineData("/patron")]
[InlineData("/v")]
[InlineData("/vassal")]
[InlineData("/vassals")]
[InlineData("/c")]
[InlineData("/covassal")]
[InlineData("/covassals")]
[InlineData("/co-vassals")]
public void IsBareRegisteredChannelVerb_TrueForEveryLegacyChannelAlias(string verb)
{
Assert.True(ChatInputParser.IsBareRegisteredChannelVerb(verb));
}
[Theory]
[InlineData("/general")]
[InlineData("/cg")]
[InlineData("/lfg")]
[InlineData("/clfg")]
[InlineData("/trade")]
[InlineData("/ct")]
[InlineData("/roleplay")]
[InlineData("/crp")]
[InlineData("/society")]
[InlineData("/soc")]
[InlineData("/olthoi")]
[InlineData("/o")]
public void IsBareRegisteredChannelVerb_FalseForTurbineOnlyChannels(string verb)
{
// The seven Turbine-only channels are never DoStupidChannelHack's
// clients (command-registry doc §2.3).
Assert.False(ChatInputParser.IsBareRegisteredChannelVerb(verb));
}
[Theory]
[InlineData("/g hi gang")]
[InlineData("/a hey")]
[InlineData("hello")]
[InlineData("/say hi")]
[InlineData("/r hi")]
public void IsBareRegisteredChannelVerb_FalseWithMessageOrNotAChannelVerb(string input)
{
Assert.False(ChatInputParser.IsBareRegisteredChannelVerb(input));
}
[Theory]
[InlineData("/r hi there")]
[InlineData("/reply hi there")]
[InlineData("/rp hi there")]
public void IsReplyMissingLastTeller_TrueWithMessageAndNoLastTeller(string input)
{
Assert.True(ChatInputParser.IsReplyMissingLastTeller(input, lastTellSender: null));
Assert.True(ChatInputParser.IsReplyMissingLastTeller(input, lastTellSender: ""));
}
[Fact]
public void IsReplyMissingLastTeller_FalseWhenLastTellerPresent()
{
Assert.False(ChatInputParser.IsReplyMissingLastTeller("/r hi there", lastTellSender: "Bestie"));
}
[Theory]
[InlineData("/r")]
[InlineData("/r ")]
[InlineData("/g hi gang")]
[InlineData("hello")]
public void IsReplyMissingLastTeller_FalseWithoutAMessageOrNotAReplyVerb(string input)
{
// Bare "/r" (no message at all) is a DIFFERENT retail branch
// (DoReply's own copy of the "you must specify text" string) and
// is deliberately out of scope for this predicate — see its doc
// comment.
Assert.False(ChatInputParser.IsReplyMissingLastTeller(input, lastTellSender: null));
}
}