Commit graph CultLib/tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs
Author SHA1 Message Date
Meta Vens
3b83cb6c08 Merge origin/main into hands/variants-c2a-main (main-into-c2a)
Sixteen conflicted files resolved by the spec's rule: main owns payload
ownership, bounds and the Clock seam; C2a owns persistence semantics and exact
binding. MintStoredAt is internal static (previous, now); the cache and the
store pass their cache's Clock. Package versions are main's. The cultmesh-ts
payload-parse acceptance and its test are deleted under retire-schema-alias;
peer snapshot reads take the schema's own id, else an id the definition lists
as compatible. The held firstRecordAtKey fallback is removed: main's handles
take definitions only, so nothing could reach it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:06:41 +02:00
Meta Vens
a44e189e0c C#: a peer's undecodable message ends that peer's session, never the poll
RudpCultNetSchemaServer decoded each schema frame outside its per-message
backstop, so a malformed payload (0xc1) or a well-formed message with an
unknown schemaVersion threw MessagePackSerializationException out of
PollAvailableAsync and PollOnceAsync. The drain aborted: a good peer's
request in the same drain went unserved, a consumer loop that did not
catch died, and the bad frame was already acknowledged while its peer
stayed connected.

Decoding now runs inside the backstop. A peer whose message cannot be
decoded or handled has its session ended: the transport's new
EndRefusedPeer sends the refusal goodbye under the gate, counts the drop
in PacketsDropped, drops that peer's frames still queued, forgets the
peer and raises PeerDisconnected. A session that already ended (replaced
by a new Connect, or ended by a send failure) gets no goodbye, since the
endpoint may belong to a new session. Other peers in the drain are still
served. Ending the session matches cultmesh-py's server, where one
peer's bad message ends that peer; runtime parity is the reason.

The log names the fault's type only and never its message, because the
decoder's message quotes the peer's input (the unknown schemaVersion).
The dispatch-backstop test now asserts that rule instead of the handler
message it used to require.

Tests, each failing on the pre-fix code: a malformed payload and an
unknown schemaVersion each leave the good peer served without Poll
throwing (also through PollOnceAsync); the bad peer's session ends,
counted and told; the log carries the type and not the canary value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 17:23:40 +02:00
Meta Vens
c99f29c309 Test: the selection guard probe passes the descriptor's new compatible-ids argument
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 11:48:38 +02:00
Meta Vens
22d0722d5a R-AT: fix the C# self-citer fixture's naming so it actually defeats push-order luck
The zebra/alpha pairing that kills Rust's drop-both-to-components mutant
does not kill it in C#: EvaluateAll concatenates citesEdges before
citedEdges (the opposite of cultnet_rs's single cited-then-cites Vec), so
that pairing's push order already matched the correct sort order here too
- just for the opposite reason. Swap to alpha (self) / zebra (target),
the pairing that actually disagrees with this runtime's own push order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:38:27 +02:00
Meta Vens
0bdcf6fd55 R-AT: add missing MessagePack using the C# self-citer fixture needs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:35:37 +02:00
Meta Vens
c9ba53cfe1 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 <noreply@anthropic.com>
2026-09-23 08:34:44 +02:00
Meta Vens
f01c0b6112 R-AL/R-AM/R-AO: C# regression tests for the fix batch
- R-AL: DataMemberNameCollidingWithAReferenceMembersAliasIsRefusedAtRegistration
  pins D10b's registration refusal. ReferenceMembersExcludesADataMember...
  pins the restored evaluator guard directly (hand-built descriptor via
  reflection, since D10b now closes the registry door this shape used to
  slip through).
- R-AM: CultNetDatabaseServer_SnapshotV1Handler_CatchesAnUntypedFault... and
  RudpCultNetSchemaServer_DispatchBackstop_LogsEveryUntypedHandlerFault pin
  the handler's catch and the backstop's logging, each against a real
  untyped fault.
- R-AO: Evaluator_CitedWithTwoInTargetCandidates_ResolvesToWhicheverThe-
  CallerListedFirst pins "first row in the caller's order wins" when two
  in-target rows share a key, in both row orders.

Every test in this commit was checked against a hand-reverted mutation of
its own production fix and confirmed to fail before being confirmed to pass
against the real fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:17:21 +02:00
Meta Vens
3c4f83b038 R-AG/R-AK: checked C# cursor-length arithmetic, a dispatch backstop, and the redundant guard
R-AG. ReadLengthPrefixedFields tested pos + length > bytes.Length in int
math; a length prefix near int.MaxValue overflowed the unchecked add,
slipped past the guard, and Encoding.UTF8.GetString threw
ArgumentOutOfRangeException instead of this method returning null the
way every other malformed shape already does. Promotes to long before
the add, which cannot itself overflow (both operands stay far under
long's range), so the guard sees the true sum.

CultNetRudpSchemaServer.DispatchAsync gains a catch around the handler
invocation: HandleSnapshotRequestV1Async already answers
cursor_invalid/selection_invalid/reference_outside_target for its three
typed exceptions, but nothing stopped an untyped one from an
unanticipated bug reaching DynamicInvoke's caller and stalling the poll
loop for every other peer. Answers nothing on the wire (an exception
this generic carries no typed refusal to report) and drops the one
malformed dispatch, the same as an unregistered handler already does.

Hostile-cursor tests (CultNetSelectionFixBatchTests.cs) feed
CultNetSelectionCursor.Parse a length prefix near int.MaxValue (C#'s own
proven overflow), near usize::MAX (Rust's proven overflow, fed here too
per the ruling), wider than any integer type, and negative-looking -
each asserted to throw the typed CultNetSelectionCursorException.

R-AK. Deletes ReferenceMembers's `!member.IsReference` guard.
CultCache.ReferencesOf (CultCache.cs:262-278) already searches only its
own reference members for a role match and returns empty for anything
else; index aliases are unique per descriptor, so a data member's own
role string can never coincide with a reference member's. Verified
before deleting: traced ReferencesOf's role lookup directly rather than
trusting the ruling's premise. A second copy of the same exclusion here
was duplicate authority, not a second decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 04:44:09 +02:00
Meta Vens
e99bf46520 Fix batch 4 (R-V/R-W/S-8/R-Y): row identity is (schemaId, recordKey), and the cursor digest covers its own body
R-V (S-1): byKey and BuildIncomingIndex keyed the hop's row resolution by a
bare record key. Two schemas sharing one key collapsed into a single
dictionary slot (last writer wins), so which row a reference/citation
resolved to - and therefore whether a selection matched or refused
reference_outside_target - depended on row insertion order. byKey is now a
bare-key -> row-list index, and a shared TryResolveReferenceTarget searches
each edge's declared leaf set to resolve the one row that actually belongs
to it, deterministically regardless of order (Soul's P2, tested both ways).
EdgesFor's page-row anchor index is keyed the same way for the same reason.

R-W (S-2): EnsureWithinDeclaredTarget's equivalent check (now folded into
TryResolveReferenceTarget) now runs for every edge a reference carries,
before a citation's own key/schema filters that edge out - not only for the
edge the citation happens to ask about. A many-reference edge into a schema
outside its declared target refuses the selection even when the caller's
key filter would otherwise skip past it without ever inspecting it (Soul's
P3).

S-8: the dead `targetType ?? typeof(object)` fallback is deleted. D11
already refuses every reference shape whose target the cache cannot infer
before it can reach the evaluator, so the branch had no reachable fixture.

R-Y: the cursor's HMAC now covers its own body (asOf, ordinal, schemaId,
recordKey) as well as the selection. Before this, a forged cursor could
reuse a legitimately minted digest while rewriting the ordinal or record
key underneath it, since nothing about the body was ever authenticated
(Soul's P4).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 21:11:54 +02:00
Meta Vens
a4cf54f4a9 Land fix batch 3's R-O and R-Q: keyed length-prefixed cursors, replica-apply sequence gap
R-O: CultNetSelectionCursorKey is a per-process (per-CultNetDatabase-instance) 32-byte
random key. CultNetSelectionCursor.ComputeDigest is now an HMAC-SHA256 under that key
instead of an unkeyed SHA-256, so a cursor cannot be forged without the key and does not
verify across a restart (a fresh CultNetDatabase mints a fresh key). The cursor body
(asOf/ordinal/schemaId/recordKey/digest) is length-prefixed (<byte length>:<value>, no
delimiter) instead of joined with a bare "\x01"-separated string, so a record key
carrying any character, U+241F included, round-trips. CultNetSelectionEvaluator.Select/
Page/ComputeDigest take an optional CultNetSelectionCursorKey, defaulting to a
process-wide key generated once so existing callers that never wire up a database still
get a keyed digest. CultNetDatabase gets a CursorKey property (random unless
CultNetDatabaseOptions.CursorKey is supplied) and passes it through
CultNetDatabaseServer.CreateSelectionResponse.

Rust already landed R-O (5aac120) under its own algorithm, deliberately not byte-matching
the reference: its own doc comment says a cursor is never handed across runtimes, so only
collision-freedom and keying are load-bearing, not the wire bytes. That is a real
divergence from this pass's instruction to match Rust's wire encoding exactly - reported
in full in the batch report.

R-Q: the two replica-apply paths (ApplyCommittedPutAsync, ApplyCommittedDelete) recorded
the mutation-log entry but never set _lastWriteSequence, so a row's order/cursor position
looked like "never written" until it was written again locally. Both now set it from the
entry's own sequence, matching AppendMutationLogEntry's local-commit path.
InitializeLogSequencesFromStore rebuilt only _nextLogSequences at startup; it now also
rebuilds _lastWriteSequence by replaying the durable log's Put/Delete entries in sequence
order. CultNetDatabase.ResolveShard is a new public wrapper over the existing shard
resolution; CultNetDocumentRegistry.SelectPage/CreateSelectionResponse take an optional
shardIdOf callback and refuse (reusing CultNetSelectionInvalidException/selection_invalid,
R-N) when a selection's matched rows resolve to more than one shard's log, because asOf is
a single watermark and is not exact for a shard-spanning page.
CultNetDatabaseServer.CreateSelectionResponse wires shardIdOf/cursorKey through from the
database.

Also fixes a defect the Rust Hands found while matching wire bytes:
CultNetErrorMessage.ForReferenceOutsideTarget double-prefixed "reference_outside_target: "
- the exception's own message already carries it. Now prefixed once, matching Rust's
ForReferenceOutsideTarget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:04:22 +02:00
Meta Vens
d94174d597 CultNet selection cut1 fix batch, second Hands pass: mutation entries and S9
- Adds mutation entries for R-A, R-B, R-C, R-E, R-F, R-G, R-H, R-J
  (scripts/mutate-cultnet-selection-networking.entries.ps1), each a revert
  and a loosening where the rule supports one, targeting
  CultNetSelectionEvaluator.cs, CultNetSelection.cs, CultNetDatabaseServer.cs
  and CultNetDatabaseSubscriptionServer.cs.
- Adds S9 (docs/cultnet-selection-cut.md section 10): a new RUDP round-trip
  test proving a hop-bearing v1 subscription reconciles the whole matched set
  when the citer (not the page row itself) changes, plus its own mutation
  entry (NET-S9-HopFastPath-Revert) reverting D6's Watch routing.
- Adds tests/GameCult.Networking.Tests/CultNetSelectionFixBatchTests.cs:
  regression coverage for R-B (edge anchor/order), R-C (code-point order in
  both the row tiebreak and the cursor position compare), R-E (alias
  matcher; door refusal), R-F (Select validates unconditionally), R-H
  (digest collision), R-J (numeric any_of canonical rendering) - none of
  these rules had a test that could fail independently of the fix-batch
  regression tests already in place.
- Fixes a real gap R-E named but the first fix-batch pass never wrote:
  CultNetSelection.Validate now refuses a cites.target.schemaId that
  matches no declared schema, at the door, instead of MatchesCitation
  silently treating an unresolved target as "not found" and answering an
  empty page. Resolved through CultNetSchemaAliasMatching, the reference
  rule, against every declared schema - not only the selection's own
  Schemas-filtered reachable set, which a second entry
  (NET-RE-DoorTargetReachableOnly-Loosening) proves: a cites target is a
  separate structural reference, not itself constrained by Schemas.
  Coordinator-assigned item; the shared parity fixture's
  refuses_unmatched_cites_target_schema vector (Rust side, another Hands)
  is not yet in selection-vectors.rs-written.json, so
  SelectionParityVectorTests.SelectionVectorsWrittenByRustDecodeAndEvaluateIdenticallyInTheReference
  does not yet exercise it - it will once that vector lands, unmodified
  by this commit.
- NetworkingTests.cs gains CultNetDatabaseServer_AnswersV1SnapshotRequestRespectingItsFields
  (R-A's loosening) and DatabaseSubscriptionServer_SubscribesV1AndReconcilesAHopBearingSelectionWhenTheCiterChanges
  (S9 / R-A's subscribe-listener revert).

Discrepancy: R-A's snapshot-request v1 listener registration
(CultNetDatabaseServer's _server.OnCultNet(_snapshotHandlerV1)) has no
mutation entry. CultNetDatabaseServer pairs only with Server (the
LiteNetLib/TCP-secured production construction site), and no existing test
in this suite - before or after this pass - drives Server's real
client/listener dispatch for CultNetDatabaseServer; every test calls
CreateSelectionResponse/CreateSnapshotResponse directly. That revert is
recorded as not yet reached in the entries file, not swapped for an easier
mutant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 13:11:21 +02:00