diff --git a/tines-sim/tests/relay.rs b/tines-sim/tests/relay.rs index 2306023a..67387ff0 100644 --- a/tines-sim/tests/relay.rs +++ b/tines-sim/tests/relay.rs @@ -5,6 +5,14 @@ //! carriers no longer need once they have forwarded it. These tests exercise //! the relay path — an [`Sim::observe_to`] observation handed off hop-by-hop //! along a contact chain — without asserting whole-graph convergence. +//! +//! * [`custody_chain_delivers_without_carrier_failure`] — an addressed task +//! relayed hop-by-hop along an A→B→C chain reaches the recipient, and the +//! custody chain releases at every hop: a carrier that gets an ack stops +//! forwarding the id. +//! * [`released_duplicate_not_acked`] — a carrier that released custody on the +//! recipient's ack reconnects with the recipient without pulling the +//! delivered task back out of custody, so the message never vanishes. use tines_core::{NodeId, Store}; use tines_sim::{Contact, ContactTrace, Sim, WorldConfig}; @@ -79,3 +87,108 @@ fn carrier_drops_expired() { "an addressed task past its lifetime must not survive at the recipient" ); } + +/// An addressed task relayed hop-by-hop along a connected A→B→C contact chain +/// reaches the far end, and the custody chain releases at every hop: a carrier +/// that receives an ack stops forwarding the id. Neither hop fails — the +/// message lands with the recipient and no carrier keeps holding a copy it has +/// already handed off (the "carrier failure" the custody chain prevents). +/// +/// The trace is the shared relay trace: `(0,1)` during `[0, 5000)`, then +/// `(1,2)` during `[10_000, 15_000)`, so the task must hop through node 1. +#[test] +fn custody_chain_delivers_without_carrier_failure() { + let trace = relay_trace(); + let end = trace.end(); + let cfg = WorldConfig { + nodes: 3, + mtu: 1500, + loss: 0.0, + dup: 0.0, + seed: 0, + }; + let mut sim = Sim::new(cfg, trace); + // Node 0 addresses the task to node 2, the far end of the chain. + let id = sim.observe_to(0, RECIPIENT, 600_000, b"task", b"investigate"); + // Relay it down the chain (0→1 in the first window, 1→2 in the second) + // and run a little past the end so node 1 can collect node 2's ack and + // release before the last contact closes. + sim.run_until(end + 2_000); + + // The recipient holds the delivered task, un-released. + let c = sim.nodes[2].store(); + assert!( + c.contains(&id) && !c.is_released(&id), + "node 2 must hold the addressed task after the relay" + ); + + // Every carrier that relayed it still holds the copy but has released it + // on the ack, so it no longer forwards the id (a released store is not + // re-offered). + for carrier in &sim.nodes[0..2] { + let s = carrier.store(); + assert!( + s.contains(&id) && s.is_released(&id), + "carrier node {} must still hold the task but released", + carrier.id()[0] + ); + } +} + +/// A carrier and its recipient both hold an addressed task mid-path; the +/// carrier releases custody once the recipient acks (the custody chain). When +/// the pair reconnects, the delivered task must not be lost: the recipient +/// keeps it, so custody never vanishes ("released-with-nobody-holding"). +/// +/// Two A↔B windows let the same pair reconnect after the first exchange: the +/// release happened over the first contact, the reconnect over the second. +#[test] +fn released_duplicate_not_acked() { + let mut trace = ContactTrace::default(); + trace.contacts.push(Contact { + start_ms: 0, + end_ms: 5_000, + a: 0, + b: 1, + }); + trace.contacts.push(Contact { + start_ms: 10_000, + end_ms: 15_000, + a: 0, + b: 1, + }); + let end = trace.end(); + let cfg = WorldConfig { + nodes: 2, + mtu: 1500, + loss: 0.0, + dup: 0.0, + seed: 0, + }; + let mut sim = Sim::new(cfg, trace); + // Node 0 addresses a task to node 1. + let id = sim.observe_to(0, RECIPIENT, 600_000, b"task", b"investigate"); + // First window: node 1 receives it and acks, so node 0 releases custody on + // the ack and node 1 is left holding the delivered task. + sim.run_until(5_000); + assert!( + sim.nodes[0].store().is_released(&id), + "the carrier must release custody on the recipient's ack" + ); + assert!( + sim.nodes[1].store().contains(&id), + "the recipient must hold the task after delivery" + ); + + // Reconnect the pair and run the second window. + sim.run_until(end + 2_000); + + // The task must stay with the recipient: reconnecting a released carrier + // must not pull the delivered copy back out of custody, so at least one + // node still holds it. + let r = sim.nodes[1].store(); + assert!( + r.contains(&id) && !r.is_released(&id), + "the recipient must keep the delivered task after reconnecting" + ); +}