fix(net): FA1 review round -- zero-id tree rejection, monarch clear, 0x001F builder

Applies both MUST-FIX items and the code-facing SHOULD-FIX items from the
dual-lens FA1 review (docs/research/2026-08-12-fa1-review-mechanism.md,
docs/research/2026-08-12-fa1-review-blast.md):

Mechanism MF-1 / blast SF-2: AllegianceHierarchy::Add @0x005B6E90 wraps its
entire body in `if (_id != 0)` -- a record whose own id is zero discards
the WHOLE message, for both the monarch and a child record, and this is
also what makes treeParent == 0 unconditionally fatal for a non-monarch
record. ReadAllegianceProfileBody now rejects CharacterId == 0 on both
paths; four new boundary tests in AllegianceProfileVersionGateTests.cs
(zero-id monarch, zero-id child, zero treeParent, plus the existing
orphan/self-parent/duplicate trio).

Mechanism MF-2: added the missing 0x001F AllegianceUpdateRequest builder --
the structural twin of the fellowship 0x00A6 this slice already repaired --
with golden-vector tests for both on/off.

Mechanism SF-1 / blast SF-3: UnPack's last act before returning success
forces the monarch's MayPassupExperience to false regardless of the wire
bit or the HasPackedLevel-absent legacy-compat fallback. Ported at the end
of the record loop; the pre-existing HasPackedLevel-absent test moved off
the monarch record (which the new clear makes indistinguishable from "the
fallback never fired") onto a vassal record, and a new test proves the
monarch clear fires even when the wire bit explicitly asks for true.

Mechanism SF-2: removed ParseFellowshipDisband's invented body-length
validation -- retail's DispatchUI_Disband reads only the opcode and never
inspects a trailing body. The parser now always succeeds; the matching
test flips from asserting rejection to asserting acceptance.

Mechanism SF-3: added the D5 `<<1` shareLoot-shape test at the 0x02C0
FellowshipUpdateFellow site -- previously only pinned at 0x02BE, so a
future split of the shared ReadFellow helper could silently reintroduce a
bool read on this leg undetected.

Mechanism SF-5: renumbered the version-gate comments in
ReadAllegianceProfileBody to the true AllegianceVersion enum values
(1-11, matching acclient.h's SpokespersonAdded..ApprovedVassal) instead of
wire-appearance order, which only reached 10 and silently dropped gate 5
(BannedCharactersAdded, which is real but gates nothing in UnPack -- now
called out explicitly). Fixed the stale "lane B §12" citation in
SocialActions.cs to the actual master-table row.

Blast SF-1: pinned the two retail-faithful but user-visible behavior
changes FA1 made to the ALREADY-LIVE `@allegiance info` command --
reversed vassal print order (3-vassal test through
FormatAllegianceInfoLines) and malformed-tree silent-drop (test at the
GameEventWiring registration layer, which is `if (info is null) return;`).

Blast SF-4: fixed a doc comment citing a nonexistent `ConfirmationResponseTests`
class; the actual class is `ConfirmationTripleTests`.

Blast SF-5: cross-referenced the confirmation-triple discriminator's split
representation (ConfirmationType on the response leg only; bare uint on
the two inbound legs production actually reads) at both sites, so FA4
inherits a stated decision rather than an unexplained inconsistency.

Full Release suite: 13,158 passed / 4 skipped / 0 failed (13,162 total),
up from the pre-fix-round 13,149/4/0 (+9 tests this round).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 00:46:42 +02:00
parent bc693728a6
commit ed30808720
8 changed files with 385 additions and 34 deletions

View file

@ -88,4 +88,41 @@ public sealed class AllegianceRequestsTests
Assert.Equal(AllegianceRequests.BreakOpcode,
BinaryPrimitives.ReadUInt32LittleEndian(body.AsSpan(8)));
}
// FA1 review round (mechanism MF-2): the missing 0x001F
// AllegianceUpdateRequest builder — the structural twin of the
// fellowship 0x00A6 this slice already repaired. Golden vector
// hand-derived from lane C §3.2: [u32 0xF7B1][u32 seq][u32 0x1F]
// [u32 on], total 0x10.
[Fact]
public void BuildAllegianceUpdateRequest_GoldenByteVector_On()
{
byte[] body = AllegianceRequests.BuildAllegianceUpdateRequest(gameActionSequence: 4, on: true);
byte[] expected =
[
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
0x04, 0x00, 0x00, 0x00, // seq 4
0x1F, 0x00, 0x00, 0x00, // opcode 0x001F
0x01, 0x00, 0x00, 0x00, // on = 1
];
Assert.Equal(expected, body);
}
[Fact]
public void BuildAllegianceUpdateRequest_GoldenByteVector_Off()
{
byte[] body = AllegianceRequests.BuildAllegianceUpdateRequest(gameActionSequence: 7, on: false);
byte[] expected =
[
0xB1, 0xF7, 0x00, 0x00, // envelope 0xF7B1
0x07, 0x00, 0x00, 0x00, // seq 7
0x1F, 0x00, 0x00, 0x00, // opcode 0x001F
0x00, 0x00, 0x00, 0x00, // on = 0
];
Assert.Equal(expected, body);
}
}