fix(chat): consolidated-review fixes — retail /help Detail extraction, seam wiring test

SHOULD-FIX 1: RetailClientCommandCatalog's ~45 catalog leaf verbs were
showing acdream-authored Summary text for /help <verb> instead of
retail's own Detail_HelpType(2) text. Byte-swept every Help* handler
against the PDB-paired acclient.exe (verified MATCH), confirmed each
Detail/Summary branch by reading the actual decompiled if/else shape
(address order and string length both proved unreliable alone), and
fixed a sweep_weenie_strings.py 800-char truncation bug that silently
dropped several longer Detail branches. Resolved every ambiguous
CmdHashData-registered verb (hor/hr/hom/hoa/alh/ah/friends_add/
friends_remove/squelch/unsquelch) by reading for Binary Ninja's
nullptr-4th-arg decompiler artifact instead of trusting it. Coverage:
42 of 47 distinct catalog Definitions verbatim-extracted, 4
confirmed-null (index/clist/on/off register with a genuinely null help
pointer — DoHelp falls to UnknownCommand for these, now reproduced),
1 honest UNVERIFIED (messagetypes builds its text from a runtime enum
table, not a static string). ChatCommandRouter now prefers retail
Detail text over the catalog summary; RetailCommandHelpTable's class
doc no longer overclaims its own scope.

SHOULD-FIX 2: extracted the a5a7eb4f-class OnInterfaceText wiring into
a testable CreateChatViewModel method and added
ComposedChatViewModelWiresOnInterfaceTextToSpewBox, which the prior
FakeFactory-based test suite could never exercise.

SHOULD-FIX 3: retires register row AP-113. DoLifestone/DoMarketplace
print their own 0x1A refusal text (byte-recovered, UTF-16LE) instead
of falling through to the generic 0x26 fallback; ChatCommandRouter's
comment corrected to state the fallback's real scope.

SHOULD-FIX 4: corrected the divergence register's stale AP section
header sentence about AP-190's opacity default (refuted by cc582899).

NITs: (a) HeadlessStaticStateAudit routes through the injected
HeadlessDiagnosticWriter instead of Console.WriteLine; (b) a bounded
300-pump liveness diagnostic on the IsQuiescent conductor gate (no
retry, no behavior change); (c) fixed the #365 hydration test's doc
comment contradiction against diagnosis §8; (d) the 0x26 fallback
dispatches on WeenieErrorMessages' own Type instead of hardcoding
ClientLocal.

Full Release suite: 12,553 passed / 4 skipped / 0 failed (baseline
03404b71: 12,542/4/0; net +11 tests, zero regressions).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 16:22:36 +02:00
parent 03404b7121
commit f7a6f46ba0
15 changed files with 947 additions and 85 deletions

View file

@ -93,20 +93,23 @@ public class ChatCommandRouterTests
}
[Fact]
public void LifestoneWithArguments_ShowsRetailBadArgsRefusal_ViaInterfaceTextSeam()
public void LifestoneWithArguments_ShowsRetailsBespokeRefusal_ViaInterfaceTextSeam()
{
// #363 / register row AP-183: a Definition with no bespoke
// InvalidArgumentsText falls to retail's own generic bad-args
// fallback (HandleFailureEvent(0x26), "That is not a valid
// command.") at 0x1A ClientLocal — never the acdream-invented
// "Usage: /lifestone" line this used to synthesize.
// Consolidated-review round (2026-08-10), SHOULD-FIX 3: retires
// register row AP-113. DoLifestone prints ITS OWN 0x1A string for
// bad args and returns 1 -- retail never reaches the generic
// HandleFailureEvent(0x26) fallback for it. Byte-recovered from the
// PDB-paired acclient.exe (Binary Ninja mis-attributes the literal
// to an unrelated vtable-slot symbol).
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
var outcome = ChatCommandRouter.Submit("/ls now", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
Assert.Empty(bus.Published);
Assert.Equal("That is not a valid command.", Assert.Single(interfaceTexts));
Assert.Equal(
"Please see @help lifestone for more information on how to use this command.",
Assert.Single(interfaceTexts));
Assert.Empty(log.Snapshot());
}
@ -122,10 +125,51 @@ public class ChatCommandRouterTests
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entry = Assert.Single(log.Snapshot());
Assert.Equal("That is not a valid command.", entry.Text);
Assert.Equal(
"Please see @help lifestone for more information on how to use this command.",
entry.Text);
Assert.Equal((uint)RetailLogTextType.ClientLocal, entry.LogTextType);
}
[Fact]
public void PkArenaWithArguments_ShowsRetailsGenericBadArgsFallback_ViaInterfaceTextSeam()
{
// #363 / register row AP-183: a Definition with no bespoke
// InvalidArgumentsText (PkArena never had one, unlike Lifestone/
// Marketplace above) falls to retail's own generic bad-args
// fallback (HandleFailureEvent(0x26), "That is not a valid
// command.") at 0x1A ClientLocal -- never the acdream-invented
// "Usage: /pkarena" line this used to synthesize. Carries the
// generic-fallback coverage the two Lifestone tests above used to
// own before Lifestone got its own bespoke text.
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
var outcome = ChatCommandRouter.Submit("/pka now", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
Assert.Empty(bus.Published);
Assert.Equal("That is not a valid command.", Assert.Single(interfaceTexts));
Assert.Empty(log.Snapshot());
}
[Fact]
public void MarketplaceWithArguments_ShowsRetailsBespokeRefusal_ViaInterfaceTextSeam()
{
// Same methodology and same vtable-mislabeling artifact as
// Lifestone above -- see RetailClientCommandCatalog.Marketplace's
// own citation.
var (vm, log, bus, interfaceTexts) = FixtureWithInterfaceSink();
var outcome = ChatCommandRouter.Submit("/mar now", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
Assert.Empty(bus.Published);
Assert.Equal(
"Please see @help marketplace for more information on how to use this command.",
Assert.Single(interfaceTexts));
Assert.Empty(log.Snapshot());
}
[Fact]
public void PkLiteAlias_ResolvesAsClientHandled_NotTheServerTextPath()
{

View file

@ -255,6 +255,182 @@ public sealed class RetailCommandHelpTableTests
entries[1].Text);
}
// ── Consolidated-review round (2026-08-10), SHOULD-FIX 1 ────────────
// Catalog leaf-verb Detail extraction: /help die and /help lifestone
// pinned byte-exact through the router (the actual user-visible
// outcome, not just the table's stored string — same pattern as
// DeathGroup_ThroughRouter_PrintsRetailsCompleteTwoEntryShape above),
// plus a coverage-count assertion so a future extraction pass (or an
// accidental regression that silently drops an override) is caught.
[Fact]
public void HelpDie_ThroughRouter_PrintsRetailsExactDetailText()
{
// HelpDie @0x005784a0: Summary @0x005784ad ("Kills your character
// and leaves a corpse..."), Detail @0x005784b4 (the long paragraph
// below) -- DoHelp always calls Detail_HelpType(2) for a resolved
// verb, never Summary. Byte-swept against the PDB-paired
// C:\Users\erikn\Downloads\acclient.exe (verified MATCH).
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
"/help die", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Equal(2, entries.Length);
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
Assert.Equal(
RetailCommandHelpTable.ForMoreInformationPrefix
+ RetailCommandHelpTable.DieDetail,
entries[1].Text);
Assert.Equal(
"@die - If you wish to kill your character and leave a corpse, "
+ "you may use the @die command. This will result in your "
+ "character's death, you will leave behind a corpse with some "
+ "of your items, and you will appear at your lifestone. If "
+ "you wish to travel to your lifestone without leaving behind "
+ "a corpse, you may use the @lifestone command.\n",
RetailCommandHelpTable.DieDetail);
}
[Fact]
public void HelpLifestone_ThroughRouter_PrintsRetailsExactDetailText()
{
// HelpLifestone @0x00578500 is UNCONDITIONAL (no Summary/Detail
// branch at all) -- the same string prints for every HelpType,
// confirmed by reading the decompiled body directly (no if/else).
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
"/help lifestone", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Equal(2, entries.Length);
Assert.Equal(RetailCommandHelpTable.HelpPrefixNote, entries[0].Text);
Assert.Equal(
RetailCommandHelpTable.ForMoreInformationPrefix
+ RetailCommandHelpTable.LifestoneDetail,
entries[1].Text);
Assert.Equal(
"@lifestone - Returns you to the last lifestone you used without killing you.\n",
RetailCommandHelpTable.LifestoneDetail);
}
[Fact]
public void HelpAlh_RoutesToTheSameHelpAllegianceTextAsHelpAllegiance()
{
// alh/ah are registered with HelpAllegiance as their OWN help
// pointer (confirmed via InitializeCommands's field-store
// registration pattern, not the CmdHashData-constructor pattern) --
// the exact same function "allegiance"/"all" use, so /help alh must
// show byte-identical text to /help allegiance, not a separate
// (possibly stale) copy.
Assert.True(RetailCommandHelpTable.TryGetCatalogVerbDetailText(
"alh", out string alhText));
Assert.Equal(RetailCommandHelpTable.AllegianceOverview, alhText);
}
[Theory]
[InlineData("index")]
[InlineData("clist")]
[InlineData("on")]
[InlineData("off")]
public void ConfirmedNullHelpVerb_ThroughRouter_ShowsUnknownCommandNotCatalogSummary(
string verb)
{
// index/clist/on/off are registered in InitializeCommands with a
// plain `int32_t var_3c_NN = 0;` local -- no preceding Help-pointer
// 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.
var log = new AcDream.Core.Chat.ChatLog();
var vm = new ChatVM(log, displayLimit: 50);
var bus = new RecordingCommandBus();
var outcome = ChatCommandRouter.Submit(
$"/help {verb}", vm, bus, ChatChannelKind.Say);
Assert.Equal(SubmitOutcome.ClientHandled, outcome);
var entries = log.Snapshot();
Assert.Single(entries);
Assert.Equal(RetailCommandHelpTable.UnknownCommand, entries[0].Text);
Assert.Equal(
(uint)AcDream.Core.Chat.RetailLogTextType.ClientLocal,
entries[0].LogTextType);
}
[Fact]
public void CatalogLeafVerbCoverage_ExtractedVsConfirmedNullVsUnverified_MatchesConsolidatedReviewCount()
{
// Coverage-count assertion (SHOULD-FIX 1's explicit acceptance
// criterion): dedupe RetailClientCommandCatalog.KnownVerbs down to
// one entry per distinct Definition via its (unique-per-Definition)
// catalog summary text, excluding the two meta-dispatchers
// ("house"/"hou" and "allegiance"/"all" resolve subcommand-by-
// subcommand, not as a single leaf Definition), then classify each
// distinct Definition into exactly one bucket. A future extraction
// pass should bump ExtractedCount down UnverifiedCount and update
// this pin in the same commit; an accidental regression that drops
// an override without updating CatalogVerbsWithNoRetailHelp would
// fail this test instead of silently reverting to a summary.
var seenSummaries = new HashSet<string>(StringComparer.Ordinal);
int extractedCount = 0;
int confirmedNullCount = 0;
int unverifiedCount = 0;
foreach (string verb in RetailClientCommandCatalog.KnownVerbs)
{
if (verb is "house" or "hou" or "allegiance" or "all")
continue;
Assert.True(
RetailClientCommandCatalog.TryGetHelpText(verb, out string summary),
$"catalog verb '{verb}' has no summary text");
if (!seenSummaries.Add(summary))
continue; // an alias of a Definition already counted
bool confirmedNull = RetailCommandHelpTable.CatalogVerbsWithNoRetailHelp
.Contains(verb);
bool extracted = RetailCommandHelpTable.TryGetCatalogVerbDetailText(
verb, out _);
// A verb must never be BOTH confirmed-null AND carry an
// extracted override -- that would mean ChatCommandRouter's
// ordering (confirmed-null checked first) is hiding a real
// Detail text, or the two tables disagree about the same verb.
Assert.False(
confirmedNull && extracted,
$"'{verb}' is both confirmed-null and has an extracted Detail override");
if (confirmedNull)
confirmedNullCount++;
else if (extracted)
extractedCount++;
else
unverifiedCount++;
}
// Measured, not hand-counted -- 47 distinct leaf Definitions total
// (squelch/unsquelch and friends/friends_add/friends_remove each
// count separately here since they are DISTINCT Definitions with
// their own summary text, even though several share ONE extracted
// Detail text -- see the CmdHashData note in RetailCommandHelpTable's
// class remarks).
Assert.Equal(42, extractedCount);
Assert.Equal(4, confirmedNullCount);
Assert.Equal(1, unverifiedCount);
}
private sealed class RecordingCommandBus : ICommandBus
{
public List<object> Published { get; } = new();