From 8fd1b33b867381ffec9034c5b9f5d2f22760e10d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:22:36 -0700 Subject: [PATCH 1/9] test(network): specify typed session.end send boundary --- .../webdriver_bidi_session_end_command.rs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_end_command.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs new file mode 100644 index 000000000..f481c18b8 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs @@ -0,0 +1,110 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { + let mut header = [0_u8; 2]; + stream.read_exact(&mut header)?; + if header[0] != 0x81 || header[1] & 0x80 == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one final masked client text frame", + )); + } + let length = usize::from(header[1] & 0x7f); + if length > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required extended framing", + )); + } + let mut mask = [0_u8; 4]; + stream.read_exact(&mut mask)?; + let mut payload = vec![0_u8; length]; + stream.read_exact(&mut payload)?; + for (index, byte) in payload.iter_mut().enumerate() { + *byte ^= mask[index % mask.len()]; + } + Ok(payload) +} + +#[test] +fn session_end_command_writes_the_exact_typed_frame_without_claiming_completion() +-> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != br#"{"id":11,"method":"session.end","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "unexpected session.end command: {}", + String::from_utf8_lossy(&command) + ), + )); + } + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiSessionEndCommand::new(11)?; + let _established = command.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + )?; + + assert_eq!(correlation.outstanding_count(), 1); + server + .join() + .map_err(|_| io::Error::other("session.end command test server panicked"))??; + Ok(()) +} From e87b4459b98084ccb68075b0d37c7f704d6c77cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:24:37 -0700 Subject: [PATCH 2/9] style(network): apply canonical session.end test formatting --- .../tests/webdriver_bidi_session_end_command.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs index f481c18b8..89e26e917 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_command.rs @@ -8,9 +8,9 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; From 575ac294a62e73cc9d8dde05fafda440be49b6eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:31:39 -0700 Subject: [PATCH 3/9] test(network): cover session.end failure contracts --- ...river_bidi_session_end_command_failures.rs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs new file mode 100644 index 000000000..17b8f5dda --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs @@ -0,0 +1,128 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, + WebDriverBiDiSessionEndCommandError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; +type HandshakeOnlyServer = ( + WebDriverBiDiWebSocketEstablished, + thread::JoinHandle>, +); + +fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut request = Vec::new(); + let mut buffer = [0_u8; 512]; + while !request.ends_with(b"\r\n\r\n") { + let count = stream.read(&mut buffer)?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "client opening request ended before the header terminator", + )); + } + request.extend_from_slice(&buffer[..count]); + } + Ok(()) +} + +fn establish_with_handshake_only_server() -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + Ok((established, server)) +} + +#[test] +fn session_end_rejects_ids_above_the_webdriver_bidi_js_uint_range() { + let rejected = WebDriverBiDiSessionEndCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT + 1); + assert_eq!( + rejected.err().map(|error| error.to_string()).as_deref(), + Some("WebDriver BiDi session.end command id is outside the js-uint range") + ); +} + +#[test] +fn session_end_rejects_duplicate_correlation_before_any_frame_write() +-> Result<(), Box> { + let (established, server) = establish_with_handshake_only_server()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + let command = WebDriverBiDiSessionEndCommand::new(7)?; + + let error = command + .send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + ) + .err() + .ok_or_else(|| io::Error::other("duplicate correlation unexpectedly sent session.end"))?; + assert!(matches!( + error, + WebDriverBiDiSessionEndCommandError::Correlation { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("duplicate-correlation session.end server panicked"))??; + Ok(()) +} + +#[test] +fn session_end_preserves_registration_when_frame_timeout_is_invalid() +-> Result<(), Box> { + let (established, server) = establish_with_handshake_only_server()?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let command = WebDriverBiDiSessionEndCommand::new(11)?; + + let error = command + .send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::ZERO, + ) + .err() + .ok_or_else(|| io::Error::other("zero frame timeout unexpectedly sent session.end"))?; + assert!(matches!( + error, + WebDriverBiDiSessionEndCommandError::FrameWrite { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("invalid-timeout session.end server panicked"))??; + Ok(()) +} From 29aee517b3171fd29033d4ae8c59e49393727fb0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:32:15 -0700 Subject: [PATCH 4/9] feat(network): add typed session.end command --- .../src/webdriver_bidi_session_end_command.rs | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_session_end_command.rs diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs new file mode 100644 index 000000000..03337cd6b --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -0,0 +1,181 @@ +use std::{error::Error, fmt, time::Duration}; + +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketMaskKey, +}; + +const SESSION_END_METHOD: &str = "session.end"; + +/// One bounded WebDriver BiDi `session.end` command. +/// +/// The command is deliberately concrete rather than a generic JSON or arbitrary-method escape +/// hatch. It carries only a WebDriver BiDi `js-uint` correlation identifier and always serializes +/// the standards-defined empty parameter map. Successfully writing the frame does not claim that +/// the remote session ended; callers must wait for a separately validated correlated response. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiSessionEndCommand { + command_id: u64, +} + +impl WebDriverBiDiSessionEndCommand { + /// Construct one `session.end` command with a JavaScript-safe correlation identifier. + pub fn new(command_id: u64) -> Result { + if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { + return Err(WebDriverBiDiSessionEndCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }); + } + Ok(Self { command_id }) + } + + /// Return the exact local correlation identifier serialized for this command. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Register and write this exact command on an already established verified BiDi stream. + /// + /// Registration occurs before the first possible remote side effect. A correlation failure + /// therefore writes nothing. Once registration succeeds, any frame-write failure consumes the + /// transport and intentionally leaves the identifier outstanding: a partial or fully emitted + /// frame is ambiguous, so silently retiring the id could allow unsafe reuse. A successful write + /// also leaves the identifier outstanding until a later correlated response proves completion. + pub fn send( + self, + established: WebDriverBiDiWebSocketEstablished, + correlation: &mut WebDriverBiDiCommandCorrelation, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + ) -> Result { + correlation + .register_command(self.command_id) + .map_err(|source| WebDriverBiDiSessionEndCommandError::Correlation { source })?; + let message = self.serialized(); + established + .write_text_frame(&message, masking_key, frame_timeout) + .map_err(|source| WebDriverBiDiSessionEndCommandError::FrameWrite { source }) + } + + fn serialized(self) -> String { + format!( + "{{\"id\":{},\"method\":\"{SESSION_END_METHOD}\",\"params\":{{}}}}", + self.command_id + ) + } +} + +/// Fail-closed errors while constructing or sending one typed `session.end` command. +#[derive(Debug)] +pub enum WebDriverBiDiSessionEndCommandError { + /// The requested command identifier is outside WebDriver BiDi's `js-uint` range. + CommandIdOutOfRange { + /// Rejected command identifier. + command_id: u64, + /// Largest JavaScript-safe identifier admitted by this boundary. + maximum_command_id: u64, + }, + /// The bounded local correlation registry rejected the command before network I/O. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// Writing the already-registered command frame failed and the transport is not reusable. + FrameWrite { + /// Exact typed bounded WebSocket frame-write failure. + source: WebDriverBiDiWebSocketFrameError, + }, +} + +impl fmt::Display for WebDriverBiDiSessionEndCommandError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CommandIdOutOfRange { .. } => formatter + .write_str("WebDriver BiDi session.end command id is outside the js-uint range"), + Self::Correlation { .. } => formatter + .write_str("WebDriver BiDi session.end command correlation was rejected"), + Self::FrameWrite { .. } => { + formatter.write_str("WebDriver BiDi session.end command frame write failed") + } + } + } +} + +impl Error for WebDriverBiDiSessionEndCommandError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::CommandIdOutOfRange { .. } => None, + Self::Correlation { source } => Some(source), + Self::FrameWrite { source } => Some(source), + } + } +} + +#[cfg(test)] +mod tests { + use std::io; + + use super::*; + + #[test] + fn constructor_enforces_the_webdriver_bidi_js_uint_range() { + let accepted = WebDriverBiDiSessionEndCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT); + assert_eq!( + accepted.ok().map(|command| command.command_id()), + Some(MAX_WEBDRIVER_BIDI_JS_UINT) + ); + + let rejected = WebDriverBiDiSessionEndCommand::new(MAX_WEBDRIVER_BIDI_JS_UINT + 1); + assert_eq!( + rejected.err().map(|error| error.to_string()).as_deref(), + Some("WebDriver BiDi session.end command id is outside the js-uint range") + ); + } + + #[test] + fn command_serialization_is_static_and_exact() { + let command = WebDriverBiDiSessionEndCommand { command_id: 42 }; + assert_eq!(command.command_id(), 42); + assert_eq!( + command.serialized(), + r#"{"id":42,"method":"session.end","params":{}}"# + ); + } + + #[test] + fn command_errors_have_stable_messages_and_typed_sources() { + let range = WebDriverBiDiSessionEndCommandError::CommandIdOutOfRange { + command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }; + assert_eq!( + range.to_string(), + "WebDriver BiDi session.end command id is outside the js-uint range" + ); + assert!(range.source().is_none()); + + let correlation = WebDriverBiDiSessionEndCommandError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.end command correlation was rejected" + ); + assert!(correlation.source().is_some()); + + let frame = WebDriverBiDiSessionEndCommandError::FrameWrite { + source: WebDriverBiDiWebSocketFrameError::FrameWriteFailed { + bytes_written: 0, + source: io::Error::other("test frame failure"), + }, + }; + assert_eq!( + frame.to_string(), + "WebDriver BiDi session.end command frame write failed" + ); + assert!(frame.source().is_some()); + } +} From bd2a366284d7b3dbd480a60b15768c44073e550c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:32:35 -0700 Subject: [PATCH 5/9] feat(network): export typed session.end command --- crates/originweave-network/src/lib.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index a4a8f883e..3b14bd575 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -8,10 +8,10 @@ //! the RFC 6455 opening exchange, provides bounded masked client writes and //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response -//! correlation, sends one narrowly typed `session.status` command, and admits its -//! required readiness result through one command-specific correlated parser without -//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or Agent -//! authority. +//! correlation, sends narrowly typed `session.status` and `session.end` commands, +//! and admits the required readiness result through one command-specific correlated +//! parser without exposing generic JSON bodies or granting browser, TLS, policy, +//! secret, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -20,6 +20,7 @@ mod connection; mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; +mod webdriver_bidi_session_end_command; mod webdriver_bidi_session_status_command; mod webdriver_bidi_session_status_response; mod webdriver_bidi_websocket_frame; @@ -47,6 +48,9 @@ pub use webdriver_bidi_json_envelope::{ MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, }; +pub use webdriver_bidi_session_end_command::{ + WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndCommandError, +}; pub use webdriver_bidi_session_status_command::{ WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, }; From e3b14bbc28c3a8977edc78778040a289bab267d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:42:30 -0700 Subject: [PATCH 6/9] style(network): apply canonical session.end formatting --- .../src/webdriver_bidi_session_end_command.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index 03337cd6b..406b5b795 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -95,8 +95,9 @@ impl fmt::Display for WebDriverBiDiSessionEndCommandError { match self { Self::CommandIdOutOfRange { .. } => formatter .write_str("WebDriver BiDi session.end command id is outside the js-uint range"), - Self::Correlation { .. } => formatter - .write_str("WebDriver BiDi session.end command correlation was rejected"), + Self::Correlation { .. } => { + formatter.write_str("WebDriver BiDi session.end command correlation was rejected") + } Self::FrameWrite { .. } => { formatter.write_str("WebDriver BiDi session.end command frame write failed") } From 2daa2422f4515ab62b2ddc3b197800625297591a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:42:51 -0700 Subject: [PATCH 7/9] style(network): format session.end failure regressions --- .../tests/webdriver_bidi_session_end_command_failures.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs index 17b8f5dda..93fb1aef4 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_command_failures.rs @@ -71,8 +71,8 @@ fn session_end_rejects_ids_above_the_webdriver_bidi_js_uint_range() { } #[test] -fn session_end_rejects_duplicate_correlation_before_any_frame_write() --> Result<(), Box> { +fn session_end_rejects_duplicate_correlation_before_any_frame_write() -> Result<(), Box> +{ let (established, server) = establish_with_handshake_only_server()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(7)?; @@ -100,8 +100,8 @@ fn session_end_rejects_duplicate_correlation_before_any_frame_write() } #[test] -fn session_end_preserves_registration_when_frame_timeout_is_invalid() --> Result<(), Box> { +fn session_end_preserves_registration_when_frame_timeout_is_invalid() -> Result<(), Box> +{ let (established, server) = establish_with_handshake_only_server()?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let command = WebDriverBiDiSessionEndCommand::new(11)?; From edec535b8d8b3c843f9f9baf8562f31ff5c7af23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:55:25 +0900 Subject: [PATCH 8/9] test(network): reject replacement status replies in session end stack Require pending-state preservation and acceptance of the original connection reply after rejection. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../webdriver_bidi_session_status_response.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 347dc5fec..1c1842e42 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -175,3 +175,20 @@ fn empty_status_result_fails_before_consuming_the_outstanding_command() -> Resul assert_eq!(correlation.outstanding_count(), 1); Ok(()) } + +#[test] +fn replacement_status_reply_preserves_original_pending_request_and_recovery() +-> Result<(), Box> { + let (original, mut pending) = send_status_and_read_response(STATUS_RESPONSE)?; + let (replacement, _replacement_pending) = send_status_and_read_response(STATUS_RESPONSE)?; + + assert!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending).is_err(), + "a replacement connection must not complete the original status request" + ); + assert_eq!(pending.outstanding_count(), 1); + let completed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&original, &mut pending)?; + assert_eq!(completed.command_id(), 7); + assert_eq!(pending.outstanding_count(), 0); + Ok(()) +} From 924ad97551750d4a901ded38b89488cc5438e54f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:57:25 +0900 Subject: [PATCH 9/9] docs: bound session end parent adoption evidence Require exact replacement-connection rejection without claiming stream liveness or end-response provenance. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../tests/webdriver_bidi_session_status_response.rs | 12 ++++++++---- docs/doctoring.md | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8a809101..ad12a72bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- The session-ending command stack now retains the status-reply protections from its current parent. A reply from a replacement connection is rejected while the original pending status request remains recoverable; sending the end command still does not prove that the browser session ended. - Typed outbound WebDriver BiDi `session.end` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, rejects invalid frame deadlines before correlation registration, retires only the just-registered id when frame preflight proves no command bytes were emitted, preserves exact command-kind correlation across ambiguous writes, and does not treat frame-write success as proof that the browser session ended. - Regression checks now exercise fragmented browser replies, interleaved control messages, and rejected replies without losing a pending request. These checks do not establish browser readiness or release acceptance. - The typed browser-status response stack now includes its verified command and opening-exchange prerequisites, including the release-record check that previously did not execute; parsing remains bounded and does not grant browser authority or prove operational readiness. diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index ff7a9dd34..6779da6c1 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -231,10 +231,14 @@ fn replacement_status_reply_preserves_original_pending_request_and_recovery() let (original, mut pending) = send_status_and_read_response(STATUS_RESPONSE)?; let (replacement, _replacement_pending) = send_status_and_read_response(STATUS_RESPONSE)?; - assert!( - WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending).is_err(), - "a replacement connection must not complete the original status request" - ); + assert!(matches!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&replacement, &mut pending), + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: originweave_network::WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7, + }, + }) + )); assert_eq!(pending.outstanding_count(), 1); let completed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&original, &mut pending)?; assert_eq!(completed.command_id(), 7); diff --git a/docs/doctoring.md b/docs/doctoring.md index 0950b9a2d..ffe0f82ee 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -128,6 +128,12 @@ On 6 September 2026, that historical no-connection-provenance state was supersed The #251 integration adopts #250 `ec433b844a121f8554c062f92267991af9cacb6f` by ordinary merge and retains both release records. Native discovery on predecessor `86e8ad76838f2a64aa7e0cd56ba1f931c8d0c3dc` collected zero command-correlation release tests; the current parent supplies the existing discoverable TestCase and synchronized opening-exchange fixtures. The session-end sender, its public exports and both child-owned Rust integration tests remain unchanged. The sender still registers only the exact typed command, retires correlation only after proven preflight rejection, and leaves ambiguous writes outstanding. A successful frame write does not prove session termination, browser-process exit, profile deletion, authenticated connection provenance or real Chromium acceptance. Local verification and current-head hosted checks remain separate prerequisites; earlier stack results do not transfer. +### Current session-end sender adoption of status-reply provenance + +On 7 September 2026, the session-end sender stack reproduced the inherited status-reply gap at `edec535b8d8b3c843f9f9baf8562f31ff5c7af23`: two real loopback connections using the same session and command id allowed the replacement reply to complete the original pending status request. The ordinary merge of #250 `bbdc6ace7a5932adf24836700f806850e6b230bc` preserves the session-end sender and both original sender integration-test files byte-for-byte while inheriting sealed status receipts and connection-aware correlation. The regression now requires the exact connection-mismatch error, unchanged pending count, and subsequent completion using the original received reply. The server fixtures have already finished; this proves retained reply/correlation recovery, not liveness of an open original stream or same-endpoint replacement coverage. + +The shared routing validation, result projection order, and parent connection safeguards are unchanged. This supersedes the earlier no-received-provenance description for the status path only: the session-end sender still makes no received-acknowledgment, process-exit, profile-cleanup, browser-policy or release-acceptance claim. End-response provenance belongs to its separate consumer. Local tests and exact-head hosted checks must be evaluated independently after this adoption. + ## References Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html