diff --git a/server/tests/sse_cleanup_test.rs b/server/tests/sse_cleanup_test.rs new file mode 100644 index 0000000..c02c7f3 --- /dev/null +++ b/server/tests/sse_cleanup_test.rs @@ -0,0 +1,47 @@ +//! Regression: SSE connection-pool leak via missing `EventSource.close()`. +//! +//! Every HTML template that opens `new EventSource('/web/events')` must +//! also close it on `pagehide`. Without the close, Chrome keeps the +//! socket in a "draining" state after navigation; after ~6 rapid +//! back-and-forth navigations all 6 per-origin pool slots are draining +//! and new requests stall with `blocked` timings of 15-30 seconds +//! (confirmed by HAR capture on 2026-04-20). + +use std::fs; +use std::path::Path; + +#[test] +fn every_event_source_template_closes_it_on_pagehide() { + let template_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("templates"); + let entries = fs::read_dir(&template_dir).expect("templates directory readable"); + + let mut checked = 0; + let mut missing: Vec = Vec::new(); + + for entry in entries { + let path = entry.unwrap().path(); + if path.extension().and_then(|s| s.to_str()) != Some("html") { + continue; + } + let content = fs::read_to_string(&path).unwrap(); + if !content.contains("new EventSource('/web/events')") { + continue; + } + checked += 1; + let has_pagehide = content.contains("pagehide"); + let has_close = content.contains(".close()"); + if !(has_pagehide && has_close) { + missing.push(path.file_name().unwrap().to_string_lossy().into_owned()); + } + } + + assert!( + checked >= 1, + "test itself is stale: no template contains EventSource('/web/events')", + ); + assert!( + missing.is_empty(), + "templates open EventSource but lack `pagehide` + `.close()` cleanup: {missing:?} \ + — this leaks Chrome socket-pool slots and stalls navigation after ~6 cycles", + ); +}