From cc4c5a6c5a4410649dbc32b63dce100ca5872e21 Mon Sep 17 00:00:00 2001 From: Brian Charbonneau Date: Mon, 10 Aug 2026 17:43:13 -0700 Subject: [PATCH] fix(desktop): isolate relay admission gate tests Signed-off-by: Brian Charbonneau --- desktop/src-tauri/src/relay.rs | 5 +++ desktop/src-tauri/src/relay_admission.rs | 46 +++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/src/relay.rs b/desktop/src-tauri/src/relay.rs index 71aa21c4133..7a4626174c3 100644 --- a/desktop/src-tauri/src/relay.rs +++ b/desktop/src-tauri/src/relay.rs @@ -655,6 +655,11 @@ mod tests { use crate::relay_admission::MAX_HINT_SECONDS; use std::io::{Read as _, Write as _}; + // relay_error_message() arms the process-wide admission gate on 429. + // Participate in the gate test boundary so this test cannot leak its + // 300-second capped expiry into parallel relay_admission tests. + let _gate_isolation = crate::relay_admission::isolate_test_gate().await; + // Use a std::net listener on a std::thread — the same pattern as the // relay_admission loopback tests. This avoids two races that cause CI // failures with tokio::net + into_std(): diff --git a/desktop/src-tauri/src/relay_admission.rs b/desktop/src-tauri/src/relay_admission.rs index 15222f85908..0486ef01b82 100644 --- a/desktop/src-tauri/src/relay_admission.rs +++ b/desktop/src-tauri/src/relay_admission.rs @@ -100,14 +100,52 @@ pub fn reset_rate_limit_gate() { *GATE_EXPIRY.lock().unwrap_or_else(|e| e.into_inner()) = None; } +// The gate is process-wide, so every test that can arm it must serialize on +// the same lock even when that test lives in another module. +#[cfg(test)] +static TEST_SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + +/// RAII boundary for tests outside this module that exercise a path capable of +/// arming the process-wide gate. It starts clean and clears the gate before +/// releasing the shared test lock, including when the test unwinds. +#[cfg(test)] +pub(crate) struct TestGateIsolation { + _serial: tokio::sync::MutexGuard<'static, ()>, +} + +#[cfg(test)] +impl Drop for TestGateIsolation { + fn drop(&mut self) { + reset_rate_limit_gate(); + } +} + +#[cfg(test)] +pub(crate) async fn isolate_test_gate() -> TestGateIsolation { + let serial = TEST_SERIAL.lock().await; + reset_rate_limit_gate(); + TestGateIsolation { _serial: serial } +} + #[cfg(test)] mod tests { use super::*; - // The gate is a process-wide static shared by every test in this binary, - // so all gate tests serialize on one async lock to keep armed expiries - // from bleeding between parallel test threads. - pub(crate) static TEST_SERIAL: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + #[tokio::test(start_paused = true)] + async fn isolation_guard_clears_an_armed_gate_when_dropped() { + let start = Instant::now(); + { + let _gate_isolation = isolate_test_gate().await; + activate_rate_limit(Some(MAX_HINT_SECONDS)); + } + + wait_for_rate_limit().await; + assert_eq!( + Instant::now(), + start, + "dropping the test isolation guard must clear the process-wide gate" + ); + } #[tokio::test(start_paused = true)] async fn wait_returns_immediately_when_gate_is_inactive() {