From c9ba53cfe156f71fcc0993ab910be74e6729265b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 23 Sep 2026 08:34:44 +0200 Subject: [PATCH] R-AT: pin the edge comparator's tied-from-and-role survivor in C# The same live mutant Soul found in Rust (dropping to.schemaId/to.recordKey from the edge comparator) survives in C# too - both runtimes share the comparator's shape, not a rule proving it. Land the C# mirror of Soul's self-citing-peer counterexample from the rig, renamed so the self edge's push order and the correct sort order actually disagree (the rig's own "hub"/"other" naming left the drop-both mutation surviving by coincidence of push order matching alphabetical order - see the Rust commit on this branch for the construction). Co-Authored-By: Claude Sonnet 5 --- .../CultNetSelectionFixBatchTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs b/tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs index ae16eedd..134850c7 100644 --- a/tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs +++ b/tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs @@ -626,6 +626,81 @@ namespace GameCult.Networking.Tests public CultRecordRef Ref = default; } + // R-AT (Soul's SM-7, merge gate third pass): the edge comparator's `to.schemaId`/ + // `to.recordKey` components survived dropping entirely, and reversing `to.recordKey` + // survived too, because every existing fixture ties `from`+`to` or `from`+`role` but never + // ties `from`+`role` while leaving `to` free to vary. Hands had argued no real selection + // could do that; Soul falsified the argument by building one: a selection carrying BOTH + // `cites` and `cited` mixes their edges into one Vec, and a row that self-references + // through the same role it also uses to cite a peer produces a `cites` edge (To = the + // queried peer) and a `cited` edge (To = the anchor row itself), tied on From and Role, + // differing only on To, in the same page-position bucket. Taken from the rig rather than + // rebuilt. + [CultDocument("cultnet.selection-tests.soul_self_citer", "cultnet.selection-tests.soul_self_citer.v1")] + [MessagePackObject(AllowPrivate = true)] + public sealed class SoulSelfCiter + { + [Key(0)] + [CultName] + public string Name = string.Empty; + + [Key(1)] + [CultReference(typeof(SoulSelfCiter), many: true)] + public CultRecordRef[] Peers = Array.Empty>(); + } + + // Named "zebra"/"alpha", not the rig's "hub"/"other": CultNetSelectionEvaluator.Select runs + // the Cited hop before the Cites hop (line ~198 vs ~206), so the self edge is always pushed + // to CitedEdges before the peer edge is pushed to CitesEdges. With "hub" self-citing and + // "other" as the cites target, "hub" < "other" already matches that push order, so dropping + // the comparator's To component reproduces the right answer by coincidence and the mutation + // survives. Naming the self-citer "zebra" and the target "alpha" makes push order + // [zebra, alpha] disagree with the required sorted order [alpha, zebra], so only the + // comparator's own To component - not insertion order - can produce it. + [Test] + public void Evaluator_EdgeOrderPinsToWhenFromAndRoleAreBothTiedByASelfCitingPeer() + { + var registry = CultDocumentRegistry.ForTypes(new[] { typeof(SoulSelfCiter) }); + var descriptor = registry.GetRequired(); + + var zebra = new SoulSelfCiter + { + Name = "zebra", + Peers = new[] + { + new CultRecordRef(new CultRecordKey("zebra")), + new CultRecordRef(new CultRecordKey("alpha")) + } + }; + var alpha = new SoulSelfCiter { Name = "alpha" }; + + var rows = new[] + { + new CultNetSelectionEvaluator.Row(descriptor, new CultRecordKey("zebra"), zebra, 1), + new CultNetSelectionEvaluator.Row(descriptor, new CultRecordKey("alpha"), alpha, 2) + }; + + var selection = new CultNetSelection + { + Cites = new CultNetCitation + { + Target = new CultNetRecordRef { SchemaId = descriptor.SchemaId, RecordKey = "alpha" }, + Role = "Peers" + }, + Cited = new CultNetIncoming { Role = "Peers", Exists = true } + }; + + var evaluation = CultNetSelectionEvaluator.Select(registry, rows, selection, asOf: 1); + + Assert.That(evaluation.Edges.Count, Is.EqualTo(2), "expected two edges in one page-position bucket"); + Assert.That(evaluation.Edges[0].From.Key.Value, Is.EqualTo(evaluation.Edges[1].From.Key.Value)); + Assert.That(evaluation.Edges[0].Role, Is.EqualTo(evaluation.Edges[1].Role)); + Assert.That(evaluation.Edges[0].To.Key.Value, Is.Not.EqualTo(evaluation.Edges[1].To.Key.Value), + "To must differ - if it does not, Hands' equivalence claim survives in C# too"); + Assert.That(evaluation.Edges[0].To.Key.Value, Is.EqualTo("alpha")); + Assert.That(evaluation.Edges[1].To.Key.Value, Is.EqualTo("zebra")); + } + // R-AM: HandleSnapshotRequestV1Async (CultNetDatabaseServer.cs) now answers an untyped fault // like its siblings HandleShardLogRequestAsync/HandlePutAsync - catch (Exception), log, try to // answer a CultNetErrorMessage - instead of only catching the three typed selection exceptions