From 43d7e53efdce308c89823f86f3da7095dd38f6d3 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 00:20:04 +0200 Subject: [PATCH] Ack Cut 1 tests: three held frames, the Rust byte bound, reset reseeds A gap now holds three ordered frames so a watermark that advances one step per call fails; the reset test follows a second handshake at a different sequence so a watermark that survives the reset fails; Rust gains the 4 MiB hold bound in bytes. Co-Authored-By: Claude Sonnet 5.5 --- .../tests/test_rudp_ordered_delivery.py | 18 ++++-- packages/cultnet-rs/tests/cultnet.rs | 60 +++++++++++++++++-- .../test/rudp-ordered-delivery.test.ts | 17 ++++-- .../CultNetRudpOrderedDeliveryTests.cs | 17 ++++-- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/packages/cultnet-py/tests/test_rudp_ordered_delivery.py b/packages/cultnet-py/tests/test_rudp_ordered_delivery.py index f6b398bc..0417aeb0 100644 --- a/packages/cultnet-py/tests/test_rudp_ordered_delivery.py +++ b/packages/cultnet-py/tests/test_rudp_ordered_delivery.py @@ -10,7 +10,6 @@ import unittest from cultnet_py.transport import ( CultNetRudpPacket, - CultNetRudpPacketType, CultNetRudpSendOptions, CultNetRudpSession, CultNetRudpSessionOptions, @@ -40,11 +39,13 @@ class CultNetRudpOrderedDeliveryTests(unittest.TestCase): u = send(sender, "rel", "u", False) o2 = send(sender, "schema", "o2", True) o3 = send(sender, "schema", "o3", True) + o4 = send(sender, "schema", "o4", True) self.assertEqual(names(receiver.receive(o1, 1)), ["o1"]) self.assertEqual(names(receiver.receive(o2, 2)), [], "u is missing") self.assertEqual(names(receiver.receive(o3, 3)), [], "u is missing") - self.assertEqual(names(receiver.receive(u, 4)), ["u", "o2", "o3"]) + self.assertEqual(names(receiver.receive(o4, 4)), [], "u is missing") + self.assertEqual(names(receiver.receive(u, 5)), ["u", "o2", "o3", "o4"]) def test_two_ordered_channels_release_each_other_in_sequence_order(self) -> None: sender, receiver = handshake() @@ -93,16 +94,21 @@ class CultNetRudpOrderedDeliveryTests(unittest.TestCase): self.assertEqual(names(receiver.receive(s1, 3)), ["s1", "s2"]) self.assertEqual(names(receiver.receive(s2, 4)), []) - def test_reset_peer_state_forgets_held_frames_and_the_watermark(self) -> None: + def test_reset_peer_state_forgets_held_frames_and_reseeds_the_watermark_from_the_next_handshake(self) -> None: sender, receiver = handshake() - s1 = send(sender, "schema", "s1", True) + send(sender, "schema", "s1", True) s2 = send(sender, "schema", "s2", True) self.assertEqual(names(receiver.receive(s2, 1)), []) receiver.reset_peer_state() - receiver.receive(CultNetRudpPacket(CultNetRudpPacketType.ACCEPT, 410, 1, 0, 0, "control"), 2) + following = CultNetRudpSession(CultNetRudpSessionOptions(connection_id=410, initial_sequence=100)) + following.receive(receiver.accept_connect(following.create_connect(0), 2), 2) + d1 = send(following, "schema", "d1", True) + d2 = send(following, "schema", "d2", True) - self.assertEqual(names(receiver.receive(s1, 3)), ["s1"], "the held s2 died with the reset") + # The held s2 died with the reset, and the watermark follows the new peer. + self.assertEqual(names(receiver.receive(d2, 3)), []) + self.assertEqual(names(receiver.receive(d1, 4)), ["d1", "d2"]) if __name__ == "__main__": diff --git a/packages/cultnet-rs/tests/cultnet.rs b/packages/cultnet-rs/tests/cultnet.rs index ca760ebe..77d93441 100644 --- a/packages/cultnet-rs/tests/cultnet.rs +++ b/packages/cultnet-rs/tests/cultnet.rs @@ -2762,16 +2762,18 @@ fn rudp_ordered_frame_waits_for_a_gap_filled_by_another_channel() -> Result<()> let u = sender.send("media", b"u".to_vec(), rudp_send_options(false))?; let o2 = sender.send("schema", b"o2".to_vec(), rudp_send_options(true))?; let o3 = sender.send("schema", b"o3".to_vec(), rudp_send_options(true))?; + let o4 = sender.send("schema", b"o4".to_vec(), rudp_send_options(true))?; assert_eq!(rudp_delivered_names(&receiver.receive(&o1, 1)?.delivered), ["o1"]); // `u` is lost, so nothing behind it may be delivered. assert!(receiver.receive(&o2, 2)?.delivered.is_empty()); assert!(receiver.receive(&o3, 3)?.delivered.is_empty()); - // The other channel's packet fills the gap and releases the ordered frames - // in the same call. + assert!(receiver.receive(&o4, 4)?.delivered.is_empty()); + // The other channel's packet fills the gap and releases every ordered + // frame behind it in the same call. assert_eq!( - rudp_delivered_names(&receiver.receive(&u, 4)?.delivered), - ["u", "o2", "o3"] + rudp_delivered_names(&receiver.receive(&u, 5)?.delivered), + ["u", "o2", "o3", "o4"] ); Ok(()) } @@ -2826,6 +2828,30 @@ fn rudp_accept_seeds_the_watermark_of_the_connecting_side() -> Result<()> { Ok(()) } +#[test] +fn rudp_reset_forgets_held_frames_and_reseeds_the_watermark_from_the_next_handshake() -> Result<()> { + let (mut sender, mut receiver) = rudp_ordered_pair()?; + sender.send("schema", b"s1".to_vec(), rudp_send_options(true))?; + let s2 = sender.send("schema", b"s2".to_vec(), rudp_send_options(true))?; + assert!(receiver.receive(&s2, 1)?.delivered.is_empty()); + + receiver.reset_peer_state(); + let mut next = CultNetRudpSession::new(CultNetRudpSessionOptions { + connection_id: 410, + initial_sequence: 100, + ..CultNetRudpSessionOptions::default() + }); + let accept = receiver.accept_connect(&next.create_connect(0, Vec::new())?, 2, Vec::new())?; + next.receive(&accept, 2)?; + let d1 = next.send("schema", b"d1".to_vec(), rudp_send_options(true))?; + let d2 = next.send("schema", b"d2".to_vec(), rudp_send_options(true))?; + + // The held s2 died with the reset, and the watermark follows the new peer. + assert!(receiver.receive(&d2, 3)?.delivered.is_empty()); + assert_eq!(rudp_delivered_names(&receiver.receive(&d1, 4)?.delivered), ["d1", "d2"]); + Ok(()) +} + #[test] fn rudp_large_ordered_frame_without_a_gap_is_never_held_or_refused() -> Result<()> { let (mut sender, mut receiver) = rudp_ordered_pair()?; @@ -2851,6 +2877,32 @@ fn rudp_large_ordered_frame_without_a_gap_is_never_held_or_refused() -> Result<( Ok(()) } +#[test] +fn rudp_ordered_hold_buffer_is_bounded_in_bytes() -> Result<()> { + let (mut sender, mut receiver) = rudp_ordered_pair()?; + let withheld = sender.send("schema", b"first".to_vec(), rudp_send_options(true))?; + sender.receive(&receiver.create_ack_for(withheld.sequence), 1)?; + + // Four 1 MiB frames fit the 4 MiB hold; the fifth does not. + let mut refused_at = None; + for step in 0..8u32 { + let packet = sender.send("schema", vec![7u8; 1024 * 1024], rudp_send_options(true))?; + match receiver.receive(&packet, 2) { + Ok(result) => { + assert!(result.delivered.is_empty(), "the gap holds every frame"); + sender.receive(&receiver.create_ack_for(packet.sequence), 2)?; + } + Err(refusal) => { + assert!(refusal.to_string().contains("ordered hold buffer is full"), "{refusal}"); + refused_at = Some(step); + break; + } + } + } + assert_eq!(refused_at, Some(4)); + Ok(()) +} + #[test] fn rudp_far_ahead_sequence_cannot_starve_the_session() -> Result<()> { let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { diff --git a/packages/cultnet-ts/test/rudp-ordered-delivery.test.ts b/packages/cultnet-ts/test/rudp-ordered-delivery.test.ts index 4e6bc05a..519fc8aa 100644 --- a/packages/cultnet-ts/test/rudp-ordered-delivery.test.ts +++ b/packages/cultnet-ts/test/rudp-ordered-delivery.test.ts @@ -27,11 +27,13 @@ test("an ordered frame waits for a gap filled by another channel", () => { const u = send(sender, "rel", "u", false); const o2 = send(sender, "schema", "o2", true); const o3 = send(sender, "schema", "o3", true); + const o4 = send(sender, "schema", "o4", true); assert.deepEqual(names(receiver.receive(o1, 1)), ["o1"]); assert.deepEqual(names(receiver.receive(o2, 2)), [], "u is missing"); assert.deepEqual(names(receiver.receive(o3, 3)), [], "u is missing"); - assert.deepEqual(names(receiver.receive(u, 4)), ["u", "o2", "o3"]); + assert.deepEqual(names(receiver.receive(o4, 4)), [], "u is missing"); + assert.deepEqual(names(receiver.receive(u, 5)), ["u", "o2", "o3", "o4"]); }); test("two ordered channels release each other in sequence order", () => { @@ -90,14 +92,19 @@ test("a duplicate of a held frame is not delivered twice", () => { assert.deepEqual(names(receiver.receive(s2, 4)), []); }); -test("resetting peer state forgets held frames and the watermark", () => { +test("resetting peer state forgets held frames and reseeds the watermark from the next handshake", () => { const { client: sender, server: receiver } = handshake(); - const s1 = send(sender, "schema", "s1", true); + send(sender, "schema", "s1", true); const s2 = send(sender, "schema", "s2", true); assert.deepEqual(names(receiver.receive(s2, 1)), []); receiver.resetPeerState(); - receiver.receive({ packetType: "accept", connectionId: 410, sequence: 1, ack: 0, ackMask: 0, channelId: "control" }, 2); + const next = new CultNetRudpSession({ connectionId: 410, initialSequence: 100 }); + next.receive(receiver.acceptConnect(next.createConnect(0), 2), 2); + const d1 = send(next, "schema", "d1", true); + const d2 = send(next, "schema", "d2", true); - assert.deepEqual(names(receiver.receive(s1, 3)), ["s1"], "the held s2 died with the reset"); + // The held s2 died with the reset, and the watermark follows the new peer. + assert.deepEqual(names(receiver.receive(d2, 3)), []); + assert.deepEqual(names(receiver.receive(d1, 4)), ["d1", "d2"]); }); diff --git a/tests/GameCult.Networking.Tests/CultNetRudpOrderedDeliveryTests.cs b/tests/GameCult.Networking.Tests/CultNetRudpOrderedDeliveryTests.cs index 1bad16f2..aeca5bc4 100644 --- a/tests/GameCult.Networking.Tests/CultNetRudpOrderedDeliveryTests.cs +++ b/tests/GameCult.Networking.Tests/CultNetRudpOrderedDeliveryTests.cs @@ -38,11 +38,13 @@ namespace GameCult.Networking.Tests var u = Send(sender, "rel", "u", false); var o2 = Send(sender, "schema", "o2", true); var o3 = Send(sender, "schema", "o3", true); + var o4 = Send(sender, "schema", "o4", true); Assert.That(Names(receiver.Receive(o1, 1)), Is.EqualTo(new[] { "o1" })); Assert.That(Names(receiver.Receive(o2, 2)), Is.Empty, "u is missing"); Assert.That(Names(receiver.Receive(o3, 3)), Is.Empty, "u is missing"); - Assert.That(Names(receiver.Receive(u, 4)), Is.EqualTo(new[] { "u", "o2", "o3" })); + Assert.That(Names(receiver.Receive(o4, 4)), Is.Empty, "u is missing"); + Assert.That(Names(receiver.Receive(u, 5)), Is.EqualTo(new[] { "u", "o2", "o3", "o4" })); } [Test] @@ -111,17 +113,22 @@ namespace GameCult.Networking.Tests } [Test] - public void ResetPeerStateForgetsHeldFramesAndTheWatermark() + public void ResetPeerStateForgetsHeldFramesAndReseedsTheWatermarkFromTheNextHandshake() { var (sender, receiver) = Handshake(); - var s1 = Send(sender, "schema", "s1", true); + Send(sender, "schema", "s1", true); var s2 = Send(sender, "schema", "s2", true); Assert.That(Names(receiver.Receive(s2, 1)), Is.Empty); receiver.ResetPeerState(); - receiver.Receive(new CultNetRudpPacket { PacketType = CultNetRudpPacketType.Accept, ConnectionId = 410, Sequence = 1, ChannelId = "control" }, 2); + var next = new CultNetRudpSession(new CultNetRudpSessionOptions { ConnectionId = 410, InitialSequence = 100 }); + next.Receive(receiver.AcceptConnect(next.CreateConnect(0), 2), 2); + var d1 = Send(next, "schema", "d1", true); + var d2 = Send(next, "schema", "d2", true); - Assert.That(Names(receiver.Receive(s1, 3)), Is.EqualTo(new[] { "s1" }), "the held s2 died with the reset"); + // The held s2 died with the reset, and the watermark follows the new peer. + Assert.That(Names(receiver.Receive(d2, 3)), Is.Empty); + Assert.That(Names(receiver.Receive(d1, 4)), Is.EqualTo(new[] { "d1", "d2" })); } } }