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 bycc582899). 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 (baseline03404b71: 12,542/4/0; net +11 tests, zero regressions). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
03404b7121
commit
f7a6f46ba0
15 changed files with 947 additions and 85 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue