acdream/tests/AcDream.UI.Abstractions.Tests/Panels/Chat/RetailCommandRegistryConformanceTests.cs
Erik 090825e703 feat(chat): Campaign CH slice CH4 — command registry completion
Brings acdream's / and @ command parsing to parity with the complete
retail registry (130 registered verbs + 22 unregistered GetChannelID
fallback tags = 152 client-parsed verbs), per
docs/research/2026-08-09-chat-retail-command-registry.md.

Parser semantics (retail OnChatCommand/DoCommand):
- : and ; rewrite to "@emote <rest>" before dispatch.
- Verb trailing-comma trim ("@f, hi" == "@f hi") applied at every
  verb-lookup site in the catalog and the parser.
- @tell/aliases split the target on the FIRST COMMA, not the first
  whitespace token, so multi-word names work ("@tell Aunt Agatha, hi").
- The 22 unregistered GM/faction channel tags (admin, sentinel,
  celestialhand, ...) now broadcast for real via a new
  RetailChannelTagTable + SendRawChannelCmd bypass, reusing the existing
  BuildChatChannel wire builder.

Binding corrections:
- /g, /group, /party -> Fellowship (0x800), not General.
- /rp -> reply alias (retail's own help text confirms "@r or @rp"), not
  Roleplay; /role (an acdream invention) deleted.
- /allegiance, /all -> the allegiance management command
  (RetailClientCommandCatalog), not a channel verb.
- /house no longer swallows unrecognized subcommands with a local usage
  error; they now correctly fall through to ACE.
- @mr/@pr pinned as permanently non-executable (retail registers them
  with a null function pointer).

New verbs with real local execution: endurance, speaker, title (silent,
AP-182), chat, notell, join, leave, permit, hslist, index, clist, on,
off, alh/ah (+ "@allegiance hometown"/"ho"), "@allegiance info",
"@house abandon"; a missing-alias sweep across pkl/hou/message_types/
msgtypes/msg_types/rt/send/whisper/w/vassal/covassal/co-vassals/c/
fellows/group/party/guild/gu/cg/ct/clfg/crp/soc/o; the non-retail
inventions gen/cv/lookingforgroup/tr/role/h are deleted. New Core.Net
wire builders (IndexChannels, ListChannels, AddChannel, RemoveChannel,
RecallAllegianceHometown, AllegianceInfoRequest, ListAvailableHouses,
AddPlayerPermission, RemovePlayerPermission, AbandonHouse) are all
parameterless or single-field payloads cross-checked against ACE's
GameAction readers, not guessed.

Deferred (filed as #360/#361/#362, register rows TS-68/TS-69/TS-70):
the ~22 remaining allegiance/house subcommands + standalone @motd
(largest single item, needs its own slice per the doc), the three
still-inert pure-local commands (day/log/render), and the inbound
GameEvent responses for the new outbound requests. All correctly fall
through to ACE server-passthrough rather than being silently swallowed
or faking success.

RetailCommandRegistryConformanceTests pins the complete 152-verb
registry against production: every verb resolves through exactly one
production surface if Implemented, through none if HelpOnly/
ServerPassthrough, and two reverse-direction tests fail the build if
RetailClientCommandCatalog or ChatInputParser ever claims a verb
outside this registry again. Final tally: 138 Implemented / 5
ServerPassthrough / 9 HelpOnly = 152.

Release suite: 12,190 passed / 4 skipped / 0 failed (up from CH3's
11,964/4/0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 21:10:17 +02:00

269 lines
12 KiB
C#

using AcDream.UI.Abstractions.Panels.Chat;
namespace AcDream.UI.Abstractions.Tests.Panels.Chat;
/// <summary>
/// Campaign CH slice CH4 (2026-08-09): pins acdream's complete chat-command
/// surface against the retail registry enumeration in
/// <c>docs/research/2026-08-09-chat-retail-command-registry.md</c> §2/§4 —
/// 130 verbs registered by <c>InitializeCommands</c> +
/// <c>StartupTurbineChatSystem</c>, plus the 22 unregistered
/// <c>ChannelSystem::GetChannelID</c> fallback tags (152 total).
///
/// <para>
/// Every verb has an explicit <see cref="Status"/>:
/// <list type="bullet">
/// <item><see cref="Status.Implemented"/> — executes locally (typed client
/// command, chat alias, channel send, or local presentation).</item>
/// <item><see cref="Status.HelpOnly"/> — retail registers it with a NULL
/// function pointer (verified byte-level in the registry doc); it can
/// never execute in retail OR acdream. Typing it bare reaches the server
/// as literal text.</item>
/// <item><see cref="Status.ServerPassthrough"/> — a real retail verb
/// acdream has not yet ported (TS-68/TS-69/TS-70, or the deliberate
/// @loadfile product decision); falls through to ACE as literal text.</item>
/// </list>
/// The FIRST test enforces that every verb resolves to the RIGHT status.
/// The SECOND enforces the ownership rule in reverse: nothing in
/// <see cref="RetailClientCommandCatalog"/> or <see cref="ChatInputParser"/>
/// exists that ISN'T in this registry (no invented verbs slip back in).
/// </para>
/// </summary>
public sealed class RetailCommandRegistryConformanceTests
{
private enum Status { Implemented, HelpOnly, ServerPassthrough }
private sealed record Entry(Status Status, params string[] Verbs);
/// <summary>
/// The complete 152-verb registry, one entry per retail handler (or
/// per fallback-tag family), transcribed directly from the
/// command-registry doc's §2 tables. Verb-string counts per section
/// were cross-checked against the doc's own §4 totals (130 + 22 = 152;
/// the Turbine §2.4 net-new count here is exactly 14, matching the
/// doc's "15 added, 14 net-new (a replaced)" note) before this test
/// was written.
/// </summary>
private static readonly Entry[] Registry =
[
// §2.1 Help / group nodes (9)
new(Status.Implemented, "help", "?"),
new(Status.HelpOnly, "commands"),
new(Status.HelpOnly, "allegiances"),
new(Status.HelpOnly, "channels"),
new(Status.HelpOnly, "chatting"),
new(Status.HelpOnly, "death"),
new(Status.HelpOnly, "status"),
new(Status.HelpOnly, "text"),
// §2.2 Local chat routing (31)
new(Status.Implemented, "say", "s"),
new(Status.Implemented, "tell", "t", "send", "whisper", "w"),
new(Status.Implemented, "reply", "r", "rp"),
new(Status.HelpOnly, "mr"),
new(Status.HelpOnly, "pr"),
new(Status.Implemented, "retell", "rt"),
new(Status.Implemented, "chat"),
new(Status.Implemented, "notell"),
new(Status.Implemented, "join"),
new(Status.Implemented, "leave"),
new(Status.Implemented, "index"),
new(Status.Implemented, "clist"),
new(Status.Implemented, "on"),
new(Status.Implemented, "off"),
new(Status.Implemented, "title"),
new(Status.ServerPassthrough, "log"), // TS-69
new(Status.Implemented, "clear"),
new(Status.Implemented, "filter"),
new(Status.Implemented, "unfilter"),
new(Status.Implemented, "messagetypes", "message_types", "msgtypes", "msg_types"),
new(Status.ServerPassthrough, "loadfile"), // deliberate — doc §3 "explicitly do NOT implement"
// §2.3 Chat channels (20)
new(Status.Implemented, "a", "ab"),
new(Status.Implemented, "co-vassals", "covassals", "covassal", "c"),
new(Status.Implemented, "monarch", "m"),
new(Status.Implemented, "patron", "p"),
new(Status.Implemented, "vassals", "vassal", "v"),
new(Status.Implemented, "fellowship", "fellows", "fellow", "f", "group", "g", "party"),
// §2.3 fallback: 22 GetChannelID tags with NO registered verb (22)
new(Status.Implemented, "av", "av1", "advocate", "advocate1"),
new(Status.Implemented, "av2", "advocate2"),
new(Status.Implemented, "av3", "advocate3"),
new(Status.Implemented, "abuse"),
new(Status.Implemented, "ad", "admin"),
new(Status.Implemented, "au", "audit"),
new(Status.Implemented, "sent", "sentinel"),
new(Status.Implemented, "celestialhand", "celhan"),
new(Status.Implemented, "eldrytchweb", "eldweb"),
new(Status.Implemented, "radiantblood", "radblo"),
new(Status.Implemented, "ol"),
// §2.4 Turbine chat, 14 net-new (a already counted in §2.3) (14)
new(Status.Implemented, "guild", "gu"),
new(Status.Implemented, "general", "cg"),
new(Status.Implemented, "trade", "ct"),
new(Status.Implemented, "lfg", "clfg"),
new(Status.Implemented, "roleplay", "crp"),
new(Status.Implemented, "society", "soc"),
new(Status.Implemented, "olthoi", "o"),
// §2.5 Allegiance management (6; "ab" already counted in §2.3)
new(Status.Implemented, "allegiance", "all"),
new(Status.Implemented, "alh", "ah"),
new(Status.ServerPassthrough, "motd"), // TS-68
new(Status.Implemented, "speaker"),
// §2.5b Housing (7)
new(Status.Implemented, "house", "hou"),
new(Status.Implemented, "hor", "hr"),
new(Status.Implemented, "hom", "hoa"),
new(Status.Implemented, "hslist"),
// §2.6 Death / recall / PK (17)
new(Status.Implemented, "lifestone", "lif", "ls"),
new(Status.Implemented, "marketplace", "mar", "mp"),
new(Status.Implemented, "pkarena", "pka"),
new(Status.Implemented, "pklarena", "pla"),
new(Status.Implemented, "pklite", "pkl"),
new(Status.Implemented, "die"),
new(Status.Implemented, "corpse", "cor"),
new(Status.Implemented, "consent"),
new(Status.Implemented, "permit"),
// §2.7 Status / display (8)
new(Status.Implemented, "age"),
new(Status.Implemented, "birth"),
new(Status.ServerPassthrough, "day"), // TS-69 — no sky/time-of-day override hook
new(Status.Implemented, "endurance"),
new(Status.Implemented, "framerate"),
new(Status.Implemented, "loc"),
new(Status.Implemented, "version"),
new(Status.ServerPassthrough, "render"), // TS-69 — no SmartBox equivalent
// §2.8 Interface layout, emotes, social, components (18)
new(Status.Implemented, "saveui"),
new(Status.Implemented, "loadui"),
new(Status.Implemented, "saveautoui"),
new(Status.Implemented, "loadautoui"),
new(Status.Implemented, "lockui"),
new(Status.Implemented, "emote", "e", "em", "me"),
new(Status.Implemented, "emotes"),
new(Status.Implemented, "afk"),
new(Status.Implemented, "friends"),
new(Status.Implemented, "friends_add"),
new(Status.Implemented, "friends_remove"),
new(Status.Implemented, "squelch"),
new(Status.Implemented, "unsquelch"),
new(Status.Implemented, "fillcomps"),
];
private static readonly HashSet<string> CatalogVerbs =
new(RetailClientCommandCatalog.KnownVerbs, StringComparer.OrdinalIgnoreCase);
private static bool IsExecutable(string verb)
{
if (verb is "help" or "?")
return true; // local presentation — not a catalog/parser entry.
if (CatalogVerbs.Contains(verb))
return true;
if (ChatInputParser.IsKnownVerb("/" + verb))
return true;
if (RetailChannelTagTable.IsUnregisteredFallbackTag(verb))
return true;
return false;
}
public static IEnumerable<object[]> AllVerbsWithStatus() =>
Registry.SelectMany(entry => entry.Verbs.Select(verb => new object[] { verb, entry.Status }));
[Theory]
[MemberData(nameof(AllVerbsWithStatus))]
public void EveryRegistryVerb_HasTheCorrectOwnershipStatus(string verb, object statusObj)
{
var status = (Status)statusObj;
bool executable = IsExecutable(verb);
if (status == Status.Implemented)
{
Assert.True(executable,
$"'{verb}' is marked Implemented in the registry but no production surface " +
"(RetailClientCommandCatalog, ChatInputParser, or RetailChannelTagTable) recognizes it.");
}
else
{
Assert.False(executable,
$"'{verb}' is marked {status} (must fall through to server passthrough) but a " +
"production surface claims to execute it locally — that's a real behavior change " +
"the registry doesn't know about yet.");
}
}
[Fact]
public void Registry_EnumeratesExactly152Verbs()
{
int total = Registry.Sum(entry => entry.Verbs.Length);
Assert.Equal(152, total);
}
[Fact]
public void Registry_StatusCountsMatchTheAuditedTotals()
{
Assert.Equal(9, Registry.Where(e => e.Status == Status.HelpOnly).Sum(e => e.Verbs.Length));
Assert.Equal(5, Registry.Where(e => e.Status == Status.ServerPassthrough).Sum(e => e.Verbs.Length));
Assert.Equal(138, Registry.Where(e => e.Status == Status.Implemented).Sum(e => e.Verbs.Length));
}
[Fact]
public void NoDuplicateVerbsAcrossEntries()
{
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (Entry entry in Registry)
{
foreach (string verb in entry.Verbs)
{
Assert.True(seen.Add(verb), $"'{verb}' appears more than once in the registry.");
}
}
}
// ── Ownership-rule enforcement, reverse direction ──────────────────
//
// A verb RetailClientCommandCatalog or ChatInputParser claims to
// execute that ISN'T in the registry above is exactly the "acdream
// invented a verb retail doesn't have" class of bug the doc's §4
// "candidates for removal" list called out (gen, cv, lookingforgroup,
// tr, role, h) — deleted at CH4. This test fails the build the next
// time one slips back in.
[Fact]
public void RetailClientCommandCatalog_HasNoVerbsOutsideTheRegistry()
{
var registryVerbs = new HashSet<string>(
Registry.SelectMany(e => e.Verbs), StringComparer.OrdinalIgnoreCase);
foreach (string verb in RetailClientCommandCatalog.KnownVerbs)
{
Assert.True(registryVerbs.Contains(verb),
$"RetailClientCommandCatalog recognizes '{verb}', which is not in the retail " +
"command-registry doc — either it's a genuine retail verb missing from this " +
"test's registry, or it's an invented alias that must be deleted.");
}
}
[Fact]
public void ChatInputParser_HasNoVerbsOutsideTheRegistry()
{
var registryVerbs = new HashSet<string>(
Registry.SelectMany(e => e.Verbs), StringComparer.OrdinalIgnoreCase);
foreach (string verbWithSlash in ChatInputParser.KnownVerbs)
{
string verb = verbWithSlash.TrimStart('/');
Assert.True(registryVerbs.Contains(verb),
$"ChatInputParser recognizes '{verbWithSlash}', which is not in the retail " +
"command-registry doc — either it's a genuine retail verb missing from this " +
"test's registry, or it's an invented alias that must be deleted.");
}
}
}