fix(chat): CH4 re-review fixes — dialog-queue reentrancy, settings option-bit chokepoint

Should-fix 1: RetailDialogFactory.CloseDialog's queued branch removed the
active DialogInfo, ran DialogDone (whose callback can synchronously open a
new dialog under the SAME queue key — the two-stage house-abandon
confirmation does exactly this), then called OpenNextDialog, which did an
unconditional Dictionary.Add on a key the reentrant dialog had already
re-occupied. Retail's HashTable::add tolerates the duplicate; Dictionary
throws. OpenNextDialog now returns early when the queue key is already
active — the reentrant dialog's own eventual close drains the queue.

Should-fix 2: @join/@leave wrote the local RuntimeCharacterOptionsState bit
before sending, but the Settings Chat toggles reached a second binding
(SendSingleCharacterOption) that only sent the wire message, leaving the
Turbine membership gate stale until the next PlayerDescription.
LiveSessionRuntimeFactory.CreateCommandBindings now has one shared local
function for both entrances.

Should-fix 3: corrected TS-68/#360 wording again — retail's DoAllegiance
dispatcher table EXECUTES boot/ban/officer/title/motd/name/lock/house/
chat/broadcast locally through their own handlers; acdream shows the
unrecognized-subcommand refusal for all nine pending the #360 port. What
matches retail is the ownership rule (the verb never reaches
DoChannelCommand/the server), not the subcommand behavior itself. Removed
the inaccurate "matching retail, not merely harmless" / "now matches
this" claims from both the register row and the issue.

Nits: corrected the HouseAbandonDialogCallback_First citation (0x00580E1A
is DoHouse's load site for the callback pointer, not the function entry —
the entry is 0x00580240, with the stage-2 confirmation string built at
0x005802D8) in both ClientCommandController.cs and the mirrored test
comment; added an InlineData case pinning "@clist allegiance" to
RequestChannelList(0x02000000); converted RetailClientCommandCatalog.
KnownVerbs from a plain array to a FrozenSet<string> with
StringComparer.OrdinalIgnoreCase, matching the file's other lookup tables.

Suite: 12,221 passed / 4 skipped / 0 failed (Release), up from CH4's
12,216/4/0 — net +5 tests, no removals.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-09 22:24:43 +02:00
parent 59c053ee47
commit 5d247d5518
10 changed files with 204 additions and 34 deletions

View file

@ -303,9 +303,12 @@ public sealed class ClientCommandControllerTests
// ── CH4 REJECT-review Blocker 2 (2026-08-09) ────────────────────────
// "@house abandon" must run retail's real two-stage confirmation
// (DoHouse's abandon branch @0x00580D58 → HouseAbandonDialogCallback_
// First @0x00580E1A → HouseAbandonDialogCallback_Second @0x0057BE90,
// First, function entry @0x00580240 (0x00580E1A is only DoHouse's load
// site for the callback pointer), whose own stage-2 string site
// @0x005802D8 opens HouseAbandonDialogCallback_Second @0x0057BE90,
// the ONLY Event_AbandonHouse() call site @0x0057BF01) before sending
// 0x021F — previously it sent immediately with no confirmation at all.
// Citations corrected 2026-08-09, CH4 re-review nit 4.
[Fact]
public void HouseAbandon_BothStagesAccepted_ShowsBothPromptsThenSendsExactlyOnce()
@ -398,24 +401,26 @@ public sealed class ClientCommandControllerTests
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
}
[Fact]
public void ChannelArgumentCommands_KnownTag_SendsWithoutError()
[Theory]
[InlineData(ClientCommandId.ListChannel, "fellowship", "clist:2048")]
[InlineData(ClientCommandId.OnChannel, "admin", "on:2")]
[InlineData(ClientCommandId.OffChannel, "sentinel", "off:512")]
// CH4 re-review nit 5 (2026-08-09): pins "@clist allegiance" to
// RequestChannelList(0x02000000) — retail's real Allegiance channel id
// (RetailChannelTagTable's "allegiance"/"a"/"ab" entries), distinct
// from the small legacy-channel ids the rows above cover.
[InlineData(ClientCommandId.ListChannel, "allegiance", "clist:33554432")]
public void ChannelArgumentCommands_KnownTag_SendsWithoutError(
ClientCommandId id, string tag, string expectedCall)
{
var calls = new List<string>();
var errors = new List<uint>();
var controller = NewController(calls, errors);
Execute(ClientCommandId.ListChannel, "fellowship");
Execute(ClientCommandId.OnChannel, "admin");
Execute(ClientCommandId.OffChannel, "sentinel");
controller.Execute(new ExecuteClientCommandCmd(id, tag));
Assert.Empty(errors);
Assert.Equal(
["clist:2048", "on:2", "off:512"],
calls);
void Execute(ClientCommandId id, string arguments) =>
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
Assert.Equal([expectedCall], calls);
}
[Fact]