fix(chat): CH4 review fixes — allegiance ownership guard, house-abandon confirmation
Blocker 1: an unrecognized "@allegiance <sub>" subcommand escaped TryMatchAllegiance (which only claimed "info"/"hometown") and fell through the unregistered-tag channel fallback, broadcasting the raw subcommand text to the Allegiance chat channel (0x02000000). Retail's own DoAllegiance never reaches DoChannelCommand for an unrecognized subcommand — it claims the whole verb and prints its own client-local refusal. TryMatchAllegiance now claims "allegiance"/"all" unconditionally and shows retail's "Please see @help Allegiance..." text; ChatCommandRouter also gained a blanket RetailClientCommandCatalog.KnownVerbs ownership guard in TryDispatchChannelFallback as defense in depth. Blocker 2: "@house abandon" sent 0x021F immediately with no confirmation. Retail runs a real two-stage dialog before Event_AbandonHouse(); ported both verbatim strings and chained two ShowConfirmation calls. Should-fixes: a bare unregistered tag with no text now passes through silently instead of showing a refusal that belongs to a different retail function; @join/@leave update RuntimeCharacterOptionsState locally (new SetOptionBit) before the wire push so the Turbine membership gate stops refusing a just-joined room; @permit accepts multi-word names; @clist/ @on/@off validate shape only and raise WeenieError 0x422 for an unknown tag; @mr/@pr help text is now the verbatim retail strings; corrected issue #360, register row TS-68, the campaign doc's B.7 note, and a stale RetailChannelTagTable comment; filed issue #363 + register row AP-183 for the deferred error-typing debt. Nits: fixed TryMatchHouse's stale doc comment, the AP-182/@title "stores the value" comments (the binding is a no-op), IsUnregisteredFallbackTag's olthoi false-positive, added /g and /rp binding-level conformance pins, made @index ignore extra arguments, and noted the six removed invented verbs in ISSUES.md. Suite: 12,216 passed / 4 skipped / 0 failed (Release), up from CH4's 12,190/4/0 — net +26 tests, no removals. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
090825e703
commit
724ef2d389
17 changed files with 853 additions and 85 deletions
|
|
@ -300,6 +300,124 @@ public sealed class ClientCommandControllerTests
|
|||
Assert.Equal(["Component list cleared.", "You need an open vendor."], messages);
|
||||
}
|
||||
|
||||
// ── 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,
|
||||
// the ONLY Event_AbandonHouse() call site @0x0057BF01) before sending
|
||||
// 0x021F — previously it sent immediately with no confirmation at all.
|
||||
|
||||
[Fact]
|
||||
public void HouseAbandon_BothStagesAccepted_ShowsBothPromptsThenSendsExactlyOnce()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var controller = NewController(calls);
|
||||
|
||||
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.HouseAbandon, ""));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"confirm:Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",
|
||||
"confirm:Are you absolutely certain you wish to abandon your house? Click yes only if you are sure!",
|
||||
"houseabandon",
|
||||
],
|
||||
calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HouseAbandon_DeclineFirstStage_ShowsOnlyOnePromptAndNeverSends()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var controller = NewController(calls, confirmationResponses: new Queue<bool>([false]));
|
||||
|
||||
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.HouseAbandon, ""));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"confirm:Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",
|
||||
],
|
||||
calls);
|
||||
Assert.DoesNotContain("houseabandon", calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HouseAbandon_DeclineSecondStage_ShowsBothPromptsAndNeverSends()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var controller = NewController(calls, confirmationResponses: new Queue<bool>([true, false]));
|
||||
|
||||
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.HouseAbandon, ""));
|
||||
|
||||
Assert.Equal(
|
||||
[
|
||||
"confirm:Do you really want to abandon your house? Any items in the house (on hooks or in storage) will stay with the house, and you will lose access to them.",
|
||||
"confirm:Are you absolutely certain you wish to abandon your house? Click yes only if you are sure!",
|
||||
],
|
||||
calls);
|
||||
Assert.DoesNotContain("houseabandon", calls);
|
||||
}
|
||||
|
||||
// ── CH4 REJECT-review SHOULD-FIX 5 (2026-08-09) ─────────────────────
|
||||
// "@permit add/remove <multi-word name>" joins every token after the
|
||||
// mode word into the name (retail's JoinArgsAsName).
|
||||
|
||||
[Fact]
|
||||
public void Permit_MultiWordName_JoinsTheRemainderIntoOneName()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var controller = NewController(calls);
|
||||
|
||||
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Permit, "add Aunt Agatha"));
|
||||
controller.Execute(new ExecuteClientCommandCmd(ClientCommandId.Permit, "remove Lord Gnarly Beard"));
|
||||
|
||||
Assert.Equal(
|
||||
["permitadd:Aunt Agatha", "permitremove:Lord Gnarly Beard"],
|
||||
calls);
|
||||
}
|
||||
|
||||
// ── CH4 REJECT-review SHOULD-FIX 6 (2026-08-09) ─────────────────────
|
||||
// @clist/@on/@off with an unresolvable (but single-token) tag raises
|
||||
// retail's WeenieError 0x422 ("That channel doesn't exist.") instead
|
||||
// of silently doing nothing.
|
||||
|
||||
[Fact]
|
||||
public void ChannelArgumentCommands_UnknownTag_ShowsWeenieError422WithoutSending()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var errors = new List<uint>();
|
||||
var controller = NewController(calls, errors);
|
||||
|
||||
Execute(ClientCommandId.ListChannel, "nonsense");
|
||||
Execute(ClientCommandId.OnChannel, "nonsense");
|
||||
Execute(ClientCommandId.OffChannel, "nonsense");
|
||||
|
||||
Assert.Empty(calls);
|
||||
Assert.Equal([0x0422u, 0x0422u, 0x0422u], errors);
|
||||
|
||||
void Execute(ClientCommandId id, string arguments) =>
|
||||
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChannelArgumentCommands_KnownTag_SendsWithoutError()
|
||||
{
|
||||
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");
|
||||
|
||||
Assert.Empty(errors);
|
||||
Assert.Equal(
|
||||
["clist:2048", "on:2", "off:512"],
|
||||
calls);
|
||||
|
||||
void Execute(ClientCommandId id, string arguments) =>
|
||||
controller.Execute(new ExecuteClientCommandCmd(id, arguments));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UnknownCommandId_FailsAtApplicationBoundary()
|
||||
{
|
||||
|
|
@ -319,7 +437,13 @@ public sealed class ClientCommandControllerTests
|
|||
FriendsState? friends = null,
|
||||
SquelchState? squelch = null,
|
||||
string? lastTeller = null,
|
||||
bool vendorOpen = false)
|
||||
bool vendorOpen = false,
|
||||
// CH4 REJECT-review Blocker 2 (2026-08-09): lets a test drive a
|
||||
// specific accept/decline sequence through consecutive
|
||||
// ShowConfirmation calls (e.g. house-abandon's two-stage prompt).
|
||||
// Defaults to "always accept" so every pre-existing single-stage
|
||||
// test (Die, etc.) keeps its original behavior unchanged.
|
||||
Queue<bool>? confirmationResponses = null)
|
||||
{
|
||||
calls ??= [];
|
||||
errors ??= [];
|
||||
|
|
@ -347,7 +471,10 @@ public sealed class ClientCommandControllerTests
|
|||
(message, completed) =>
|
||||
{
|
||||
calls.Add($"confirm:{message}");
|
||||
completed(true);
|
||||
bool accepted = confirmationResponses is { Count: > 0 }
|
||||
? confirmationResponses.Dequeue()
|
||||
: true;
|
||||
completed(accepted);
|
||||
},
|
||||
() => calls.Add("suicide"),
|
||||
all => calls.Add("clear:" + all),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue