From 0da08c8ea9487b548eeabf4243b4d3a8656a947d Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 5 Nov 2026 12:20:10 +0000 Subject: [PATCH 1/2] test(e2e): bind test nodes to port 0 instead of a random fixed port Test nodes bound 127.0.0.1 on a random port in 20000-60000, which overlaps the OS ephemeral range. Every node and client starts a UPnP gateway search that holds a 0.0.0.0: UDP socket for up to 2 s, and a 127.0.0.1 bind to a port a wildcard socket holds fails with "address in use", so node creation now and then failed with "Failed to create transport". ant-client's harness had the same race (ant-client#248, V2-1852), where it was reproduced. The e2e harness now binds every node to port 0 and bootstraps later nodes from the addresses the bootstrap nodes actually bound (listen_addrs()). The OS never assigns a port that is in use or reserved, so the two retries that covered the race go: - start_node's 4 attempts on the same port (they fell inside one 2 s search window, so they rarely helped); - start_network's bring-up on a fresh port range (with_fresh_ports), which also covered Windows runners' reserved port ranges. base_port, TEST_PORT_RANGE_MIN/MAX, the port-overflow check and TestNode::port go with them. The retry's test is replaced by one that checks every node reports a distinct, non-zero port. The node.rs unit tests that built a node on a random port in the same range (two with retries, three without) now use port 0, which NodeConfig documents as auto-select. Co-Authored-By: Claude Opus 5.5 --- src/node.rs | 56 +++------ tests/e2e/harness.rs | 48 ++------ tests/e2e/integration_tests.rs | 42 ++++--- tests/e2e/testnet.rs | 215 +++++++++------------------------ 4 files changed, 107 insertions(+), 254 deletions(-) diff --git a/src/node.rs b/src/node.rs index 390dd84c..1377e72f 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1165,25 +1165,22 @@ fn jittered_interval(base: std::time::Duration) -> std::time::Duration { #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { - use rand::Rng; use tempfile::TempDir; use crate::storage::legacy_artifacts::LEGACY_ENV_DIR; - /// The e2e port range, so a test bind never lands on a production or dev instance. - const TEST_PORT_RANGE: std::ops::Range = 20000..60000; - - /// How many times a bind is retried before the failure is treated as real. - const BIND_ATTEMPTS: u32 = 5; - /// A well-formed address that receives nothing; no chain is contacted in these tests. const TEST_REWARDS_ADDRESS: &str = "0x0000000000000000000000000000000000000001"; /// A node config that builds without touching a chain or a real network. - fn local_node_config(root: &std::path::Path, port: u16) -> NodeConfig { + /// + /// Port 0: the OS assigns a free port, so a build never fails on a port that + /// is in use (V2-1894). These tests used to pick a random one in 20000-60000, + /// which overlaps the OS ephemeral range. + fn local_node_config(root: &std::path::Path) -> NodeConfig { NodeConfig { root_dir: root.to_path_buf(), - port, + port: 0, ipv4_only: true, network_mode: NetworkMode::Development, payment: crate::config::PaymentConfig { @@ -1205,7 +1202,7 @@ mod tests { let occupied = tokio::net::UdpSocket::bind("127.0.0.1:0") .await .expect("bind"); - let mut config = local_node_config(&root, 0); + let mut config = local_node_config(&root); config.webrtc_direct.enabled = true; config.webrtc_direct.bind = occupied.local_addr().expect("address"); let mut node = NodeBuilder::new(config).build().await.expect("build"); @@ -1225,27 +1222,10 @@ mod tests { let root = dir.path().join("node"); std::fs::create_dir_all(&root).expect("mkdir"); - let mut built = None; - let mut last_err = String::new(); - for _ in 0..BIND_ATTEMPTS { - let port = rand::thread_rng().gen_range(TEST_PORT_RANGE); - match NodeBuilder::new(local_node_config(&root, port)) - .build() - .await - { - Ok(node) => { - built = Some(node); - break; - } - Err(e) => { - last_err = e.to_string(); - tokio::time::sleep(std::time::Duration::from_millis(250)).await; - } - } - } - let Some(node) = built else { - panic!("could not build a node after {BIND_ATTEMPTS} attempts: {last_err}"); - }; + let node = NodeBuilder::new(local_node_config(&root)) + .build() + .await + .expect("a node builds on a clean root"); node.shutdown.cancel(); } @@ -1274,8 +1254,7 @@ mod tests { std::fs::write(env.join("data.mdb"), b"chunks that were never copied out") .expect("seed"); - let port = rand::thread_rng().gen_range(TEST_PORT_RANGE); - let mut config = local_node_config(&root, port); + let mut config = local_node_config(&root); config.storage.enabled = storage_enabled; let node = NodeBuilder::new(config).build().await.unwrap_or_else(|e| { @@ -1313,8 +1292,7 @@ mod tests { .expect("seed"); std::fs::write(env.join("RETIRED"), b"retired").expect("mark"); - let port = rand::thread_rng().gen_range(TEST_PORT_RANGE); - let mut config = local_node_config(&root, port); + let mut config = local_node_config(&root); config.storage.enabled = false; let node = NodeBuilder::new(config).build().await.expect("must start"); settle(); @@ -1340,8 +1318,7 @@ mod tests { .expect("seed"); std::fs::write(env.join("RETIRED"), b"retired").expect("mark"); - let port = rand::thread_rng().gen_range(TEST_PORT_RANGE); - let node = NodeBuilder::new(local_node_config(&root, port)) + let node = NodeBuilder::new(local_node_config(&root)) .build() .await .expect("a node with a finished leftover must start"); @@ -1371,10 +1348,7 @@ mod tests { // A file where the chunk directory has to be, so the store cannot create it. std::fs::write(root.join("chunks"), b"not a directory").expect("block the store"); - let port = rand::thread_rng().gen_range(TEST_PORT_RANGE); - let built = NodeBuilder::new(local_node_config(&root, port)) - .build() - .await; + let built = NodeBuilder::new(local_node_config(&root)).build().await; assert!( built.is_err(), "the file store was supposed to fail to open" diff --git a/tests/e2e/harness.rs b/tests/e2e/harness.rs index 971d1d13..3a4ded43 100644 --- a/tests/e2e/harness.rs +++ b/tests/e2e/harness.rs @@ -4,23 +4,14 @@ //! both the ant node network and optional Anvil EVM testnet. use super::anvil::TestAnvil; -use super::testnet::{TestNetwork, TestNetworkConfig, TestNode, TestnetError}; +use super::testnet::{TestNetwork, TestNetworkConfig, TestNode}; use ant_node::client::XorName; use ant_node::storage::DiskSpace; use evmlib::common::TxHash; use saorsa_core::P2PNode; use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use tracing::{info, warn}; - -/// How many port ranges a network is brought up on before its bind failure -/// is reported. -/// -/// A node retries its own port, which clears a socket still being released. -/// It cannot clear a port the host will not hand out at all, as happens on -/// Windows runners whose reserved port ranges move between runs, so a network -/// that cannot create a node moves to a fresh random range instead. -const NETWORK_START_ATTEMPTS: u32 = 3; +use tracing::info; /// Size of the partition the prune tests report: 1 TiB. const TEST_DISK_BYTES: u64 = 1 << 40; @@ -36,32 +27,15 @@ pub const ROOMY_DISK: DiskSpace = DiskSpace { total: TEST_DISK_BYTES, }; -/// Create and start a network from `config`, moving it to a fresh port range -/// if a node cannot be created. -async fn start_network(mut config: TestNetworkConfig) -> Result { - let mut attempt = 1; - loop { - let mut network = TestNetwork::new(config.clone()).await?; - match network.start().await { - Ok(()) => return Ok(network), - Err(TestnetError::Startup(reason)) - if attempt < NETWORK_START_ATTEMPTS - && reason.starts_with("Failed to create node ") => - { - let base_port = config.base_port; - warn!( - "Test network on base port {base_port} could not create a node \ - ({reason}); retrying on a fresh port range" - ); - if let Err(e) = network.shutdown().await { - warn!("Cleanup after a failed bring-up failed: {e}"); - } - config = config.with_fresh_ports(); - attempt += 1; - } - Err(e) => return Err(e.into()), - } - } +/// Create and start a network from `config`. +/// +/// Its nodes bind OS-assigned ports, so a bring-up never fails on a port that +/// is in use or reserved. This used to retry the whole network on a fresh port +/// range when a node could not be created (V2-1894). +async fn start_network(config: TestNetworkConfig) -> Result { + let mut network = TestNetwork::new(config).await?; + network.start().await?; + Ok(network) } /// Error type for test harness operations. diff --git a/tests/e2e/integration_tests.rs b/tests/e2e/integration_tests.rs index 18f8612a..020d4e81 100644 --- a/tests/e2e/integration_tests.rs +++ b/tests/e2e/integration_tests.rs @@ -6,12 +6,11 @@ use super::testnet::{ DEFAULT_BOOTSTRAP_COUNT, DEFAULT_NODE_COUNT, MINIMAL_BOOTSTRAP_COUNT, MINIMAL_NODE_COUNT, - SMALL_NODE_COUNT, TEST_PORT_RANGE_MAX, TEST_PORT_RANGE_MIN, + SMALL_NODE_COUNT, }; use super::{NetworkState, TestHarness, TestNetwork, TestNetworkConfig}; use saorsa_core::P2PEvent; use serial_test::serial; -use std::net::UdpSocket; use std::time::Duration; /// Test that a minimal network (5 nodes) can form and stabilize. @@ -172,8 +171,6 @@ fn test_config_presets() { let default = TestNetworkConfig::default(); assert_eq!(default.node_count, DEFAULT_NODE_COUNT); assert_eq!(default.bootstrap_count, DEFAULT_BOOTSTRAP_COUNT); - // Ports are randomly generated in a wide range to avoid collisions - assert!(default.base_port >= TEST_PORT_RANGE_MIN && default.base_port < TEST_PORT_RANGE_MAX); let minimal = TestNetworkConfig::minimal(); assert_eq!(minimal.node_count, MINIMAL_NODE_COUNT); @@ -278,23 +275,32 @@ async fn test_node_to_node_messaging() { .expect("Failed to teardown test harness"); } -/// A network whose first node cannot bind its port is brought up on a fresh -/// port range rather than failing the test that asked for it. +/// Test nodes bind port 0, and every node reports the port the OS assigned it: +/// later nodes bootstrap from those addresses, so a node reporting port 0, or +/// two nodes sharing a port, would break the network (V2-1894). #[tokio::test] #[serial] -async fn a_network_whose_port_is_taken_moves_to_a_fresh_range() { - let config = TestNetworkConfig::minimal(); - let taken = config.base_port; - let _holder = UdpSocket::bind(("127.0.0.1", taken)).expect("hold the first node's port"); - - let harness = TestHarness::setup_with_config(config) +async fn nodes_listen_on_distinct_os_assigned_ports() { + let harness = TestHarness::setup_with_config(TestNetworkConfig::minimal()) .await - .expect("the network comes up on another range"); - assert_ne!( - harness.network().config().base_port, - taken, - "the network moved off the port it could not bind" - ); + .expect("setup"); + + let mut ports = Vec::new(); + for node in harness.network().nodes() { + let p2p = node.p2p_node.as_ref().expect("node is running"); + let port = p2p + .listen_addrs() + .await + .iter() + .find_map(saorsa_core::MultiAddr::port) + .expect("node has a listen port"); + assert_ne!(port, 0, "node {} reports port 0", node.index); + ports.push(port); + } + let mut distinct = ports.clone(); + distinct.sort_unstable(); + distinct.dedup(); + assert_eq!(distinct.len(), ports.len(), "nodes share a port: {ports:?}"); harness.teardown().await.expect("teardown"); } diff --git a/tests/e2e/testnet.rs b/tests/e2e/testnet.rs index c036211a..e518302a 100644 --- a/tests/e2e/testnet.rs +++ b/tests/e2e/testnet.rs @@ -38,7 +38,6 @@ use saorsa_core::{ NodeConfig as CoreNodeConfig, P2PEvent, P2PNode, }; use std::collections::HashMap; -use std::net::{Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -52,25 +51,21 @@ use tracing::{debug, info, warn}; // Test Isolation Constants // ============================================================================= // -// NOTE: E2E tests use a SEPARATE port range from production ant-node. -// -// - Production ant-node: 10000-10999 (see CLAUDE.md) -// - E2E tests: 20000-60000 (this file) -// -// This separation prevents test conflicts with: -// 1. Running local development nodes (which use 10000-10999) -// 2. Parallel test execution (via random port allocation) +// NOTE: E2E test nodes bind port 0, so the OS assigns each one a free port +// from its ephemeral range (by default Linux 32768-60999, macOS and Windows +// 49152-65535). That keeps them clear of: +// 1. Running local development nodes (which use 10000-10999, see CLAUDE.md) +// 2. Parallel test execution // 3. Other Autonomi services (ant-quic: 9000-9999, communitas: 11000-11999) - -/// Minimum port for random test allocation. -/// Avoids well-known ports, production ranges, and other Autonomi services. -pub const TEST_PORT_RANGE_MIN: u16 = 20_000; - -/// Maximum port for random test allocation. -pub const TEST_PORT_RANGE_MAX: u16 = 60_000; +// +// The harness used to pick a random port in 20000-60000 for each node, which +// overlaps the ephemeral range. Every node and client starts a UPnP gateway +// search that holds a `0.0.0.0:` UDP socket for up to 2 s, and a +// 127.0.0.1 bind to a port a wildcard socket holds fails with "address in +// use", so node creation now and then failed with "Failed to create +// transport" (V2-1894). The OS never assigns a port that is in use. /// Maximum nodes supported in a test network. -/// Limited to ensure port calculations don't overflow u16. pub const MAX_TEST_NODE_COUNT: usize = 1000; // ============================================================================= @@ -195,16 +190,14 @@ pub type Result = std::result::Result; /// Configuration for the test network. /// -/// Each configuration is automatically isolated with unique ports and -/// data directories to prevent test pollution when running in parallel. +/// Each configuration is automatically isolated with a unique data directory, +/// and its nodes bind OS-assigned ports, to prevent test pollution when +/// running in parallel. #[derive(Debug, Clone)] pub struct TestNetworkConfig { /// Number of nodes to spawn (default: 25). pub node_count: usize, - /// Base port for node allocation (auto-generated for isolation). - pub base_port: u16, - /// Number of bootstrap nodes (first N nodes, default: 3). pub bootstrap_count: usize, @@ -245,43 +238,14 @@ pub struct TestNetworkConfig { pub storage_disk_reserve_overrides: HashMap, } -impl TestNetworkConfig { - /// The same configuration on a freshly drawn port range and data - /// directory, for a network whose first bring-up could not bind. - #[must_use] - pub fn with_fresh_ports(self) -> Self { - let fresh = Self::default(); - Self { - base_port: fresh.base_port, - test_data_dir: fresh.test_data_dir, - ..self - } - } -} - impl Default for TestNetworkConfig { fn default() -> Self { - let mut rng = rand::thread_rng(); - - // Random port in isolated range to avoid collisions in parallel tests. - // Ensure we have room for DEFAULT_NODE_COUNT consecutive ports. - // Calculation: base_port + (DEFAULT_NODE_COUNT - 1) must be < TEST_PORT_RANGE_MAX - // Safety: DEFAULT_NODE_COUNT (25) fits in u16. - #[allow(clippy::cast_possible_truncation)] - let max_base_port = TEST_PORT_RANGE_MAX.saturating_sub(DEFAULT_NODE_COUNT as u16); - let base_port = if max_base_port > TEST_PORT_RANGE_MIN { - rng.gen_range(TEST_PORT_RANGE_MIN..max_base_port) - } else { - TEST_PORT_RANGE_MIN - }; - // Random suffix for unique temp directory - let suffix: u64 = rng.gen(); + let suffix: u64 = rand::thread_rng().gen(); let test_data_dir = std::env::temp_dir().join(format!("ant_test_{suffix:x}")); Self { node_count: DEFAULT_NODE_COUNT, - base_port, bootstrap_count: DEFAULT_BOOTSTRAP_COUNT, test_data_dir, spawn_delay: Duration::from_millis(DEFAULT_SPAWN_DELAY_MS), @@ -409,9 +373,6 @@ pub struct TestNode { /// Unique node ID. pub node_id: String, - /// Port this node listens on. - pub port: u16, - /// Root directory for this node's data. pub data_dir: PathBuf, @@ -959,7 +920,6 @@ impl TestNetwork { )); } - // Validate node count fits in u16 for port calculations if config.node_count > MAX_TEST_NODE_COUNT { return Err(TestnetError::Config(format!( "Node count {} exceeds maximum {}", @@ -967,25 +927,6 @@ impl TestNetwork { ))); } - // Validate port range doesn't overflow - let node_count_u16 = u16::try_from(config.node_count).map_err(|_| { - TestnetError::Config(format!("Node count {} exceeds u16::MAX", config.node_count)) - })?; - let max_port = config - .base_port - .checked_add(node_count_u16) - .ok_or_else(|| { - TestnetError::Config(format!( - "Port range overflow: base_port {} + node_count {} exceeds u16::MAX", - config.base_port, config.node_count - )) - })?; - if max_port > TEST_PORT_RANGE_MAX { - return Err(TestnetError::Config(format!( - "Port range overflow: max port {max_port} exceeds TEST_PORT_RANGE_MAX {TEST_PORT_RANGE_MAX}" - ))); - } - // Ensure test data directory exists tokio::fs::create_dir_all(&config.test_data_dir).await?; @@ -1065,7 +1006,7 @@ impl TestNetwork { let node = self.create_node(i, true, vec![]).await?; self.start_node(node).await?; - // Delay between spawns to prevent port conflicts + // Delay between spawns tokio::time::sleep(self.config.spawn_delay).await; } @@ -1082,13 +1023,7 @@ impl TestNetwork { let regular_count = self.config.node_count - self.config.bootstrap_count; info!("Starting {} regular nodes", regular_count); - let bootstrap_addrs: Vec = self - .nodes - .get(0..self.config.bootstrap_count) - .unwrap_or_default() - .iter() - .map(|n| MultiAddr::quic(SocketAddr::from((Ipv4Addr::LOCALHOST, n.port)))) - .collect(); + let bootstrap_addrs = self.bootstrap_addrs().await; for i in self.config.bootstrap_count..self.config.node_count { let node = self.create_node(i, false, bootstrap_addrs.clone()).await?; @@ -1102,6 +1037,22 @@ impl TestNetwork { Ok(()) } + /// The addresses the running bootstrap nodes bound. Nodes bind port 0, so + /// these come from each node's `listen_addrs()`, not from the config. + async fn bootstrap_addrs(&self) -> Vec { + let mut addrs = Vec::new(); + for node in self + .nodes + .get(0..self.config.bootstrap_count) + .unwrap_or_default() + { + if let Some(ref p2p) = node.p2p_node { + addrs.extend(p2p.listen_addrs().await); + } + } + addrs + } + /// Create a test node (but don't start it yet). /// /// Initializes the `AntProtocol` handler with: @@ -1114,10 +1065,6 @@ impl TestNetwork { is_bootstrap: bool, bootstrap_addrs: Vec, ) -> Result { - // Safe: node_count is validated in TestNetwork::new() to fit in u16 - let index_u16 = u16::try_from(index) - .map_err(|_| TestnetError::Config(format!("Node index {index} exceeds u16::MAX")))?; - let port = self.config.base_port + index_u16; let node_id = format!("test_node_{index}"); let data_dir = self.config.test_data_dir.join(&node_id); @@ -1151,7 +1098,6 @@ impl TestNetwork { Ok(TestNode { index, node_id, - port, data_dir, p2p_node: None, ant_protocol: Some(Arc::new(ant_protocol)), @@ -1272,74 +1218,35 @@ impl TestNetwork { /// Start a single node. #[allow(clippy::too_many_lines)] async fn start_node(&mut self, mut node: TestNode) -> Result<()> { - debug!("Starting node {} on port {}", node.index, node.port); + debug!("Starting node {}", node.index); *node.state.write().await = NodeState::Starting; - // Retry budget for a transient transport/port-bind race at node - // creation (a `let`, not an item, to avoid clippy's items-after- - // statements in this scope). See the loop below for the rationale. - let node_create_attempts: u32 = 4; - - // Build the saorsa-core config (a fresh one per creation attempt, since - // P2PNode::new consumes it). Factored into a closure so the transient- - // error retry below can rebuild it. + // .port(0): the OS assigns a free port (see the note at the top of this + // file); `bootstrap_addrs` reads it back from `listen_addrs()`. // .local(true) auto-enables allow_loopback for test nodes on 127.0.0.1. // .ipv6(false) keeps the bind on 127.0.0.1 only; Windows GitHub // Actions runners (and some macOS runners) can't bind a dual-stack // v6 socket and the whole testnet startup fails with // "Failed to create dual-stack network nodes: Failed to create transport". // Loopback-only tests don't need v6. - let build_core_config = || -> Result { - let mut core_config = CoreNodeConfig::builder() - .port(node.port) - .ipv6(false) - .local(true) - .connection_timeout(Duration::from_secs(TEST_CORE_CONNECTION_TIMEOUT_SECS)) - .max_message_size(MAX_REPLICATION_MESSAGE_SIZE.max(MAX_WIRE_MESSAGE_SIZE)) - .build() - .map_err(|e| TestnetError::Core(format!("Failed to create core config: {e}")))?; - core_config - .bootstrap_peers - .clone_from(&node.bootstrap_addrs); - core_config.diversity_config = Some(CoreDiversityConfig::permissive()); - // Inject the ML-DSA identity so the P2PNode's transport peer ID - // matches the pub_key embedded in payment quotes. - core_config.node_identity.clone_from(&node.node_identity); - Ok(core_config) - }; - - // Create the P2P node, retrying a transient transport/port-bind failure. - // On Windows (and occasionally macOS) GitHub runners, node creation can - // intermittently fail with "Failed to create transport" when the freshly - // chosen loopback port is momentarily still held by a just-released - // socket — a flaky-setup race, not a logic error. A few retries with a - // short backoff (same port; the race clears in milliseconds) makes the - // multi-node bring-up deterministic without masking a real transport - // misconfiguration (which fails every attempt). - let mut p2p_node = None; - let mut last_err = String::new(); - for attempt in 1..=node_create_attempts { - match P2PNode::new(build_core_config()?).await { - Ok(n) => { - p2p_node = Some(n); - break; - } - Err(e) => { - last_err = e.to_string(); - debug!( - "Node {} creation attempt {attempt}/{node_create_attempts} failed \ - (transient transport/port race?): {last_err}", - node.index - ); - tokio::time::sleep(Duration::from_millis(250 * u64::from(attempt))).await; - } - } - } - let p2p_node = p2p_node.ok_or_else(|| { - TestnetError::Startup(format!( - "Failed to create node {} after {node_create_attempts} attempts: {last_err}", - node.index - )) + let mut core_config = CoreNodeConfig::builder() + .port(0) + .ipv6(false) + .local(true) + .connection_timeout(Duration::from_secs(TEST_CORE_CONNECTION_TIMEOUT_SECS)) + .max_message_size(MAX_REPLICATION_MESSAGE_SIZE.max(MAX_WIRE_MESSAGE_SIZE)) + .build() + .map_err(|e| TestnetError::Core(format!("Failed to create core config: {e}")))?; + core_config + .bootstrap_peers + .clone_from(&node.bootstrap_addrs); + core_config.diversity_config = Some(CoreDiversityConfig::permissive()); + // Inject the ML-DSA identity so the P2PNode's transport peer ID + // matches the pub_key embedded in payment quotes. + core_config.node_identity.clone_from(&node.node_identity); + + let p2p_node = P2PNode::new(core_config).await.map_err(|e| { + TestnetError::Startup(format!("Failed to create node {}: {e}", node.index)) })?; p2p_node.start().await.map_err(|e| { @@ -1804,13 +1711,7 @@ impl TestNetwork { let index = self.nodes.len(); - let bootstrap_addrs: Vec = self - .nodes - .get(0..self.config.bootstrap_count) - .unwrap_or_default() - .iter() - .map(|n| MultiAddr::quic(SocketAddr::from((Ipv4Addr::LOCALHOST, n.port)))) - .collect(); + let bootstrap_addrs = self.bootstrap_addrs().await; let node = self.create_node(index, false, bootstrap_addrs).await?; self.start_node(node).await?; @@ -1950,8 +1851,6 @@ mod tests { let config = TestNetworkConfig::default(); assert_eq!(config.node_count, 25); assert_eq!(config.bootstrap_count, 3); - // Port is randomly generated in range 20000-60000 - assert!(config.base_port >= 20000 && config.base_port < 60000); // Data dir has unique suffix assert!(config.test_data_dir.to_string_lossy().contains("ant_test_")); } @@ -1965,7 +1864,7 @@ mod tests { #[test] fn test_config_isolation() { - // Each config should get unique port and data dir + // Each config should get a unique data dir let config1 = TestNetworkConfig::default(); let config2 = TestNetworkConfig::default(); From dc16ddabb90dc6c57d49577f35fda3392fe65db3 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Thu, 5 Nov 2026 12:20:57 +0000 Subject: [PATCH 2/2] docs(claude): e2e test nodes use OS-assigned ports, not 20000-60000 Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a126dcf4..3eea5642 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,9 +77,9 @@ This includes: | ant-quic | 9000-9999 | 9000 | QUIC transport layer | | **ant-node** | **10000-10999** | **10000** | Core P2P network nodes (THIS PROJECT) | | communitas | 11000-11999 | 11000 | Collaboration platform nodes | -| **ant-node tests** | **20000-60000** | **random** | E2E test isolation (local only) | +| **ant-node tests** | **OS-assigned** | **port 0** | E2E test isolation (local only) | -**Note:** The E2E test suite uses ports 20000-60000 with random allocation to prevent conflicts between parallel test runs and local development instances. Production deployments MUST use 10000-10999. +**Note:** E2E test nodes bind port 0, so the OS assigns each one a free port from its ephemeral range (by default Linux 32768-60999, macOS and Windows 49152-65535). That keeps parallel test runs apart and clear of local development instances. Production deployments MUST use 10000-10999. ### DO NOT DISTURB OTHER NETWORKS