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
|
|
@ -13,6 +13,7 @@ using AcDream.Core.Items;
|
|||
using AcDream.Core.Spells;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Panels.Chat;
|
||||
|
||||
namespace AcDream.App.Tests.Composition;
|
||||
|
||||
|
|
@ -149,6 +150,43 @@ public sealed class InteractionRetainedUiCompositionTests
|
|||
], fixture.Factory.Releases);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consolidated-review round (2026-08-10), SHOULD-FIX 2: the a5a7eb4f
|
||||
/// defect class — a composed <see cref="ChatVM.OnInterfaceText"/> hook
|
||||
/// wired but never transferred to production — had no test, because
|
||||
/// <see cref="FakeFactory.CreateRetainedUi"/> above substitutes a
|
||||
/// <see cref="Stub{T}"/> <see cref="ChatVM"/> for the composition-order
|
||||
/// tests and never exercises the REAL wiring in
|
||||
/// <see cref="RetailInteractionRetainedUiCompositionFactory.CreateChatViewModel"/>.
|
||||
/// This test calls that production method directly (it only touches
|
||||
/// <see cref="InteractionRetainedUiDependencies.Communication"/>, no
|
||||
/// GPU/dat/UiHost dependencies, so the existing null-heavy
|
||||
/// <see cref="Fixture"/> dependencies are sufficient without a full
|
||||
/// <see cref="Fixture.Compose"/>) and proves the hook both EXISTS and
|
||||
/// actually reaches <see cref="RuntimeCommunicationState.SpewBox"/>,
|
||||
/// not merely that it was assigned.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ComposedChatViewModelWiresOnInterfaceTextToSpewBox()
|
||||
{
|
||||
using var fixture = new Fixture(retailUi: true);
|
||||
|
||||
ChatVM chat = RetailInteractionRetainedUiCompositionFactory
|
||||
.CreateChatViewModel(fixture.Dependencies);
|
||||
|
||||
Assert.NotNull(chat.OnInterfaceText);
|
||||
|
||||
const string probeText = "consolidated-review SHOULD-FIX 2 probe";
|
||||
chat.ShowInterfaceText(probeText);
|
||||
// ShowInterfaceText only enqueues; SpewBoxState decouples enqueue
|
||||
// from visibility (see its own class remarks) -- Tick drains it.
|
||||
fixture.Dependencies.Runtime.CommunicationOwner.SpewBox.Tick(0);
|
||||
|
||||
Assert.Contains(
|
||||
fixture.Dependencies.Runtime.CommunicationOwner.SpewBox.Snapshot(),
|
||||
entry => entry.Text == probeText);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindowUsesPhaseAndContainsNoRetainedUiConstructionBody()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1558,14 +1558,32 @@ public sealed class HeadlessSessionHostTests
|
|||
/// a DIFFERENT landblock, the same shape as one of the OTHER eight
|
||||
/// landblocks in production's real 3x3 publication plan — proving Step
|
||||
/// 3a's gate covers "an admission is open ANYWHERE in the plan", not
|
||||
/// just the center. Must FAIL on the pre-Step-3a tree — verified by
|
||||
/// temporarily reverting the three <c>IsQuiescent</c> gates in
|
||||
/// <c>HeadlessSessionWorldProjection.ProjectSpawn</c>/
|
||||
/// <c>ProjectPosition</c>/<c>PumpFirstEntry</c> and confirming this test
|
||||
/// fails (the conductor reaches <c>PublicationCommitted</c> — a non-null
|
||||
/// dormant controller — while the unrelated admission is still held
|
||||
/// open, since driving is unconditional pre-fix).
|
||||
/// just the center.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Consolidated-review round (2026-08-10), NIT (c): what THIS bounded
|
||||
/// xunit test proves is the GATE, not the #365 hydration stall's
|
||||
/// closure — that broader claim's evidence is the separate live-ACE
|
||||
/// end-to-end run recorded in
|
||||
/// <c>docs/research/2026-08-10-365-headless-hydration-diagnosis.md</c>
|
||||
/// §8 ("hydration now succeeds ... entityCount reaches 136"). This
|
||||
/// test's own scope is narrower and fully mechanical: the conductor
|
||||
/// stays undriven (<c>runtime.MovementOwner.Controller</c> stays null)
|
||||
/// for as long as the unrelated admission is held open, and drives and
|
||||
/// reaches <c>IsRuntimePublished</c> within a bounded tick budget once
|
||||
/// it releases. Temporarily reverting the three <c>IsQuiescent</c>
|
||||
/// gates in <c>HeadlessSessionWorldProjection.ProjectSpawn</c>/
|
||||
/// <c>ProjectPosition</c>/<c>PumpFirstEntry</c> (never committed — see
|
||||
/// §8's "Verification that the new test... actually discriminates")
|
||||
/// makes this test fail the SAME way §8 itself observed: the
|
||||
/// <c>Assert.Null(runtime.MovementOwner.Controller)</c> checks below
|
||||
/// fail because the controller is ALREADY built AND published
|
||||
/// (<c>CanExecuteLiveMovement = True</c>) while the unrelated admission
|
||||
/// is still open — not merely "reached <c>PublicationCommitted</c>,
|
||||
/// still dormant" as an earlier draft of this comment claimed; that
|
||||
/// wording described a weaker intermediate state than what reverting
|
||||
/// the gate actually produces.
|
||||
/// </remarks>
|
||||
[Fact]
|
||||
public void RealAdmissionNeverDrivesTheConductorWhileOpenAndHydratesOnceReleased()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Headless.Configuration;
|
||||
using AcDream.Headless.Diagnostics;
|
||||
using AcDream.Headless.Hosting;
|
||||
|
||||
namespace AcDream.Headless.Tests;
|
||||
|
|
@ -8,9 +9,13 @@ namespace AcDream.Headless.Tests;
|
|||
/// #365 Step 1: the audit's refusal rationale is multi-root attribution
|
||||
/// ambiguity, which does not hold for a process that owns exactly one
|
||||
/// session. These tests mutate <see cref="PhysicsDiagnostics"/> process-
|
||||
/// global probe flags and swap <see cref="Console.Out"/>, so they run in
|
||||
/// their own non-parallel collection — see
|
||||
/// <see cref="HeadlessStaticStateAuditCollection"/>.
|
||||
/// global probe flags, so they run in their own non-parallel collection —
|
||||
/// see <see cref="HeadlessStaticStateAuditCollection"/>. Consolidated-
|
||||
/// review round (2026-08-10), NIT (a): the single-session log line now
|
||||
/// routes through the injected <see cref="HeadlessDiagnosticWriter"/>
|
||||
/// instead of <see cref="Console.Out"/>, so these tests capture its
|
||||
/// backing <see cref="TextWriter"/> directly rather than swapping the
|
||||
/// process-global console.
|
||||
/// </summary>
|
||||
[CollectionDefinition(
|
||||
HeadlessStaticStateAuditCollection.Name,
|
||||
|
|
@ -31,17 +36,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
|||
public void SingleSessionWithProbeEnabledIsAllowedAndLoggedLoudly()
|
||||
{
|
||||
PhysicsDiagnostics.ProbeParkEnabled = true;
|
||||
var originalOut = Console.Out;
|
||||
using var captured = new StringWriter();
|
||||
Console.SetOut(captured);
|
||||
try
|
||||
{
|
||||
HeadlessStaticStateAudit.ValidateProcessIsolation(sessionCount: 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
var diagnostics = new HeadlessDiagnosticWriter(captured);
|
||||
|
||||
HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||
sessionCount: 1, diagnostics);
|
||||
|
||||
Assert.Contains(
|
||||
nameof(PhysicsDiagnostics.ProbeParkEnabled),
|
||||
|
|
@ -52,17 +51,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
|||
[Fact]
|
||||
public void SingleSessionWithNoProbesEnabledIsSilent()
|
||||
{
|
||||
var originalOut = Console.Out;
|
||||
using var captured = new StringWriter();
|
||||
Console.SetOut(captured);
|
||||
try
|
||||
{
|
||||
HeadlessStaticStateAudit.ValidateProcessIsolation(sessionCount: 1);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Console.SetOut(originalOut);
|
||||
}
|
||||
var diagnostics = new HeadlessDiagnosticWriter(captured);
|
||||
|
||||
HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||
sessionCount: 1, diagnostics);
|
||||
|
||||
Assert.Equal(string.Empty, captured.ToString());
|
||||
}
|
||||
|
|
@ -71,11 +64,12 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
|||
public void MultiSessionWithProbeEnabledStillThrowsNamingTheProbe()
|
||||
{
|
||||
PhysicsDiagnostics.ProbeParkEnabled = true;
|
||||
var diagnostics = new HeadlessDiagnosticWriter(TextWriter.Null);
|
||||
|
||||
HeadlessConfigurationException exception = Assert.Throws<
|
||||
HeadlessConfigurationException>(
|
||||
() => HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||
sessionCount: 2));
|
||||
sessionCount: 2, diagnostics));
|
||||
|
||||
Assert.Contains(
|
||||
nameof(PhysicsDiagnostics.ProbeParkEnabled),
|
||||
|
|
@ -86,9 +80,11 @@ public sealed class HeadlessStaticStateAuditTests : IDisposable
|
|||
[Fact]
|
||||
public void MultiSessionWithNoProbesEnabledIsAllowed()
|
||||
{
|
||||
var diagnostics = new HeadlessDiagnosticWriter(TextWriter.Null);
|
||||
|
||||
Exception? exception = Record.Exception(
|
||||
() => HeadlessStaticStateAudit.ValidateProcessIsolation(
|
||||
sessionCount: 3));
|
||||
sessionCount: 3, diagnostics));
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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