fix: nested room-details objects carry only keys the client reads

Extends the parity check into the payload's nested objects — sub-room 0, the
first role, stats and the first tag each have their own generated reader, and a
key set that disagrees with one of those breaks the room just as hard as a
wrong top-level set.

It immediately found seven invented keys. Six on every sub-room (CurrentSave,
DefaultMatchmakingPolicy, LastModeratedSaveModerationState, ShouldAutoStageSaves,
StagedSubRoomDataSaveId, SubRoomUnityAssetId) and IsPrimaryGenre on every tag,
whose real shape is exactly {Tag, Type}. Six of the seven appear NOWHERE in the
decompile; SubRoomUnityAssetId exists but belongs to another DTO, not this one.

The extractor's minimum field count drops from 4 to 2, which is what let this
hide: a room tag has two fields, so a 4-field floor skipped its reader entirely
and the invented third key had nothing to be checked against. The alias rule in
fields_in() is what rejects false positives — the field count never was.

This does not explain the clone failure on its own: the captured response
carries all 41 top-level keys and is still rejected. But it removes the whole
class of "we send something the reader has no slot for" from the search.
This commit is contained in:
Alexa
2026-07-28 11:27:35 +03:00
parent 078d576383
commit 4a1ac47007
4 changed files with 1103 additions and 47 deletions
@@ -85,8 +85,40 @@ public sealed class ClientDtoParityTests : IClassFixture<DorkNetServerFactory>
Assert.True(response.IsSuccessStatusCode, $"GET /rooms/{roomId} -> {(int)response.StatusCode}: {body}");
AssertKeyParity(RoomDetailsReader, body, $"GET rooms/{roomId}");
// The reader walks into the nested objects too, and a wrong key set
// there fails exactly the same way — a 200 the client throws on.
var details = JsonDocument.Parse(body).RootElement;
foreach (var (property, reader) in NestedReaders)
{
if (!details.TryGetProperty(property, out var value)) continue;
var element = value.ValueKind switch
{
JsonValueKind.Array when value.GetArrayLength() > 0 => value[0],
JsonValueKind.Object => value,
_ => default,
};
if (element.ValueKind != JsonValueKind.Object) continue;
AssertKeyParity(reader, element.GetRawText(),
$"GET rooms/{roomId} → {property}{(value.ValueKind == JsonValueKind.Array ? "[0]" : "")}");
}
}
/// <summary>Nested objects in the room-details payload and the reader each
/// one is deserialised by. Sub-room 0, the first role, the stats object and
/// the first tag all have their own generated readers, and a key set that
/// disagrees with them breaks the whole room just as hard as a wrong
/// top-level one.</summary>
private static readonly (string Property, string Reader)[] NestedReaders =
[
("SubRooms", "JDEKAFDPGBE"),
("Roles", "LMEECKCCPHG"),
("Stats", "MLHBPDKBABN"),
("Tags", "GEGADPCDGBO"),
];
[Fact]
public async Task Chat_thread_carries_exactly_the_keys_the_client_reads()
{