From a99fc1a8a7e73bce997b72acf652e91760e8e116 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 11:16:39 -0700 Subject: [PATCH 1/4] quest: claim auth-outage-clock Co-Authored-By: Claude Opus 5.5 From a892e11f04de9d08d635be45601f319621066432 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 11:25:37 -0700 Subject: [PATCH 2/4] test(auth): run the outage tests on a paused clock without sockets Co-Authored-By: Claude Opus 5.5 --- rs/moq-auth/src/client.rs | 261 ++++++++++++++-------------- rs/moq-relay/tests/auth_lifetime.rs | 153 ++++++++++------ 2 files changed, 227 insertions(+), 187 deletions(-) diff --git a/rs/moq-auth/src/client.rs b/rs/moq-auth/src/client.rs index 935e22c784..cd07c3f06a 100644 --- a/rs/moq-auth/src/client.rs +++ b/rs/moq-auth/src/client.rs @@ -70,26 +70,24 @@ impl Client { /// Admit a session: POST `connect`, validate the reply, and return the lease the /// session holds. The session reports totals through [`lease::Consumer::close`]. pub async fn connect(&self, request: Request) -> crate::Result { - let mut request = request; - request.event = Event::Connect; - - let grant = self.post(&request).await?; - let (producer, consumer) = lease::Producer::new(grant.clone()); - - let driver = Driver { - client: self.clone(), - request, - producer: Some(producer), - expires: grant.deadline(), - started: Instant::now(), - }; - tokio::spawn(driver.run(grant)); - - Ok(consumer) + connect(self.clone(), request).await } +} + +/// Where a lease's requests go: the auth server over HTTP, or an in-process +/// answer in tests, so the lease's timers run on Tokio's paused clock with no +/// socket for them to outrun. +trait Post: Clone + Send + Sync + 'static { + /// Send a `connect` or `revalidate`: the grant as answered, not yet validated, + /// a refusal, or an outage. + fn post(&self, request: &Request) -> impl Future> + Send; + + /// Send the `end`. + fn post_end(&self, request: &Request) -> impl Future> + Send; +} - /// One POST: a 2xx with a valid grant admits, a 401 or 403 refuses, a 2xx - /// whose grant fails validation is that error, and everything else is an +impl Post for Client { + /// A 2xx carries the grant, a 401 or 403 refuses, and everything else is an /// outage the caller decides about. async fn post(&self, request: &Request) -> crate::Result { let response = self.http.post(self.url.clone()).json(request).send().await?; @@ -100,26 +98,61 @@ impl Client { if !status.is_success() { return Err(Error::Unavailable(format!("auth server answered {status}"))); } - let grant: Grant = response.json().await?; - grant.validate().map_err(|err| match err { - // An empty grant is a refusal, not a malformed answer. - Error::UselessGrant => Error::Refused, - other => other, - })?; - Ok(grant) + Ok(response.json().await?) } + + async fn post_end(&self, request: &Request) -> crate::Result<()> { + self.http + .post(self.url.clone()) + .json(request) + .send() + .await? + .error_for_status()?; + Ok(()) + } +} + +/// Admit a session through `server` and spawn the driver behind its lease. +async fn connect(server: S, mut request: Request) -> crate::Result { + request.event = Event::Connect; + + let grant = ask(&server, &request).await?; + let (producer, consumer) = lease::Producer::new(grant.clone()); + + let driver = Driver { + server, + request, + producer: Some(producer), + expires: grant.deadline(), + started: Instant::now(), + }; + tokio::spawn(driver.run(grant)); + + Ok(consumer) +} + +/// One `connect` or `revalidate`: a valid grant admits, and one that fails +/// validation is that error. +async fn ask(server: &S, request: &Request) -> crate::Result { + let grant = server.post(request).await?; + grant.validate().map_err(|err| match err { + // An empty grant is a refusal, not a malformed answer. + Error::UselessGrant => Error::Refused, + other => other, + })?; + Ok(grant) } /// The task behind a lease: re-checks on cadence and reports the end. -struct Driver { - client: Client, +struct Driver { + server: S, request: Request, producer: Option, started: Instant, expires: Option, } -impl Driver { +impl Driver { async fn run(mut self, grant: Grant) { let (reason, bytes) = self.drive(grant).await; @@ -130,7 +163,7 @@ impl Driver { bytes, }; // The session is already gone; nothing to do with a failure but say so. - if let Err(err) = self.client.post_end(&request).await { + if let Err(err) = self.server.post_end(&request).await { tracing::warn!(id = %request.id, %err, "failed to report the session end"); } } @@ -219,22 +252,10 @@ impl Driver { } fn post_revalidate(&self) -> Pin> + Send>> { - let client = self.client.clone(); + let server = self.server.clone(); let mut request = self.request.clone(); request.event = Event::Revalidate; - Box::pin(async move { client.post(&request).await }) - } -} - -impl Client { - async fn post_end(&self, request: &Request) -> crate::Result<()> { - self.http - .post(self.url.clone()) - .json(request) - .send() - .await? - .error_for_status()?; - Ok(()) + Box::pin(async move { ask(&server, &request).await }) } } @@ -316,34 +337,42 @@ mod tests { server } - /// Keep HTTP handling on the paused test clock instead of wiremock's separate runtime. - async fn clock_server(log: Log, grant: Grant, stall: bool) -> Client { - use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::post}; - let router = Router::new() - .route( - "/", - post( - |State((log, grant, stall)): State<(Log, Grant, bool)>, Json(request): Json| async move { - let event = request.event.clone(); - log.0.lock().push(request); - match event { - Event::Connect => Json(grant).into_response(), - Event::Revalidate if stall => std::future::pending().await, - Event::Revalidate => StatusCode::SERVICE_UNAVAILABLE.into_response(), - Event::End { .. } => StatusCode::OK.into_response(), - } - }, - ), - ) - .with_state((log, grant, stall)); - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let url = format!("http://{}/", listener.local_addr().unwrap()); - tokio::spawn(async move { axum::serve(listener, router).await.unwrap() }); - // Only the lease clock is under test; an HTTP timeout can auto-advance - // Tokio's paused clock before loopback I/O gets its first reactor turn. - Client { - http: reqwest::Client::builder().no_proxy().build().unwrap(), - url: url.parse().unwrap(), + /// An in-process auth server for tests on Tokio's paused clock: it logs each + /// request and answers with `respond`, never when that returns `None`. With no + /// socket, the clock only advances once every answer has landed. + #[derive(Clone)] + struct Script { + log: Log, + #[allow(clippy::type_complexity)] + respond: std::sync::Arc Option> + Send + Sync>, + } + + impl Script { + fn new(log: Log, respond: impl Fn(&Request) -> Option> + Send + Sync + 'static) -> Self { + Self { + log, + respond: std::sync::Arc::new(respond), + } + } + + async fn connect(&self) -> crate::Result { + connect(self.clone(), request()).await + } + } + + impl Post for Script { + async fn post(&self, request: &Request) -> crate::Result { + self.log.0.lock().push(request.clone()); + let answer = (self.respond)(request); + match answer { + Some(answer) => answer, + None => std::future::pending().await, + } + } + + async fn post_end(&self, request: &Request) -> crate::Result<()> { + self.log.0.lock().push(request.clone()); + Ok(()) } } @@ -497,25 +526,32 @@ mod tests { tokio::time::pause(); let mut grant = Grant::new(patterns(&["**"]), Patterns::new()); grant.expires = Some(SystemTime::now() - Duration::from_secs(1)); - let client = clock_server(Log::default(), grant, false).await; - assert!(matches!(client.connect(request()).await, Err(Error::GrantExpired))); + let script = Script::new(Log::default(), move |_| Some(Ok(grant.clone()))); + assert!(matches!(script.connect().await, Err(Error::GrantExpired))); } + /// An outage is evidence of nothing: the grant stands through failed re-checks + /// until `expires`, and closes then, not later. #[tokio::test] - async fn a_grant_closes_at_its_expiry() { + async fn an_outage_keeps_the_grant_until_expires() { tokio::time::pause(); - // Whole seconds, as the grant crosses the wire, so the client sees this exact instant. - // An hour out, so a slow runner cannot expire it before `connect` answers. - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); - let expires = SystemTime::UNIX_EPOCH + Duration::from_secs(now.as_secs() + 3600); - let mut grant = Grant::new(patterns(&["**"]), Patterns::new()); - grant.expires = Some(expires); - let client = clock_server(Log::default(), grant, false).await; + let expires = SystemTime::now() + Duration::from_secs(3); + let log = Log::default(); + let script = Script::new(log.clone(), move |request| { + Some(match request.event { + Event::Connect => { + let mut grant = grant(None, Some(Duration::from_secs(1))); + grant.expires = Some(expires); + Ok(grant) + } + _ => Err(Error::Unavailable("auth server answered 503".into())), + }) + }); - // The client reads both clocks somewhere inside `connect`, so bracket it: the - // bounds hold however long it takes. + // `expires` is wall-clock time, which the client maps onto Tokio's clock + // somewhere inside `connect`, so bracket it: the bounds hold however long it takes. let (wall, tick) = (SystemTime::now(), tokio::time::Instant::now()); - let consumer = client.connect(request()).await.unwrap(); + let consumer = script.connect().await.unwrap(); let earliest = tick + expires.duration_since(SystemTime::now()).unwrap(); let latest = tokio::time::Instant::now() + expires.duration_since(wall).unwrap(); @@ -525,51 +561,18 @@ mod tests { tokio::time::timeout_at(earliest - tolerance, consumer.closed()) .await .is_err(), - "live until its expiry" - ); - let reason = tokio::time::timeout_at(latest + tolerance, consumer.closed()) - .await - .expect("closed at its expiry, not later"); - assert_eq!(reason, Reason::Expired); - } - - /// On the real clock: a paused one jumps to the expiry timer whenever the runtime - /// waits on a socket, and macOS delivers loopback asynchronously, so the lease can - /// expire and drop the re-check before the server sees it. Load only delays the - /// close, so asserting it never lands before `expires` holds on a busy machine. - #[tokio::test] - async fn an_outage_keeps_the_grant_until_expires() { - // Whole seconds, as the grant crosses the wire, so the client sees this exact instant. - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); - let expires = SystemTime::UNIX_EPOCH + Duration::from_secs(now.as_secs() + 3); - - let log = Log::default(); - let server = server(log.clone(), move |request| match request.event { - Event::Connect => { - let mut grant = grant(None, Some(Duration::from_secs(1))); - grant.expires = Some(expires); - ResponseTemplate::new(200).set_body_json(grant) - } - Event::Revalidate => ResponseTemplate::new(503), - Event::End { .. } => ResponseTemplate::new(200), - }) - .await; - let consumer = client(&server).connect(request()).await.unwrap(); - - let reason = tokio::time::timeout(Duration::from_secs(10), consumer.closed()) - .await - .expect("expired"); - assert_eq!(reason, Reason::Expired); - assert!(SystemTime::now() >= expires, "an outage must not close before expires"); - assert!( - log.revalidates() >= 1, - "no re-check reached the server during the outage" + "an outage must not close before expires" ); + assert!(log.revalidates() >= 1, "the outage was answered before expires"); assert_eq!( consumer.grant().publish, patterns(&["**"]), "the grant stands through the outage" ); + let reason = tokio::time::timeout_at(latest + tolerance, consumer.closed()) + .await + .expect("closed at expires, not later"); + assert_eq!(reason, Reason::Expired); assert!(matches!( log.end().await.event, Event::End { @@ -582,13 +585,11 @@ mod tests { #[tokio::test] async fn expiry_fires_while_a_recheck_is_stalled() { tokio::time::pause(); - let client = clock_server( - Log::default(), - grant(Some(Duration::from_secs(3)), Some(Duration::from_secs(1))), - true, - ) - .await; - let consumer = client.connect(request()).await.unwrap(); + let script = Script::new(Log::default(), |request| match request.event { + Event::Connect => Some(Ok(grant(Some(Duration::from_secs(3)), Some(Duration::from_secs(1))))), + _ => None, + }); + let consumer = script.connect().await.unwrap(); let reason = tokio::time::timeout(Duration::from_secs(5), consumer.closed()) .await diff --git a/rs/moq-relay/tests/auth_lifetime.rs b/rs/moq-relay/tests/auth_lifetime.rs index a61e794a67..12d305d6c3 100644 --- a/rs/moq-relay/tests/auth_lifetime.rs +++ b/rs/moq-relay/tests/auth_lifetime.rs @@ -4,10 +4,11 @@ //! or QUIC) or its axum WebSocket path (`serve_ws` over `ws://`), points it at a //! scripted auth server, connects a publisher and a subscriber, confirms media //! flows, then asserts the relay follows the server's word: a re-check that moves -//! the tier retags the live session's stats, a narrower grant or a refusal closes it, an outage -//! keeps it until `expires`, and every close reports `end` with what it moved. +//! the tier retags the live session's stats, a narrower grant or a refusal closes it, +//! and every close reports `end` with what it moved. //! The last tests swap the server for an in-process decider answering -//! `Admissions`, and prove the lease it drives reaches the session the same way. +//! `Admissions`, and prove the lease it drives reaches the session the same way, +//! including an outage that keeps it until `expires`, on Tokio's paused clock. use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime}; @@ -746,60 +747,6 @@ async fn http_routes_hold_a_lease() { relay.abort(); } -/// An outage keeps the session until `expires`, then closes it as expired. -/// -/// On the real clock: a paused one jumps to the next timer whenever the runtime -/// waits on a socket, and macOS delivers loopback asynchronously, so a virtual -/// timeout can fire before the relay answers. Load only delays the close, so -/// asserting it never lands before `expires` holds on a busy machine. -#[tokio::test] -async fn an_outage_keeps_the_session_until_expires() { - // Whole seconds, as the grant crosses the wire, so the relay sees this exact instant. - let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); - let expires = SystemTime::UNIX_EPOCH + Duration::from_secs(now.as_secs() + 3); - - let mut grant = grant(Duration::ZERO); - grant.expires = Some(expires); - let script = Script::new(grant); - script.on_revalidate(Answer::Status(503)); - let (port, relay) = spawn_relay(build_auth(script.spawn().await)).await; - let (pub_session, sub_session) = connect_and_round_trip(&room_url("tcp", port)).await; - - assert_closed(pub_session, TIMEOUT, "publisher").await; - assert!( - SystemTime::now() >= expires, - "an outage must not close the publisher before expires" - ); - let outages = script - .seen - .lock() - .unwrap() - .iter() - .filter(|r| r.event == Event::Revalidate) - .count(); - assert!(outages > 0, "no re-check reached the server during the outage"); - assert_closed(sub_session, TIMEOUT, "subscriber").await; - - let ends = tokio::time::timeout(TIMEOUT, async { - loop { - let ends = script.ends(); - if ends.len() == 2 { - return ends; - } - tokio::time::sleep(Duration::from_millis(10)).await; - } - }) - .await - .expect("both sessions report their end"); - for end in ends { - let Event::End { reason, .. } = &end.event else { - unreachable!() - }; - assert_eq!(*reason, moq_auth::lease::Reason::Expired); - } - relay.abort(); -} - /// A session the client closes reports `end` with its duration and byte counters. /// Over QUIC, the one transport whose connection reports its totals. #[tokio::test] @@ -1041,6 +988,98 @@ async fn a_fixed_lease_still_expires() { } } +/// A connected `(client, server)` session pair over an in-memory byte stream, so a +/// test on Tokio's paused clock never waits on a socket: the clock only advances +/// once every byte in flight has landed. +async fn memory_sessions() -> (moq_net::Session, moq_net::Session) { + let config = || { + let mut config = qmux::Config::new(qmux::Version::QMux01); + config.protocol = qmux::Protocol::Negotiate(moq_net::ALPNS.iter().map(|alpn| alpn.to_string()).collect()); + config + }; + let stream = |io| qmux::transport::Stream::new(io, qmux::Version::QMux01, config().max_record_size); + let (client, server) = tokio::io::duplex(64 * 1024); + let (client, server) = tokio::try_join!( + qmux::Session::connect(stream(client), config()), + qmux::Session::accept(stream(server), config()), + ) + .expect("qmux handshake"); + + let now = tokio::time::Instant::now().into_std(); + let client = async { + let (session, driver) = moq_net::Client::new() + .connect(now, moq_tokio::transport::Session::new(client)) + .await + .expect("client handshake"); + tokio::spawn(moq_net::time::run(driver)); + session + }; + let server = async { + let (session, driver) = moq_net::Server::new() + .accept(now, moq_tokio::transport::Session::new(server)) + .await + .expect("server handshake"); + tokio::spawn(moq_net::time::run(driver)); + session + }; + tokio::join!(client, server) +} + +/// An outage keeps the session until `expires`, then closes it as expired and +/// reports that as its end. A decider that never answers again is an outage as +/// far as the relay can tell, so the relay enforces `expires` itself; how an auth +/// server's outage keeps the grant is `moq_auth::Client`'s to test. +#[tokio::test(start_paused = true)] +async fn an_outage_keeps_the_session_until_expires() { + let expires = SystemTime::now() + Duration::from_secs(3); + let (auth, mut admissions) = moq_relay::auth::Auth::embedded("test-relay"); + let decider = tokio::spawn(async move { + let admission = admissions.next().await.expect("an admission"); + let mut grant = Grant::new(all(), all()); + grant.expires = Some(expires); + let (producer, consumer) = moq_auth::lease::Producer::new(grant); + admission.grant(consumer); + producer + }); + + // `expires` is wall-clock time, which the relay maps onto Tokio's clock + // somewhere inside `admit`, so bracket it: the bounds hold however long it takes. + let (wall, tick) = (SystemTime::now(), tokio::time::Instant::now()); + let lease = auth + .admit(auth.request(moq_auth::Transport::Tcp, "/room")) + .await + .expect("admitted"); + let earliest = tick + expires.duration_since(SystemTime::now()).unwrap(); + let latest = tokio::time::Instant::now() + expires.duration_since(wall).unwrap(); + let producer = decider.await.expect("decider"); + + let (client, server) = memory_sessions().await; + let supervised = tokio::spawn(moq_relay::supervise( + server, + lease, + moq_relay::shutdown::Observer::disabled(), + None, + )); + + // A millisecond either side for Tokio's timer resolution. + let tolerance = Duration::from_millis(1); + assert!( + tokio::time::timeout_at(earliest - tolerance, client.closed()) + .await + .is_err(), + "an outage must not close the session before expires" + ); + tokio::time::timeout_at(latest + tolerance, client.closed()) + .await + .expect("closed at expires, not later"); + let (reason, _) = producer.closed().await; + assert_eq!(reason, moq_auth::lease::Reason::Expired); + supervised + .await + .expect("supervisor") + .expect("a lease end is not a session error"); +} + /// A relay whose config names no auth source is the embedder's to decide: `run` /// refuses to start until the admissions are taken, and once they are, the /// decider's grant admits sessions through the assembled relay. From e1f80aeb9e7dfac2ff66579255d02eb68a457d19 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 11:42:15 -0700 Subject: [PATCH 3/4] quest: finish auth-outage-clock Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 - quest/m1/auth-outage-clock.md | 63 ----------------------------------- 2 files changed, 64 deletions(-) delete mode 100644 quest/m1/auth-outage-clock.md diff --git a/quest/m1/README.md b/quest/m1/README.md index 4c9068da8f..091d46a183 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -46,7 +46,6 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. - [Watch audio under CSP](/quest/m1/watch-worklet-file.md) - production builds ship the audio worklet as a file, so `script-src 'self'` pages play audio - [More tests under load](/quest/m1/test-flakes-2.md) - the second round of load-only failures, fixed at the cause - [Interop audio cold start](/quest/m1/interop-audio-cold-start.md) - the interop audio tone check stops failing on cold start, fixed at its cause -- [Auth outage clock](/quest/m1/auth-outage-clock.md) - the relay and moq-auth outage tests run on a paused clock again and assert both bounds of `expires` - [Legacy end overshoot](/quest/m1/legacy-end-overshoot.md) - browser playback survives a group that starts inside the previous group's estimated end - [UnknownSession log flood](/quest/m1/unknown-session-logs.md) - streams reset before their WebTransport header stop being reported as UnknownSession at WARN - [Merge queue](/quest/m1/merge-queue.md) - the required checks run on `merge_group`, so a stale green check can no longer break main diff --git a/quest/m1/auth-outage-clock.md b/quest/m1/auth-outage-clock.md deleted file mode 100644 index 43c923a8b3..0000000000 --- a/quest/m1/auth-outage-clock.md +++ /dev/null @@ -1,63 +0,0 @@ -# [M] Auth outage tests on a paused clock - -## Goal - -The moq-relay and moq-auth outage tests run on tokio's paused clock again and -assert both bounds: a session (or grant) survives an auth outage until its -`expires`, and closes at `expires`, not later. No wall-clock sleeps, no -widened timeouts, and no dependence on how fast the OS delivers loopback. - -## Plan - -- The tests: `an_outage_keeps_the_session_until_expires` in - `rs/moq-relay/tests/auth_lifetime.rs` - ([#4244](https://github.com/moq-dev/moq/pull/4244)) and - `an_outage_keeps_the_grant_until_expires` in `rs/moq-auth/src/client.rs` - ([#4291](https://github.com/moq-dev/moq/pull/4291)). Both moved to the real - clock because a paused clock auto-advances while the runtime waits on a - real socket, so a virtual timer fired before macOS delivered loopback. That - swapped one violation of "unit tests mock time" for another, and #4244 - dropped the upper bound. Read both PR descriptions: they list what was - tried and why it failed (restoring a listener probe, pausing after setup, - waiting on the log). -- The race is real sockets under virtual time, so fix it by taking the - sockets out of these tests. Look at what the codebase already offers - before building anything: `rs/moq-net/tests/support/mock.rs` (an in-memory - session pair), `moq_relay::auth::Auth::embedded` with its `Admissions` - (decides leases in-process), and the lease driver in moq-auth, which could - be exercised against an in-process answer source instead of HTTP. If the - relay's `Connection` or moq-auth's `Client` cannot take such a transport, - prefer the small seam that lets them over a test-only shim. -- Decide where each assertion belongs. The outage semantics (a 503 keeps the - grant until `expires`) are moq-auth's; the relay test may only need to show - that a lease reaching `expires` closes the session as `Expired` and reports - `end`. Don't keep two tests proving the same thing. -- Measure against tokio's clock, not `SystemTime`: the grant still carries a - wall-clock `expires`, so pin how it maps onto the paused clock. -- Other paused-clock tests touch real sockets and would share the hazard - once a timeout lands on their path. #4291's audit named - `a_grant_within_clock_skew_stays_live` (moq-auth) and - `fixed_addresses_keep_tls_name_and_request_host` (moq-tokio websocket); - moq-auth's `clock_server` helper exists only to keep axum on the paused - clock. Move those onto the same seam if it is cheap. -- Prove it: loop the tests with every core loaded, on macOS if available, - and mutate the deadline both ways (close early, close late) to see each - bound fail. - -Public API: none unless a transport seam is needed; report it if so. Wire: -none. - -## Related - -- [More tests under load](/quest/m1/test-flakes-2.md) - the same rule - applied to other load-only failures -- [moq-shaper virtual time](/quest/m1/shaper-virtual-time.md) - the same - paused-clock-versus-real-socket fight in moq-shaper -- [#4280](https://github.com/moq-dev/moq/pull/4280) - moq-archive and - moq-hls tests poll with real-clock sleeps, on the archive track-timeline - line -- [#4281](https://github.com/moq-dev/moq/pull/4281) - OBS `WaitFor` polling, - on the C++ line -- [Nightly 2026-09-26](https://github.com/moq-dev/moq/actions/runs/36240326747/job/108399481809) - - the macOS relay tarball job failed this test with "publisher connect - timeout", before #4244 landed From 970e9dd1c7cde1bf7bd02042a65dd8015dfed1c2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 29 Sep 2026 11:47:43 -0700 Subject: [PATCH 4/4] quest: track the websocket paused-clock TLS tests under test-flakes-2 Co-Authored-By: Claude Opus 5.5 --- quest/m1/test-flakes-2.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/quest/m1/test-flakes-2.md b/quest/m1/test-flakes-2.md index 44fbc2ad56..a5941853f9 100644 --- a/quest/m1/test-flakes-2.md +++ b/quest/m1/test-flakes-2.md @@ -43,6 +43,12 @@ retry. `start_paused = true` but sleeps 1.2 s of real time, because the debounce reads `crate::Clock`, which uses `std::time::Instant`, so the paused tokio clock never reaches it. +- moq-tokio websocket `fixed_addresses_keep_tls_name_and_request_host` and + `ipv6_literal_fixed_addresses` pause the clock over a real TLS dial on + loopback. They pass today, but once a timeout lands on that path, the paused + clock can fire it before loopback delivers, the race + [#4527](https://github.com/moq-dev/moq/pull/4527) removed from the auth + outage tests. ## Plan