diff --git a/Cargo.lock b/Cargo.lock
index d37a442e..3c28aa44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -470,6 +470,7 @@ dependencies = [
"async-stream",
"async-trait",
"beyond-ai-providers",
+ "beyond-ai-test-support",
"bytes",
"divan",
"futures",
diff --git a/crates/agent-core/Cargo.toml b/crates/agent-core/Cargo.toml
index 28729f1f..2bc21ae3 100644
--- a/crates/agent-core/Cargo.toml
+++ b/crates/agent-core/Cargo.toml
@@ -91,6 +91,8 @@ rustls = { version = "0.23", default-features = false, features = ["ring"] }
zstd = "0.13"
[dev-dependencies]
+# Loopback listeners for the socket tests (`ports`): the one place a test binds port 0.
+beyond-ai-test-support = { path = "../test-support" }
# Bench harness for the streaming decode hot path. `divan` (the workspace's `bench:unit` pin, matching
# gateway/agent) measures **allocations** natively via its `AllocProfiler` global allocator alongside
# timing — so the per-SSE-event allocation cost the decode findings target is visible, not inferred.
diff --git a/crates/agent-core/src/client.rs b/crates/agent-core/src/client.rs
index 1371b00a..5a8caaa4 100644
--- a/crates/agent-core/src/client.rs
+++ b/crates/agent-core/src/client.rs
@@ -2082,7 +2082,7 @@ mod tests {
#[tokio::test]
async fn oauth_credential_adds_anthropics_identity_headers() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2131,7 +2131,7 @@ mod tests {
#[tokio::test]
async fn a_static_credential_never_sends_oauth_headers() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2174,7 +2174,7 @@ mod tests {
/// the wire, as opposed to anything this crate constructs itself.
#[tokio::test]
async fn a_connection_dropped_mid_body_is_tagged_as_a_mid_stream_network_error() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2222,7 +2222,7 @@ mod tests {
/// than being a dead field, since the default [`READ_TIMEOUT`] (600s) would never trip in a test.
#[tokio::test]
async fn with_idle_timeout_overrides_the_default_read_timeout() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2304,7 +2304,7 @@ mod tests {
/// `anthropic-version` for a self-hosted/proxied endpoint that speaks a different wire version).
#[tokio::test]
async fn extra_headers_are_merged_onto_the_request_and_win_on_a_name_collision() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured_clone = captured.clone();
@@ -2366,7 +2366,7 @@ mod tests {
}
}
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2424,7 +2424,7 @@ mod tests {
}
}
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = Arc::new(std::sync::Mutex::new(Vec::new()));
let captured_clone = captured.clone();
@@ -2473,7 +2473,7 @@ mod tests {
}
}
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = Arc::new(std::sync::Mutex::new(Vec::new()));
let captured_clone = captured.clone();
@@ -2519,7 +2519,7 @@ mod tests {
}
}
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2567,7 +2567,7 @@ mod tests {
/// this shutdown was actually a transport fault.
#[tokio::test]
async fn a_clean_early_close_before_the_terminal_event_is_not_tagged_as_a_network_error() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -2613,7 +2613,7 @@ mod tests {
/// never fix.
#[tokio::test]
async fn a_429_with_quota_exhaustion_body_is_not_retried() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let request_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let count = request_count.clone();
@@ -2657,7 +2657,7 @@ mod tests {
/// normal retry treatment `is_quota_exhausted` doesn't touch.
#[tokio::test]
async fn a_429_with_a_plain_rate_limit_body_is_retried() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let request_count = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let count = request_count.clone();
@@ -2719,7 +2719,7 @@ mod tests {
/// headers when compat.sendSessionAffinityHeaders is enabled" case.
#[tokio::test]
async fn session_affinity_headers_are_sent_for_a_fireworks_chat_completions_request() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured_clone = captured.clone();
@@ -2769,7 +2769,7 @@ mod tests {
/// provider got these headers indiscriminately.
#[tokio::test]
async fn session_affinity_headers_are_absent_for_a_non_fireworks_chat_completions_request() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured_clone = captured.clone();
@@ -2821,7 +2821,7 @@ mod tests {
/// dedicated negative test above for that).
#[tokio::test]
async fn session_affinity_headers_are_suppressed_when_no_cache_is_set() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured_clone = captured.clone();
@@ -2876,7 +2876,7 @@ mod tests {
/// never reach the wire with that call still unanswered.
#[tokio::test]
async fn an_orphaned_tool_use_from_a_cancelled_mid_argument_stream_never_reaches_the_wire() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured_clone = captured.clone();
@@ -2937,7 +2937,7 @@ mod tests {
/// matching `tool_use` id) rather than on substrings, so it fails for the right reason.
#[tokio::test]
async fn an_aborted_turns_tool_use_never_reaches_the_wire_as_a_dangling_tool_result() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured_clone = captured.clone();
@@ -3022,7 +3022,7 @@ mod tests {
#[tokio::test]
async fn prefixed_route_override_reaches_the_gateway_under_the_provider_prefix_with_static_headers()
{
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3108,7 +3108,7 @@ mod tests {
/// Anthropic's own OAuth identity-spoofing headers (this is a Copilot host, not Anthropic's).
#[tokio::test]
async fn direct_route_override_bypasses_the_gateway_base_url_and_attaches_copilot_headers() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3192,7 +3192,7 @@ mod tests {
#[tokio::test]
async fn a_copilot_routed_claude_model_gets_the_browser_access_header_but_not_claude_code_identity()
{
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3259,7 +3259,7 @@ mod tests {
/// Chat Completions body (`"messages"`), never a Responses body (`"input"`).
#[tokio::test]
async fn a_copilot_routed_gpt_4_1_builds_a_chat_completions_body_not_a_responses_body() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3318,7 +3318,7 @@ mod tests {
/// Responses dialect when reached through any route but Copilot's.
#[tokio::test]
async fn a_non_copilot_gpt_4_1_still_builds_a_responses_body() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3352,7 +3352,7 @@ mod tests {
/// proves the wiring from a real Copilot-routed credential through to that flag.
#[tokio::test]
async fn copilot_routed_gpt5_2_omits_the_explicit_reasoning_disable_end_to_end() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3404,7 +3404,7 @@ mod tests {
/// for the header shape — this proves it also suppresses `prompt_cache_retention` end to end.
#[tokio::test]
async fn azure_routed_request_omits_prompt_cache_retention_end_to_end() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3459,7 +3459,7 @@ mod tests {
/// reasoning-"off" wire value for) would incorrectly send the native `{"effort":"none"}` signal.
#[tokio::test]
async fn entra_id_shaped_config_with_only_deployment_name_still_suppresses_reasoning_disable() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3526,7 +3526,7 @@ mod tests {
/// (`{base_url}/responses`, base_url already carrying `/openai/v1`).
#[tokio::test]
async fn direct_route_with_custom_auth_header_sends_bare_key_and_omits_authorization() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3594,7 +3594,7 @@ mod tests {
/// `inferCopilotInitiator`/`hasCopilotVisionInput`.
#[tokio::test]
async fn copilot_dynamic_headers_reflect_the_turns_own_messages() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3665,7 +3665,7 @@ mod tests {
/// `input`, with `parallel_tool_calls`/`text.verbosity` always present, on the real wire body.
#[tokio::test]
async fn codex_routed_request_sends_instructions_and_parallel_tool_calls_end_to_end() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3760,7 +3760,7 @@ mod tests {
/// third-party provider (Kimi-Coding) would hit.
#[tokio::test]
async fn dialect_override_forces_anthropic_wire_for_a_model_id_that_fails_the_name_heuristic() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3817,7 +3817,7 @@ mod tests {
/// Anthropic's `anthropic-version` header.
#[tokio::test]
async fn without_a_dialect_override_the_same_model_id_builds_chat_completions_wire() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3861,7 +3861,7 @@ mod tests {
/// OpenAI-wire endpoint, a hard 400.
#[tokio::test]
async fn aggregator_host_from_a_byo_override_resolves_a_real_bare_id_host_collision() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3914,7 +3914,7 @@ mod tests {
#[tokio::test]
async fn without_an_aggregator_host_the_same_bare_id_keeps_the_host_agnostic_anthropic_default()
{
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3947,7 +3947,7 @@ mod tests {
/// URL — an Azure resource pinned to a dated `api-version` needs this on every request.
#[tokio::test]
async fn direct_route_query_param_is_appended_to_the_built_url() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -3999,7 +3999,7 @@ mod tests {
/// Azure deployment name doesn't have to match the app-level model id.
#[tokio::test]
async fn deployment_name_override_replaces_the_wire_level_model_field() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
@@ -4060,7 +4060,7 @@ mod tests {
/// a Bearer-prefixed value (unlike Azure's).
#[tokio::test]
async fn auth_header_prefix_is_prepended_to_the_credential_value() {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let server = std::thread::spawn(move || {
use std::io::{Read, Write};
diff --git a/crates/agent-core/src/codex_websocket.rs b/crates/agent-core/src/codex_websocket.rs
index 4e9796fa..e3b2a5b9 100644
--- a/crates/agent-core/src/codex_websocket.rs
+++ b/crates/agent-core/src/codex_websocket.rs
@@ -1306,9 +1306,7 @@ mod tests {
/// reads an entry's `created_at` and drops the rest, so no handshake or peer is needed — but the
/// type is concrete, so *some* socket is.
async fn parked_socket() -> WsSocket {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
- .await
- .expect("loopback bind");
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().expect("bound port");
let client = tokio::net::TcpStream::connect(addr).await.expect("connect");
listener.accept().await.expect("accept");
diff --git a/crates/agent-core/tests/client_socket.rs b/crates/agent-core/tests/client_socket.rs
index 1a56c2c0..831b297b 100644
--- a/crates/agent-core/tests/client_socket.rs
+++ b/crates/agent-core/tests/client_socket.rs
@@ -7,7 +7,6 @@
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::io::{Read, Write};
-use std::net::TcpListener;
use std::thread;
use agent_core::message::StreamEvent;
@@ -17,7 +16,7 @@ use futures::StreamExt;
/// Spawn a one-shot server that returns `body` as an SSE response, and return its base URL.
fn spawn_sse_server(body: &'static str) -> String {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
@@ -102,7 +101,7 @@ data: {\"type\":\"message_stop\"}\n\
async fn gateway_client_reassembles_utf8_split_across_chunks() {
use std::time::Duration;
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
@@ -145,7 +144,7 @@ async fn gateway_client_reassembles_utf8_split_across_chunks() {
#[tokio::test]
async fn gateway_client_surfaces_http_error() {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
@@ -175,7 +174,7 @@ async fn gateway_client_gives_actionable_guidance_on_a_401() {
// "gateway returned 401:
" — identical formatting to any other 4xx, with no hint at what to
// actually do about it. Unlike a 403 (still just a generic error, see the test above), 401
// specifically must name the actual cause and point at the fix.
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
@@ -215,7 +214,7 @@ async fn gateway_client_bounds_memory_on_an_oversized_error_body() {
// memory ahead of the display truncation. `Content-Length` is set so the body isn't itself framed
// as an SSE stream — this exercises the non-2xx error path, not `LineFramer`.
const BODY_LEN: usize = 3 * 1024 * 1024; // comfortably over the 1MB read cap
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
if let Ok((mut stream, _)) = listener.accept() {
@@ -262,7 +261,7 @@ async fn gateway_client_bounds_memory_on_an_oversized_error_body() {
/// Capture the raw bytes of the first request a one-shot server receives, then answer with a minimal
/// empty-body SSE response. Returns the shared buffer the caller reads after driving the request.
fn spawn_request_capturing_server() -> (String, std::sync::Arc>) {
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
let captured2 = captured.clone();
@@ -365,7 +364,7 @@ async fn gateway_client_retries_transient_503_then_succeeds() {
// The server returns a retryable 503 on the first connection and the real SSE body on the second.
// The client must transparently retry and deliver the events — a transient gateway hiccup should
// not vaporize the request.
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
// First attempt: 503 with a tiny Retry-After so the backoff stays fast.
@@ -417,7 +416,7 @@ async fn gateway_client_retries_a_connection_reset_mid_send_then_succeeds() {
// then dropped with zero bytes written back — exactly that reset-after-connect shape, not a 503 or
// a timeout — so this is real coverage for a genuinely different failure mode than the sibling test
// above, not a duplicate of it.
- let listener = TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
thread::spawn(move || {
// First attempt: accept, then drop immediately — a connection reset mid-send, not a timeout and
diff --git a/crates/agent-core/tests/codex_websocket_socket.rs b/crates/agent-core/tests/codex_websocket_socket.rs
index a79cbf6a..0ef7eeb8 100644
--- a/crates/agent-core/tests/codex_websocket_socket.rs
+++ b/crates/agent-core/tests/codex_websocket_socket.rs
@@ -20,7 +20,6 @@ use agent_core::{GatewayClient, Message, ModelRequest};
use futures::{SinkExt, StreamExt};
use serde_json::{Value, json};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
-use tokio::net::TcpListener;
use tokio_tungstenite::WebSocketStream;
use tokio_tungstenite::tungstenite::Message as WsMessage;
@@ -104,7 +103,7 @@ async fn recv_request(ws: &mut WebSocketStream) -> Value
#[tokio::test]
async fn first_turn_sends_the_full_transcript_with_no_previous_response_id() {
- let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().expect("addr");
let server = tokio::spawn(async move {
@@ -154,7 +153,7 @@ async fn first_turn_sends_the_full_transcript_with_no_previous_response_id() {
#[tokio::test]
async fn second_turn_on_the_same_session_sends_only_the_new_delta() {
- let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().expect("addr");
let server = tokio::spawn(async move {
@@ -239,7 +238,7 @@ async fn second_turn_on_the_same_session_sends_only_the_new_delta() {
#[tokio::test]
async fn a_failed_websocket_handshake_falls_back_to_http_sse_transparently() {
- let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().expect("addr");
let server = tokio::spawn(async move {
@@ -313,7 +312,7 @@ async fn a_failed_websocket_handshake_falls_back_to_http_sse_transparently() {
/// corrupt a binary zstd body) to check the header and decompress the body directly.
#[tokio::test]
async fn the_http_sse_fallback_sends_a_zstd_compressed_body_with_the_matching_header() {
- let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().expect("addr");
let server = tokio::spawn(async move {
diff --git a/crates/agent/ARCHITECTURE.md b/crates/agent/ARCHITECTURE.md
index 720b0e42..17c57cef 100644
--- a/crates/agent/ARCHITECTURE.md
+++ b/crates/agent/ARCHITECTURE.md
@@ -4239,7 +4239,7 @@ backend an operator points it at.
| `src/bin/mcp_skills_fixture_server.rs` | Hand-rolled SEP-2640 server for `tests/mcp_skills*.rs` (stdio, or `--http`): an ordinary skill with a supporting file and a nested skill, a same-name pair, a `disable-model-invocation` skill, a tampered one, a dynamic one, an unlisted one, two over the per-skill limits, and a late one published by its `publish_late` tool (with `notifications/resources/list_changed`); caching hints from env; `_meta` on every result like a `2026-07-28` server; logs every request so tests assert what was (not) fetched |
| `benches/search.rs` | Criterion macro-bench: `grep` (1 vs auto threads) and `find` (sequential) over a 5,000-file tree |
| `benches/serve_runtime.rs` | Process-level Tokio runtime A/B for `serve --listen`: spawns the real binary under `current_thread` vs multi-thread (`BEYOND_AI_AGENT_TOKIO_WORKER_THREADS`) and drives concurrent WebSocket sessions, reporting RSS, throughput, p95 latency, and CPU. Custom harness, not Criterion/divan — the cost is thread stacks and per-thread heaps, which an in-process alloc profiler never sees. |
-| `tests/common/mod.rs` | Shared test harness: mock Anthropic-SSE model server (an in-order server answers `serve`'s session-title request itself, off the conversation script and out of its record, counting them for `title_calls`; a test about titles matches `SESSION_TITLE_MARKER` on a routed one), ports with no window for another process to take them (`spawn_listening`: `serve --listen 127.0.0.1:0`, the bound port read from its announcement; `HeldPort`: a listener the test binds and hands over by socket activation, for a port named in `serve`'s own arguments; `DeadPort`: bound, never listening; the gateway binds `127.0.0.1:0`/`127.0.0.2:0` (`GATEWAY_LISTENERS`, distinct addresses because Pingora keys listeners by address string) and `spawn_gateway` reads both ports from its own `LISTEN` sockets in `/proc`; the shared `nats-server` picks its own port (`-p -1`) and reports it via `--ports_file_dir` — no code in the workspace picks a free port and releases it; the read-back helpers (`listening_on`, `gateway_ports`, `nats_port_from`, `DeadPort`, `GATEWAY_LISTENERS`) live in `beyond_ai_test_support::ports` and are shared with the gateway, verify and fleet-sim harnesses, fail at once off Linux (they need `/proc` and `127.0.0.2`), and `serve_harness_deadlines::no_test_picks_a_port_and_releases_it` enforces it workspace-wide by what happens to a port-0 socket — its port read and the socket let go, under any name — not by a helper's name), one frame deadline for every `serve` reader (`FRAME_DEADLINE`, 60s: `child_frames` for stdout — the only way to a child's stdout pipe: `ChildGuard` takes the pipe off the `Child` at spawn, so `child.stdout` is empty however it is reached (`raw_stdout` exists for tests about the pipe itself, outside the `serve_*`/`mcp_*` suites), and `serve_harness_deadlines` lints every other route (the type, `Option` methods on the field, `mem::take`/`replace`/`swap` of it, `Child { stdout, .. }` patterns, unguarded spawns) in those suites, their directory modules and `tests/common`, `ws_next_frame` for WebSockets, and `skills_env::Serve`), so a stalled run fails promptly instead of waiting for the runner's kill — `tests/serve_harness_deadlines.rs` holds every `serve_*` suite to it; `HeldPort` starts `serve` through a shell that exports its own pid as `LISTEN_PID` and `exec`s in place, as an activator does; `serve`/`run` command builders, `read_until_response`, gateway binary locator, port/connection helpers, process-lifetime JetStream for managed-gateway tests — used by every file below |
+| `tests/common/mod.rs` | Shared test harness: mock Anthropic-SSE model server (an in-order server answers `serve`'s session-title request itself, off the conversation script and out of its record, counting them for `title_calls`; a test about titles matches `SESSION_TITLE_MARKER` on a routed one), ports with no window for another process to take them (`spawn_listening`: `serve --listen 127.0.0.1:0`, the bound port read from its announcement; `HeldPort`: a listener the test binds and hands over by socket activation, for a port named in `serve`'s own arguments; `DeadPort`: bound, never listening; the gateway binds `127.0.0.1:0`/`127.0.0.2:0` (`GATEWAY_LISTENERS`, distinct addresses because Pingora keys listeners by address string) and `spawn_gateway` reads both ports from its own `LISTEN` sockets in `/proc`; the shared `nats-server` picks its own port (`-p -1`) and reports it via `--ports_file_dir` — no code in the workspace picks a free port and releases it; the read-back helpers (`listening_on`, `gateway_ports`, `nats_port_from`, `DeadPort`, `GATEWAY_LISTENERS`) live in `beyond_ai_test_support::ports` and are shared with the gateway, verify and fleet-sim harnesses, fail at once off Linux (they need `/proc` and `127.0.0.2`), and `tests/ports_lint.rs` enforces it workspace-wide with no data-flow analysis: port 0 is written only in `beyond_ai_test_support::ports` (`listener`, `tokio_listener`, `bound_socket`, `bound_reuseport_socket`, `DeadPort`), apart from the few sites that bind it and keep the socket (the MCP OAuth callback, the fixture servers, child command lines), each annotated `// port-0:` in place and counted in the lint's `ALLOWED`), one frame deadline for every `serve` reader (`FRAME_DEADLINE`, 60s: `child_frames` for stdout — the only way to a child's stdout pipe: `ChildGuard` takes the pipe off the `Child` at spawn, so `child.stdout` is empty however it is reached (`raw_stdout` exists for tests about the pipe itself, outside the `serve_*`/`mcp_*` suites), and `serve_harness_deadlines` lints every other route (the type, `Option` methods on the field, `mem::take`/`replace`/`swap` of it, `Child { stdout, .. }` patterns, unguarded spawns) in those suites, their directory modules and `tests/common`, `ws_next_frame` for WebSockets, and `skills_env::Serve`), so a stalled run fails promptly instead of waiting for the runner's kill — `tests/serve_harness_deadlines.rs` holds every `serve_*` suite to it; `HeldPort` starts `serve` through a shell that exports its own pid as `LISTEN_PID` and `exec`s in place, as an activator does; `serve`/`run` command builders, `read_until_response`, gateway binary locator, port/connection helpers, process-lifetime JetStream for managed-gateway tests — used by every file below |
| `tests/run_*.rs` | `run` binary against a mock model server (no gateway in the loop), split by domain: `run_core_flow` (tool round trips, refusal exit codes, text/json mode, stdin/@file input), `run_skills_prompts` (skill/prompt-template expansion, trust gating), `run_session_management` (`--session`/`--continue`/`--name`/`--fork`, same- and cross-project), `run_cli_flags` (`--version`/`--help`/`list-models`/`export`, flags reaching the wire request, including `models.json` header/api_key overrides, `--idle-timeout-ms`, `--block-images`), `run_stdout_robustness` (Task #10: a closed stdout pipe, simulated by dropping the child's stdout handle mid-stream, must exit 0 rather than panic on `EPIPE`, in both text and `--json` mode) |
| `tests/serve_*.rs` | `serve` binary NDJSON protocol round-trip, split by domain: `serve_session_lifecycle` (startup/resume/crash recovery/jsonl framing/export), `serve_state_reporting` (`get_state`/`get_session_stats`/`cwd_stale`), `serve_session_tree` (branches/forks/`get_tree`), `serve_models_thinking` (model/thinking-level/`--models`), `serve_compaction_retry` (compaction + whole-run auto-retry), `serve_trust_prompts` (trust gating + system prompt), `serve_tools_bash` (tool exclusion + host `bash`), `serve_prompt_flow` (`prompt`/`steer`/`follow_up` queuing, busy semantics, refusal, abort), `serve_lifecycle` (host-emitted run lifecycle POSTs: started/progress/terminal) — each a separate Cargo test binary, ~400-1200 lines apiece rather than one ~6,200-line file |
| `tests/gateway_e2e.rs` | `run` binary → real gateway binary → mock upstream (proves key-swap + the virtual key never reaches upstream). Boots a real nats-server so the fail-closed allowance watcher can seed |
@@ -4296,7 +4296,7 @@ backend an operator points it at.
| `BEYOND_AI_AGENT_TEST_SLOW_SESSION_OPEN_MS` | unset (no delay) | **Debug builds only** (`#[cfg(debug_assertions)]`), and only for tests: makes every `Persistence::open` sleep this many milliseconds, standing in for a slow network-backed session directory. `tests/serve_startup_stall.rs` uses it to prove session start-up is off the shared runtime thread. A release binary contains no latency-injection path at all. |
| `AI_AGENT_OAUTH_CALLBACK_HOST` | unset (`127.0.0.1`) | The host the fixed-port OAuth login callback listener (Anthropic 53692, OpenAI Codex 1455) binds. **Loopback only** — an address in `127.0.0.0/8`, `::1`, or `localhost` — because the listener receives an authorization code; anything else (`0.0.0.0`, a routable address, a name) fails the login with a clear error before any bind (`oauth::callback_server::callback_host`). Tests give a login its own loopback address so concurrent runs do not collide on the fixed port. |
| `BEYOND_AI_AGENT_TEST_SLOW_CHECKPOINT_MS` | unset (no delay) | **Debug builds only** (`#[cfg(debug_assertions)]`), and only for tests: makes every mid-run checkpoint write in `serve` take this many milliseconds longer, inside the write itself, standing in for a slow disk — so an acknowledgement sent before the write would be caught. `tests/serve_session_lifecycle.rs` uses it to prove a tool does not start before its `tool_use` is on disk. A release binary contains no latency-injection path at all. |
-| `BEYOND_AI_AGENT_TEST_FAIL_CHECKPOINT` | unset (no delay) | **Debug builds only** (`#[cfg(debug_assertions)]`), and only for tests: fails `serve`'s checkpoint writes — `all` of them, or only the `n`th (a number). `tests/serve_session_lifecycle.rs` uses it to prove a failed write before a tool fails closed (the tool never runs) and that a best-effort one is superseded. A release binary contains no fault-injection path and none of these names (`tests/release_seams.rs`). |
+| `BEYOND_AI_AGENT_TEST_FAIL_CHECKPOINT` | unset (no delay) | **Debug builds only** (`#[cfg(debug_assertions)]`), and only for tests: fails `serve`'s checkpoint writes — `all` of them, or only the `n`th (a number), counted per session so a daemon's sessions do not shift each other's count (`serve_session_isolation`: `an_injected_checkpoint_failure_counts_each_sessions_writes`). `tests/serve_session_lifecycle.rs` uses it to prove a failed write before a tool fails closed (the tool never runs) and that a best-effort one is superseded. A release binary contains no fault-injection path and none of these names (`tests/release_seams.rs`). |
| `BEYOND_AI_AGENT_TEST_SLOW_SESSION_END_MS` | unset (no delay) | **Debug builds only** (`#[cfg(debug_assertions)]`), and only for tests: delays a failed daemon session's `error` broadcast after its loop has ended. `tests/serve_service_session_end.rs` uses it to prove the reason still reaches the client before the close. A release binary contains no latency-injection path at all. |
| `BEYOND_AI_AGENT_TEST_SLOW_ATTACH_MS` | unset (no delay) | **Debug builds only** (`#[cfg(debug_assertions)]`), and only for tests: delays a daemon connection's registration of its output. `tests/serve_service_session_end.rs` uses it to prove a connection registering after its session ended is still told why. A release binary contains no latency-injection path at all. |
| `--idle-timeout-ms` / `AI_AGENT_IDLE_TIMEOUT_MS` (`run`-only) | `agent_core::client::READ_TIMEOUT` (~600s) | Task #19 (pi-parity feature): overrides the gateway HTTP client's idle-read timeout between response chunks (`GatewayClient::with_idle_timeout`, previously unused anywhere in this codebase) — matters most for a direct-routed/custom `models.json` `base_url` override, which bypasses the gateway's own upstream-timeout assumption entirely |
diff --git a/crates/agent/benches/serve_runtime.rs b/crates/agent/benches/serve_runtime.rs
index 4b537584..c1613807 100644
--- a/crates/agent/benches/serve_runtime.rs
+++ b/crates/agent/benches/serve_runtime.rs
@@ -359,6 +359,7 @@ fn spawn_serve(bin: &str, flavor: Flavor, gateway: &str, cwd: &Path) -> ServePro
.args([
"serve",
"--listen",
+ // port-0: the child binds it, keeps it, and announces the port it got.
"127.0.0.1:0",
"--gateway-url",
gateway,
@@ -477,7 +478,7 @@ fn parse_listen_port(line: &str) -> Option {
/// Unbounded Anthropic-SSE mock: every request gets the same one-turn text reply, concurrently.
/// Prompt load would serialize behind a FIFO mock and measure the mock, not `serve`.
fn spawn_mock_gateway() -> String {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
listener.set_nonblocking(true).unwrap();
let addr = listener.local_addr().unwrap();
let body = anthropic_text_turn("ok");
diff --git a/crates/agent/benches/web_extract.rs b/crates/agent/benches/web_extract.rs
index 8f777d91..e0925c62 100644
--- a/crates/agent/benches/web_extract.rs
+++ b/crates/agent/benches/web_extract.rs
@@ -20,7 +20,6 @@
use std::hint::black_box;
use std::io::{Read, Write};
-use std::net::TcpListener;
use std::sync::OnceLock;
use agent_core::Tool;
@@ -74,7 +73,7 @@ fn page_html() -> &'static str {
fn fixture() -> &'static str {
static BASE: OnceLock = OnceLock::new();
BASE.get_or_init(|| {
- let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let body = page_html();
let response = format!(
diff --git a/crates/agent/src/bin/mcp_fixture_events_server.rs b/crates/agent/src/bin/mcp_fixture_events_server.rs
index 0b095caa..65434e61 100644
--- a/crates/agent/src/bin/mcp_fixture_events_server.rs
+++ b/crates/agent/src/bin/mcp_fixture_events_server.rs
@@ -1735,6 +1735,7 @@ async fn main() {
forbid_left: env_u64("MCP_FIXTURE_FORBID_FIRST", 0),
..State::default()
}));
+ // port-0: held for the server's life; the address is announced through the control file.
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let control = format!("http://{addr}");
diff --git a/crates/agent/src/bin/mcp_fixture_tasks_server.rs b/crates/agent/src/bin/mcp_fixture_tasks_server.rs
index 99574259..b4c50866 100644
--- a/crates/agent/src/bin/mcp_fixture_tasks_server.rs
+++ b/crates/agent/src/bin/mcp_fixture_tasks_server.rs
@@ -760,6 +760,7 @@ async fn serve_stdio(server: Server) {
/// (or `202` for a notification), one request per connection. No SSE stream: `GET` is `405`, which
/// the transport allows for a server that never pushes.
async fn serve_http(server: Arc>, port_file: &str) {
+ // port-0: held for the server's life; the port is announced through the port file.
let Ok(listener) = tokio::net::TcpListener::bind("127.0.0.1:0").await else {
return;
};
diff --git a/crates/agent/src/bin/mcp_skills_fixture_server.rs b/crates/agent/src/bin/mcp_skills_fixture_server.rs
index b30821d4..ee6f4dc5 100644
--- a/crates/agent/src/bin/mcp_skills_fixture_server.rs
+++ b/crates/agent/src/bin/mcp_skills_fixture_server.rs
@@ -736,6 +736,7 @@ async fn main() {
/// life the same way it does a stdio server's.
async fn serve_http() {
use tokio::io::AsyncReadExt;
+ // port-0: held for the server's life; the port it got is announced to the test.
let Ok(listener) = tokio::net::TcpListener::bind("127.0.0.1:0").await else {
eprintln!("mcp_skills_fixture_server: cannot bind a loopback port");
return;
diff --git a/crates/agent/src/lifecycle.rs b/crates/agent/src/lifecycle.rs
index 0cfff38a..9a442abf 100644
--- a/crates/agent/src/lifecycle.rs
+++ b/crates/agent/src/lifecycle.rs
@@ -1131,7 +1131,7 @@ mod tests {
seen: Arc>>,
hits: Arc,
) -> String {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
loop {
diff --git a/crates/agent/src/main.rs b/crates/agent/src/main.rs
index bb48b3bb..e495140e 100644
--- a/crates/agent/src/main.rs
+++ b/crates/agent/src/main.rs
@@ -2816,6 +2816,7 @@ async fn run() -> Result<(), Box> {
// picking a free port, releasing it and binding it later let another process take it in
// between, and the login then failed before printing its URL.
let listener =
+ // port-0: the listener is held for the whole login; the redirect URI names its port.
beyond_ai_agent::oauth::callback_server::CallbackServer::bind("127.0.0.1", 0)
.map_err(|e| {
format!("failed to bind the local OAuth callback listener: {e}")
diff --git a/crates/agent/src/metrics.rs b/crates/agent/src/metrics.rs
index 3b414cdf..903d1ec3 100644
--- a/crates/agent/src/metrics.rs
+++ b/crates/agent/src/metrics.rs
@@ -479,7 +479,7 @@ mod tests {
#[tokio::test]
async fn the_listener_serves_metrics_and_refuses_every_other_path() {
- let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
let m = Metrics::new().unwrap();
m.sessions_live.set(7);
diff --git a/crates/agent/src/oauth/callback_server.rs b/crates/agent/src/oauth/callback_server.rs
index dda077c7..d06892b7 100644
--- a/crates/agent/src/oauth/callback_server.rs
+++ b/crates/agent/src/oauth/callback_server.rs
@@ -236,11 +236,17 @@ fn respond(request: tiny_http::Request, success: bool) {
mod tests {
use super::*;
+ /// A server on a port the kernel picks: what `agent mcp login` binds, and what is under test.
+ fn on_any_port() -> CallbackServer {
+ // port-0: the CallbackServer under test holds its listener for its whole life.
+ CallbackServer::bind("127.0.0.1", 0).unwrap()
+ }
+
/// Port `0` binds a port the kernel picks, and [`CallbackServer::port`] says which: the listener is
/// on it from the start, and a callback sent there arrives.
#[tokio::test]
async fn port_zero_binds_a_real_port_and_reports_it() {
- let server = CallbackServer::bind("127.0.0.1", 0).unwrap();
+ let server = on_any_port();
let port = server.port();
assert_ne!(port, 0);
assert!(std::net::TcpStream::connect(("127.0.0.1", port)).is_ok());
@@ -248,7 +254,7 @@ mod tests {
#[tokio::test]
async fn a_matching_callback_resolves_with_the_code() {
- let server = CallbackServer::bind("127.0.0.1", 0).unwrap();
+ let server = on_any_port();
let port = server.port();
let client = tokio::spawn(async move {
// Give the server a moment to be actively waiting.
@@ -272,7 +278,7 @@ mod tests {
#[tokio::test]
async fn a_state_mismatch_does_not_resolve_and_the_server_keeps_listening() {
- let server = CallbackServer::bind("127.0.0.1", 0).unwrap();
+ let server = on_any_port();
let port = server.port();
let cancel = CancellationToken::new();
let cancel_for_client = cancel.clone();
@@ -308,7 +314,7 @@ mod tests {
#[tokio::test]
async fn cancellation_resolves_with_none() {
- let server = CallbackServer::bind("127.0.0.1", 0).unwrap();
+ let server = on_any_port();
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
@@ -356,7 +362,7 @@ mod tests {
#[tokio::test]
async fn binding_an_already_used_port_fails() {
- let first = CallbackServer::bind("127.0.0.1", 0).unwrap();
+ let first = on_any_port();
let port = first.port();
let second = CallbackServer::bind("127.0.0.1", port);
assert!(matches!(second, Err(OAuthError::PortBindFailed { .. })));
diff --git a/crates/agent/src/serve.rs b/crates/agent/src/serve.rs
index bb00b84b..5a0bd6d2 100644
--- a/crates/agent/src/serve.rs
+++ b/crates/agent/src/serve.rs
@@ -1131,7 +1131,8 @@ async fn persist_messages_blocking(
messages: Arc>,
) -> (Persistence, std::io::Result<()>) {
match tokio::task::spawn_blocking(move || {
- let r = simulated_checkpoint_write().and_then(|()| persistence.persist_messages(&messages));
+ let r = simulated_checkpoint_write(&persistence.meta.id)
+ .and_then(|()| persistence.persist_messages(&messages));
(persistence, r)
})
.await
@@ -1227,19 +1228,28 @@ async fn write_then_ack(
/// A slow or failing disk under a checkpoint write, simulated, **inside** the write itself: sleeps
/// `BEYOND_AI_AGENT_TEST_SLOW_CHECKPOINT_MS` milliseconds, then fails the write when
/// `BEYOND_AI_AGENT_TEST_FAIL_CHECKPOINT` says so — `all` fails every checkpoint write, a number `n`
-/// only the `n`th (1-based, per process). Inside the write, not before it, so an
+/// only the `n`th (1-based) **of each session**, counted by session id, so on a daemon one session's
+/// writes never shift which write of another's fails. Inside the write, not before it, so an
/// acknowledgement sent ahead of the write would precede the slow part — which is the mistake the
/// slow-disk test is there to catch. `#[cfg]`-gated like [`simulated_slow_open`], names and all: a
/// release binary has no fault-injection path and carries none of these variable names.
#[cfg(debug_assertions)]
-fn simulated_checkpoint_write() -> std::io::Result<()> {
+fn simulated_checkpoint_write(session: &str) -> std::io::Result<()> {
if let Ok(ms) = std::env::var("BEYOND_AI_AGENT_TEST_SLOW_CHECKPOINT_MS")
&& let Ok(ms) = ms.parse::()
{
std::thread::sleep(std::time::Duration::from_millis(ms));
}
- static WRITES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
- let n = WRITES.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
+ static WRITES: std::sync::Mutex> =
+ std::sync::Mutex::new(std::collections::BTreeMap::new());
+ let n = {
+ let mut writes = WRITES
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ let n = writes.entry(session.to_owned()).or_default();
+ *n += 1;
+ *n
+ };
let fail = match std::env::var("BEYOND_AI_AGENT_TEST_FAIL_CHECKPOINT").as_deref() {
Ok("all") => true,
Ok(which) => which.parse::().is_ok_and(|which| which == n),
@@ -1252,7 +1262,7 @@ fn simulated_checkpoint_write() -> std::io::Result<()> {
}
#[cfg(not(debug_assertions))]
-fn simulated_checkpoint_write() -> std::io::Result<()> {
+fn simulated_checkpoint_write(_session: &str) -> std::io::Result<()> {
Ok(())
}
diff --git a/crates/agent/src/tools/mcp_events/wire.rs b/crates/agent/src/tools/mcp_events/wire.rs
index 556e9987..b9fff546 100644
--- a/crates/agent/src/tools/mcp_events/wire.rs
+++ b/crates/agent/src/tools/mcp_events/wire.rs
@@ -1347,7 +1347,7 @@ mod tests {
/// A loopback server answering 401 to any request without `Bearer fresh`, 200 otherwise;
/// records each request's `Authorization`.
async fn bearer_server() -> (String, Arc>>) {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let url = format!("http://{}/mcp", listener.local_addr().unwrap());
let seen = Arc::new(Mutex::new(Vec::new()));
let record = seen.clone();
diff --git a/crates/agent/src/tools/mcp_http_exit.rs b/crates/agent/src/tools/mcp_http_exit.rs
index 3303c9a3..b7f491bf 100644
--- a/crates/agent/src/tools/mcp_http_exit.rs
+++ b/crates/agent/src/tools/mcp_http_exit.rs
@@ -306,7 +306,7 @@ mod tests {
/// A loopback server that answers every request 200 and records each request's first line and
/// `Mcp-Session-Id`.
async fn server() -> (String, Arc>>) {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let url = format!("http://{}/mcp", listener.local_addr().unwrap());
let seen = Arc::new(Mutex::new(Vec::new()));
let record = seen.clone();
@@ -382,7 +382,7 @@ mod tests {
..Registry::default()
});
// A listener that accepts and never answers.
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let url = format!("http://{}/mcp", listener.local_addr().unwrap());
let _accepting = tokio::spawn(async move {
let mut held = Vec::new();
diff --git a/crates/agent/src/tools/mcp_oauth.rs b/crates/agent/src/tools/mcp_oauth.rs
index 8989fd38..b6dde431 100644
--- a/crates/agent/src/tools/mcp_oauth.rs
+++ b/crates/agent/src/tools/mcp_oauth.rs
@@ -930,7 +930,7 @@ mod tests {
status: &'static str,
extra: &'static str,
) -> (String, Arc>>) {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let url = format!("http://{}/mcp", listener.local_addr().unwrap());
let seen = Arc::new(Mutex::new(Vec::new()));
let record = seen.clone();
diff --git a/crates/agent/src/tools/mcp_view_http.rs b/crates/agent/src/tools/mcp_view_http.rs
index 21f9ce6c..928e59cd 100644
--- a/crates/agent/src/tools/mcp_view_http.rs
+++ b/crates/agent/src/tools/mcp_view_http.rs
@@ -191,7 +191,7 @@ mod tests {
/// [`canned`], written in `parts` with a pause between, so each reaches the client as a chunk
/// of its own (an event split across reads, as a large one always is).
async fn canned_in_parts(parts: &'static [&'static [u8]]) -> String {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((mut stream, _)) = listener.accept().await {
diff --git a/crates/agent/src/tools/mcp_wire.rs b/crates/agent/src/tools/mcp_wire.rs
index dbbe322a..c37b4f85 100644
--- a/crates/agent/src/tools/mcp_wire.rs
+++ b/crates/agent/src/tools/mcp_wire.rs
@@ -661,7 +661,7 @@ mod tests {
/// One canned HTTP response per connection, from a loopback listener; `hold` keeps the
/// connection open afterwards (an SSE stream the server never ends).
async fn canned(response: &'static [u8], hold: bool) -> String {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
while let Ok((mut stream, _)) = listener.accept().await {
@@ -891,7 +891,7 @@ mod tests {
async fn an_ordinary_request_is_sent_as_rmcps_own_client_would() {
agent_core::ensure_provider();
async fn capture() -> (String, tokio::sync::oneshot::Receiver) {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
let (tx, rx) = tokio::sync::oneshot::channel();
tokio::spawn(async move {
diff --git a/crates/agent/src/tools/web/mod.rs b/crates/agent/src/tools/web/mod.rs
index 2eb4b847..e1714ae8 100644
--- a/crates/agent/src/tools/web/mod.rs
+++ b/crates/agent/src/tools/web/mod.rs
@@ -442,12 +442,11 @@ impl Tool for Web {
mod tests {
use super::*;
use std::io::{Read, Write};
- use std::net::TcpListener;
/// A one-shot loopback HTTP/1.1 server that replies with `response` to the first connection, and
/// records the raw request line + headers it received. Returns `(port, join_handle_for_request)`.
fn one_shot(response: &'static str) -> (u16, std::thread::JoinHandle) {
- let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let port = listener.local_addr().unwrap().port();
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
diff --git a/crates/agent/src/tools/web/ssrf.rs b/crates/agent/src/tools/web/ssrf.rs
index 8fc5324c..f83c6cfb 100644
--- a/crates/agent/src/tools/web/ssrf.rs
+++ b/crates/agent/src/tools/web/ssrf.rs
@@ -221,6 +221,7 @@ impl Resolve for SsrfResolver {
let policy = Arc::clone(&self.policy);
let host = name.as_str().to_string();
Box::pin(async move {
+ // port-0: a DNS lookup, not a bind — only the addresses it returns are used.
let addrs: Vec = tokio::net::lookup_host((host.as_str(), 0))
.await
.map_err(|e| Box::new(e) as Box)?
diff --git a/crates/agent/tests/common/mcp_apps_http.rs b/crates/agent/tests/common/mcp_apps_http.rs
index 18053b76..a6e89daf 100644
--- a/crates/agent/tests/common/mcp_apps_http.rs
+++ b/crates/agent/tests/common/mcp_apps_http.rs
@@ -57,7 +57,7 @@ fn advertises_ui(request: &Value) -> bool {
}
pub fn spawn_http_apps_fixture() -> HttpAppsFixture {
- let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let ui_requests = Arc::new(AtomicUsize::new(0));
let plain_requests = Arc::new(AtomicUsize::new(0));
diff --git a/crates/agent/tests/common/mcp_fixture.rs b/crates/agent/tests/common/mcp_fixture.rs
index e5b0819b..dab0a96b 100644
--- a/crates/agent/tests/common/mcp_fixture.rs
+++ b/crates/agent/tests/common/mcp_fixture.rs
@@ -59,7 +59,7 @@ impl HttpMcpFixture {
/// Start one fixture on a loopback port. The thread runs for the test process's life.
pub fn spawn_http_mcp_fixture() -> HttpMcpFixture {
- let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().unwrap();
let requests = Arc::new(Mutex::new(Vec::new()));
let recorder = requests.clone();
diff --git a/crates/agent/tests/common/mcp_oauth_fixture.rs b/crates/agent/tests/common/mcp_oauth_fixture.rs
index 829dd43d..5a8aff04 100644
--- a/crates/agent/tests/common/mcp_oauth_fixture.rs
+++ b/crates/agent/tests/common/mcp_oauth_fixture.rs
@@ -227,7 +227,7 @@ impl OAuthFixture {
/// seconds remain, so anything below 30 is refreshed on the very next read without any waiting;
/// a test that wants a token to stay put must pass something comfortably above it.
pub fn spawn(expires_in_secs: u64) -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let base = format!("http://{}", listener.local_addr().unwrap());
let fixture = Self {
url: format!("{base}/mcp"),
diff --git a/crates/agent/tests/common/mod.rs b/crates/agent/tests/common/mod.rs
index 95cfe48b..05914fc5 100644
--- a/crates/agent/tests/common/mod.rs
+++ b/crates/agent/tests/common/mod.rs
@@ -218,6 +218,7 @@ pub fn spawn_listening(cmd: &mut Command) -> (ChildGuard, u16) {
/// [`spawn_listening`], also copying the child's stderr to `log` — for a test that reads it back.
pub fn spawn_listening_logged(cmd: &mut Command, log: Option) -> (ChildGuard, u16) {
+ // port-0: the child binds it, keeps it, and announces the port it got (read below).
cmd.args(["--listen", "127.0.0.1:0"]).stderr(Stdio::piped());
let mut child = ChildGuard::spawn(cmd);
let stderr = child.stderr.take().expect("stderr is piped");
@@ -279,7 +280,7 @@ pub struct HeldPort {
impl HeldPort {
pub fn bind() -> Self {
- let listener = TcpListener::bind("127.0.0.1:0").unwrap();
+ let listener = beyond_ai_test_support::ports::listener();
let port = listener.local_addr().unwrap().port();
Self { listener, port }
}
diff --git a/crates/agent/tests/direct_routing_wire.rs b/crates/agent/tests/direct_routing_wire.rs
index d02c0403..8adec54e 100644
--- a/crates/agent/tests/direct_routing_wire.rs
+++ b/crates/agent/tests/direct_routing_wire.rs
@@ -21,7 +21,7 @@ use futures::StreamExt;
/// A one-shot listener that records the raw request text and answers with an empty SSE 200.
fn capture_request() -> (String, Arc>) {
- let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
+ let listener = beyond_ai_test_support::ports::listener();
let addr = listener.local_addr().expect("addr");
let captured = Arc::new(Mutex::new(String::new()));
let sink = Arc::clone(&captured);
diff --git a/crates/agent/tests/exec_endpoint.rs b/crates/agent/tests/exec_endpoint.rs
index e58c9bfc..ed965ae7 100644
--- a/crates/agent/tests/exec_endpoint.rs
+++ b/crates/agent/tests/exec_endpoint.rs
@@ -26,7 +26,7 @@ async fn mock_provider(
root: std::path::PathBuf,
seen: Arc,
) -> (String, Arc) {
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
let hits = seen.clone();
tokio::spawn(async move {
@@ -235,7 +235,7 @@ async fn an_unreachable_endpoint_fails_loudly_rather_than_looking_empty() {
#[tokio::test]
async fn auth_headers_reach_the_endpoint() {
// Every real provider needs some form of auth; which form is theirs to decide.
- let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
+ let listener = beyond_ai_test_support::ports::tokio_listener().await;
let addr = listener.local_addr().unwrap();
let (tx, rx) = std::sync::mpsc::channel::();
tokio::spawn(async move {
diff --git a/crates/agent/tests/exec_endpoint_response_cap.rs b/crates/agent/tests/exec_endpoint_response_cap.rs
index a7ee0857..183bbb73 100644
--- a/crates/agent/tests/exec_endpoint_response_cap.rs
+++ b/crates/agent/tests/exec_endpoint_response_cap.rs
@@ -28,7 +28,7 @@ where
F: Fn(tokio::net::TcpStream) -> Fut + Send + Sync + 'static,
Fut: std::future::Future