From 9ab369c4dc406aef19dcecacc50304224c060242 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:16:13 +0900 Subject: [PATCH 01/36] test(network): require navigation committed subscription --- ..._bidi_navigation_committed_subscription.rs | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs new file mode 100644 index 000000000..f14df6746 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -0,0 +1,147 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCorrelatedResponseOutcome, + WebDriverBiDiJsonEnvelope, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, +}; + +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"; +const SUBSCRIBE_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#; + +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 navigation_committed_subscription_round_trips_on_the_registered_context() +-> 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":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "unexpected session.subscribe command: {}", + String::from_utf8_lossy(&command) + ), + )); + } + stream.write_all(&[0x81, SUBSCRIBE_RESPONSE.len() as u8])?; + stream.write_all(SUBSCRIBE_RESPONSE) + }); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, "context-a")?; + + 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 = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + "context-a", + )?; + let established = command.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 1); + + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "session.subscribe response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; + let completed = correlation.correlate_response(&envelope)?; + assert_eq!(completed.command_id(), 7); + assert_eq!( + completed.outcome(), + WebDriverBiDiCorrelatedResponseOutcome::Success + ); + assert_eq!(correlation.outstanding_count(), 0); + + server + .join() + .map_err(|_| io::Error::other("session.subscribe command test server panicked"))??; + Ok(()) +} From 911dedda15951b97655a8d8d9696e276318e2fd4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:19:04 +0900 Subject: [PATCH 02/36] feat(network): add context-bound navigation subscription --- ..._bidi_navigation_committed_subscription.rs | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs new file mode 100644 index 000000000..571d78e0a --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -0,0 +1,320 @@ +use std::{error::Error, fmt, time::Duration}; + +use originweave_core::{ + BrowserAuthorityRegistry, BrowserRegistryError, BrowserSessionId, BrowsingContextId, +}; + +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WEBDRIVER_BIDI_NAVIGATION_COMMITTED_METHOD, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiWebSocketMaskKey, +}; + +const SESSION_SUBSCRIBE_METHOD: &str = "session.subscribe"; + +/// One context-scoped subscription for the committed-navigation WebDriver BiDi event. +/// +/// This command is deliberately narrower than the protocol's generic `session.subscribe` surface: +/// it can request only `browsingContext.navigationCommitted`, for one external context that already +/// maps to the exact supplied OriginWeave session/context pair. It does not expose arbitrary event +/// names, global subscriptions, user-context subscriptions, generic JSON, or arbitrary method +/// dispatch. Successful construction or transport does not authenticate Chromium, authorize a +/// navigation, grant destination or policy authority, or make later event data reusable Agent +/// authority. +pub struct WebDriverBiDiNavigationCommittedSubscriptionCommand { + command_id: u64, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + external_context: String, +} + +impl WebDriverBiDiNavigationCommittedSubscriptionCommand { + /// Construct one bounded context-scoped committed-navigation subscription command. + /// + /// The external protocol identifier must already name the exact registered OriginWeave + /// session/context pair. No registry state is created as a side effect of untrusted adapter text. + pub fn new( + command_id: u64, + registry: &BrowserAuthorityRegistry, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + external_context: &str, + ) -> Result { + if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }, + ); + } + require_registered_context( + registry, + browser_session, + browsing_context, + external_context, + )?; + Ok(Self { + command_id, + browser_session, + browsing_context, + external_context: external_context.to_owned(), + }) + } + + /// Return the exact local correlation identifier serialized by this command. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return the exact registered OriginWeave browser session bound during construction. + #[must_use] + pub const fn browser_session(&self) -> BrowserSessionId { + self.browser_session + } + + /// Return the exact registered OriginWeave browsing context bound during construction. + #[must_use] + pub const fn browsing_context(&self) -> BrowsingContextId { + self.browsing_context + } + + /// Borrow the exact external WebDriver BiDi context identifier serialized by this command. + #[must_use] + pub fn external_context(&self) -> &str { + &self.external_context + } + + /// Revalidate, register, and write this exact subscription on an established verified BiDi stream. + /// + /// Context binding is revalidated immediately before command correlation and network I/O so a + /// command retained across registry retirement cannot subscribe a stale or replacement context. + /// Correlation registration then occurs before the first possible remote side effect. A binding + /// or correlation failure therefore writes nothing. After successful registration, a frame-write + /// failure consumes the transport and intentionally leaves the identifier outstanding because a + /// partial or fully emitted frame has ambiguous remote effect. + pub fn send( + self, + registry: &BrowserAuthorityRegistry, + established: WebDriverBiDiWebSocketEstablished, + correlation: &mut WebDriverBiDiCommandCorrelation, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + ) -> Result< + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiNavigationCommittedSubscriptionCommandError, + > { + require_registered_context( + registry, + self.browser_session, + self.browsing_context, + &self.external_context, + )?; + correlation + .register_command(self.command_id) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { source } + })?; + let message = self.serialized(); + established + .write_text_frame(&message, masking_key, frame_timeout) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { source } + }) + } + + fn serialized(&self) -> String { + let mut message = format!( + "{{\"id\":{},\"method\":\"{SESSION_SUBSCRIBE_METHOD}\",\"params\":{{\"events\":[\"{WEBDRIVER_BIDI_NAVIGATION_COMMITTED_METHOD}\"],\"contexts\":[", + self.command_id + ); + push_json_string(&mut message, &self.external_context); + message.push_str("]}}"); + message + } +} + +fn require_registered_context( + registry: &BrowserAuthorityRegistry, + browser_session: BrowserSessionId, + browsing_context: BrowsingContextId, + external_context: &str, +) -> Result<(), WebDriverBiDiNavigationCommittedSubscriptionCommandError> { + registry + .require_registered_context_external_identifier( + browser_session, + browsing_context, + external_context, + ) + .map_err( + |source| WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { + source, + }, + ) +} + +fn push_json_string(target: &mut String, value: &str) { + target.push('"'); + for character in value.chars() { + match character { + '"' => target.push_str("\\\""), + '\\' => target.push_str("\\\\"), + _ => target.push(character), + } + } + target.push('"'); +} + +/// Fail-closed failures while constructing or sending one typed committed-navigation subscription. +#[derive(Debug)] +pub enum WebDriverBiDiNavigationCommittedSubscriptionCommandError { + /// 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 external protocol context does not name the exact registered OriginWeave context. + ContextBinding { + /// Exact typed browser-registry authority failure. + source: BrowserRegistryError, + }, + /// 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 WebDriverBiDiNavigationCommittedSubscriptionCommandError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CommandIdOutOfRange { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription command id is outside the js-uint range", + ), + Self::ContextBinding { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription context does not match registered authority", + ), + Self::Correlation { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription command correlation was rejected", + ), + Self::FrameWrite { .. } => formatter.write_str( + "WebDriver BiDi navigation subscription command frame write failed", + ), + } + } +} + +impl Error for WebDriverBiDiNavigationCommittedSubscriptionCommandError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::CommandIdOutOfRange { .. } => None, + Self::ContextBinding { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::FrameWrite { source } => Some(source), + } + } +} + +#[cfg(test)] +mod tests { + use std::io; + + use super::*; + + #[test] + fn constructor_binds_the_exact_registered_context_and_js_uint_range() { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session("session-a").unwrap(); + let context = registry.register_context(session, "context-a").unwrap(); + + let accepted = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + MAX_WEBDRIVER_BIDI_JS_UINT, + ®istry, + session, + context, + "context-a", + ); + let command = accepted.unwrap(); + assert_eq!(command.command_id(), MAX_WEBDRIVER_BIDI_JS_UINT); + assert_eq!(command.browser_session(), session); + assert_eq!(command.browsing_context(), context); + assert_eq!(command.external_context(), "context-a"); + + let range = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + MAX_WEBDRIVER_BIDI_JS_UINT + 1, + ®istry, + session, + context, + "context-a", + ); + assert_eq!( + range.err().map(|error| error.to_string()).as_deref(), + Some("WebDriver BiDi navigation subscription command id is outside the js-uint range") + ); + + let mismatch = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 1, + ®istry, + session, + context, + "context-b", + ); + assert_eq!( + mismatch.err().map(|error| error.to_string()).as_deref(), + Some( + "WebDriver BiDi navigation subscription context does not match registered authority" + ) + ); + } + + #[test] + fn serialization_is_narrow_exact_and_json_escapes_the_registered_context() { + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand { + command_id: 42, + browser_session: BrowserSessionId::new(1).unwrap(), + browsing_context: BrowsingContextId::new(1).unwrap(), + external_context: "context-\"a\\b".to_owned(), + }; + assert_eq!( + command.serialized(), + r#"{"id":42,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-\"a\\b"]}}"# + ); + } + + #[test] + fn command_errors_have_stable_messages_and_typed_sources() { + let range = + WebDriverBiDiNavigationCommittedSubscriptionCommandError::CommandIdOutOfRange { + command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }; + assert!(range.source().is_none()); + + let context = WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { + source: BrowserRegistryError::UnknownBrowserSession, + }; + assert!(context.source().is_some()); + + let correlation = WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding, + }; + assert!(correlation.source().is_some()); + + let frame = WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { + source: WebDriverBiDiWebSocketFrameError::FrameWriteFailed { + bytes_written: 0, + source: io::Error::other("test frame failure"), + }, + }; + assert!(frame.source().is_some()); + } +} From d45a0da02d53832db945247299c3ead9ad91f7a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:19:30 +0900 Subject: [PATCH 03/36] feat(network): export navigation subscription boundary --- crates/originweave-network/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 1e94f6075..991525cdf 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -9,7 +9,8 @@ //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, transports a narrowly typed pointer click, admits its typed -//! correlated protocol acknowledgment, admits a bounded navigation-committed +//! correlated protocol acknowledgment, sends a context-bound subscription for +//! committed-navigation events, admits a bounded navigation-committed //! post-condition observation for one exact registered context and URL, rotates //! the matched context's document epoch only from an exact caller-captured //! pre-action epoch, derives and binds the committed HTTP(S) URL's canonical @@ -28,6 +29,7 @@ mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; mod webdriver_bidi_navigation_committed_postcondition; +mod webdriver_bidi_navigation_committed_subscription; mod webdriver_bidi_navigation_document_advance; mod webdriver_bidi_navigation_document_origin; mod webdriver_bidi_pointer_click_response; @@ -69,6 +71,10 @@ pub use webdriver_bidi_navigation_committed_postcondition::{ WebDriverBiDiNavigationCommittedObservationError, WebDriverBiDiNavigationCommittedProjectionError, }; +pub use webdriver_bidi_navigation_committed_subscription::{ + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionCommandError, +}; pub use webdriver_bidi_navigation_document_advance::{ WebDriverBiDiNavigationCommittedDocumentAdvance, WebDriverBiDiNavigationCommittedDocumentAdvanceError, From 87e23d256bd05f545597f127f182d40bba6c0d33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:20:14 +0900 Subject: [PATCH 04/36] test(network): revalidate subscription context before send --- .../tests/webdriver_bidi_navigation_committed_subscription.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index f14df6746..0d0015e94 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -113,6 +113,7 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() "context-a", )?; let established = command.send( + ®istry, established, &mut correlation, WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), From e648e9c206c3e4eba55a4cc61019461af1155863 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:22:46 +0900 Subject: [PATCH 05/36] style(network): apply subscription rustfmt diagnostics --- ..._bidi_navigation_committed_subscription.rs | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index 571d78e0a..3283511fc 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -148,11 +148,9 @@ fn require_registered_context( browsing_context, external_context, ) - .map_err( - |source| WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { - source, - }, - ) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { source } + }) } fn push_json_string(target: &mut String, value: &str) { @@ -231,19 +229,19 @@ mod tests { use super::*; #[test] - fn constructor_binds_the_exact_registered_context_and_js_uint_range() { + fn constructor_binds_the_exact_registered_context_and_js_uint_range() + -> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); - let session = registry.register_session("session-a").unwrap(); - let context = registry.register_context(session, "context-a").unwrap(); + let session = registry.register_session("session-a")?; + let context = registry.register_context(session, "context-a")?; - let accepted = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( MAX_WEBDRIVER_BIDI_JS_UINT, ®istry, session, context, "context-a", - ); - let command = accepted.unwrap(); + )?; assert_eq!(command.command_id(), MAX_WEBDRIVER_BIDI_JS_UINT); assert_eq!(command.browser_session(), session); assert_eq!(command.browsing_context(), context); @@ -274,29 +272,34 @@ mod tests { "WebDriver BiDi navigation subscription context does not match registered authority" ) ); + Ok(()) } #[test] - fn serialization_is_narrow_exact_and_json_escapes_the_registered_context() { + fn serialization_is_narrow_exact_and_json_escapes_the_registered_context() + -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session("session-a")?; + let context = registry.register_context(session, "context-a")?; let command = WebDriverBiDiNavigationCommittedSubscriptionCommand { command_id: 42, - browser_session: BrowserSessionId::new(1).unwrap(), - browsing_context: BrowsingContextId::new(1).unwrap(), + browser_session: session, + browsing_context: context, external_context: "context-\"a\\b".to_owned(), }; assert_eq!( command.serialized(), r#"{"id":42,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-\"a\\b"]}}"# ); + Ok(()) } #[test] fn command_errors_have_stable_messages_and_typed_sources() { - let range = - WebDriverBiDiNavigationCommittedSubscriptionCommandError::CommandIdOutOfRange { - command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, - maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, - }; + let range = WebDriverBiDiNavigationCommittedSubscriptionCommandError::CommandIdOutOfRange { + command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }; assert!(range.source().is_none()); let context = WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { From b7d94c98bd8737d1ac31336f4e7b8b123a4f63af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:39:27 +0900 Subject: [PATCH 06/36] test(network): remove synthetic subscription coverage residual --- ..._bidi_navigation_committed_subscription.rs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index 3283511fc..8dfd94578 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -241,11 +241,30 @@ mod tests { session, context, "context-a", - )?; - assert_eq!(command.command_id(), MAX_WEBDRIVER_BIDI_JS_UINT); - assert_eq!(command.browser_session(), session); - assert_eq!(command.browsing_context(), context); - assert_eq!(command.external_context(), "context-a"); + ); + assert!(command.is_ok()); + assert_eq!( + command.as_ref().map(|command| command.command_id()).ok(), + Some(MAX_WEBDRIVER_BIDI_JS_UINT) + ); + assert_eq!( + command.as_ref().map(|command| command.browser_session()).ok(), + Some(session) + ); + assert_eq!( + command + .as_ref() + .map(|command| command.browsing_context()) + .ok(), + Some(context) + ); + assert_eq!( + command + .as_ref() + .map(|command| command.external_context()) + .ok(), + Some("context-a") + ); let range = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( MAX_WEBDRIVER_BIDI_JS_UINT + 1, From d3a824355279c62b689c54456a7a177169b0bf04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:40:11 +0900 Subject: [PATCH 07/36] test(network): cover subscription send failure contracts --- ...igation_committed_subscription_failures.rs | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs new file mode 100644 index 000000000..804c8c544 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs @@ -0,0 +1,212 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionCommand, + 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"; + +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 require_no_client_command(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut byte = [0_u8; 1]; + match stream.read(&mut byte) { + Ok(0) => Ok(()), + Ok(_) => Err(io::Error::new( + io::ErrorKind::InvalidData, + "subscription command was written despite local rejection", + )), + Err(source) + if matches!( + source.kind(), + io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted + ) => + { + Ok(()) + } + Err(source) => Err(source), + } +} + +fn spawn_no_command_server(listener: TcpListener) -> thread::JoinHandle> { + thread::spawn(move || { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + require_no_client_command(&mut stream) + }) +} + +fn establish_websocket( + local_addr: SocketAddr, +) -> Result> { + 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)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +#[test] +fn retired_context_is_rejected_before_correlation_or_command_write() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_no_command_server(listener); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, "context-a")?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + "context-a", + )?; + let established = establish_websocket(local_addr)?; + registry.remove_context(context)?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let result = command.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + ); + let error = match result { + Ok(_) => return Err(io::Error::other("retired context unexpectedly sent subscription").into()), + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi navigation subscription context does not match registered authority" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 0); + + server + .join() + .map_err(|_| io::Error::other("retired-context test server panicked"))??; + Ok(()) +} + +#[test] +fn duplicate_command_id_is_rejected_before_command_write() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_no_command_server(listener); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, "context-a")?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + "context-a", + )?; + let established = establish_websocket(local_addr)?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + let result = command.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + ); + let error = match result { + Ok(_) => return Err(io::Error::other("duplicate command id unexpectedly sent subscription").into()), + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi navigation subscription command correlation was rejected" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("duplicate-command test server panicked"))??; + Ok(()) +} + +#[test] +fn invalid_frame_timeout_consumes_transport_and_retains_correlation() -> Result<(), Box> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_no_command_server(listener); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, "context-a")?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + "context-a", + )?; + let established = establish_websocket(local_addr)?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let result = command.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::ZERO, + ); + let error = match result { + Ok(_) => return Err(io::Error::other("zero frame timeout unexpectedly sent subscription").into()), + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi navigation subscription command frame write failed" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("frame-write test server panicked"))??; + Ok(()) +} From 64974f93a698a6dd8ea834e4e4ef78e8ddb38f8d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 13:45:45 +0900 Subject: [PATCH 08/36] style(network): apply canonical subscription formatting --- ..._bidi_navigation_committed_subscription.rs | 5 ++++- ...igation_committed_subscription_failures.rs | 19 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index 8dfd94578..cdd6e4631 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -248,7 +248,10 @@ mod tests { Some(MAX_WEBDRIVER_BIDI_JS_UINT) ); assert_eq!( - command.as_ref().map(|command| command.browser_session()).ok(), + command + .as_ref() + .map(|command| command.browser_session()) + .ok(), Some(session) ); assert_eq!( diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs index 804c8c544..2d85a62d8 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_failures.rs @@ -108,7 +108,9 @@ fn retired_context_is_rejected_before_correlation_or_command_write() -> Result<( Duration::from_millis(500), ); let error = match result { - Ok(_) => return Err(io::Error::other("retired context unexpectedly sent subscription").into()), + Ok(_) => { + return Err(io::Error::other("retired context unexpectedly sent subscription").into()); + } Err(error) => error, }; assert_eq!( @@ -152,7 +154,11 @@ fn duplicate_command_id_is_rejected_before_command_write() -> Result<(), Box return Err(io::Error::other("duplicate command id unexpectedly sent subscription").into()), + Ok(_) => { + return Err( + io::Error::other("duplicate command id unexpectedly sent subscription").into(), + ); + } Err(error) => error, }; assert_eq!( @@ -169,7 +175,8 @@ fn duplicate_command_id_is_rejected_before_command_write() -> Result<(), Box Result<(), Box> { +fn invalid_frame_timeout_consumes_transport_and_retains_correlation() -> Result<(), Box> +{ let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = spawn_no_command_server(listener); @@ -195,7 +202,11 @@ fn invalid_frame_timeout_consumes_transport_and_retains_correlation() -> Result< Duration::ZERO, ); let error = match result { - Ok(_) => return Err(io::Error::other("zero frame timeout unexpectedly sent subscription").into()), + Ok(_) => { + return Err( + io::Error::other("zero frame timeout unexpectedly sent subscription").into(), + ); + } Err(error) => error, }; assert_eq!( From 48a65d170e9d2a5c0e696ab8930fb534653878ad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 14:07:56 +0900 Subject: [PATCH 09/36] test(network): exercise subscription production surface --- ..._bidi_navigation_committed_subscription.rs | 53 ++++++++++++++++--- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index 0d0015e94..6fd490dfe 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -8,14 +8,16 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCorrelatedResponseOutcome, - WebDriverBiDiJsonEnvelope, WebDriverBiDiNavigationCommittedSubscriptionCommand, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-\"a\\b"; 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"; const SUBSCRIBE_RESPONSE: &[u8] = @@ -75,7 +77,7 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() stream.write_all(OPENING_RESPONSE)?; let command = read_masked_text_frame(&mut stream)?; if command - != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-\"a\\b"]}}"# { return Err(io::Error::new( io::ErrorKind::InvalidData, @@ -91,7 +93,7 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() let mut registry = BrowserAuthorityRegistry::new(); let session = registry.register_session(SESSION_ID)?; - let context = registry.register_context(session, "context-a")?; + let context = registry.register_context(session, CONTEXT_ID)?; let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? @@ -110,8 +112,12 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() ®istry, session, context, - "context-a", + CONTEXT_ID, )?; + assert_eq!(command.command_id(), 7); + assert_eq!(command.browser_session(), session); + assert_eq!(command.browsing_context(), context); + assert_eq!(command.external_context(), CONTEXT_ID); let established = command.send( ®istry, established, @@ -146,3 +152,34 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() .map_err(|_| io::Error::other("session.subscribe command test server panicked"))??; Ok(()) } + +#[test] +fn subscription_constructor_rejects_out_of_range_command_id_without_source() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + + let result = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + MAX_WEBDRIVER_BIDI_JS_UINT + 1, + ®istry, + session, + context, + CONTEXT_ID, + ); + let error = match result { + Ok(_) => { + return Err(io::Error::other( + "out-of-range session.subscribe command id was unexpectedly accepted", + ) + .into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi navigation subscription command id is outside the js-uint range" + ); + assert!(error.source().is_none()); + Ok(()) +} From 4ae26f68f6bfda0e758f29a3ba11bb14c11f61a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 14:08:41 +0900 Subject: [PATCH 10/36] test(network): keep subscription coverage on public boundary --- ..._bidi_navigation_committed_subscription.rs | 122 ------------------ 1 file changed, 122 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs index cdd6e4631..fae6b9bd0 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription.rs @@ -221,125 +221,3 @@ impl Error for WebDriverBiDiNavigationCommittedSubscriptionCommandError { } } } - -#[cfg(test)] -mod tests { - use std::io; - - use super::*; - - #[test] - fn constructor_binds_the_exact_registered_context_and_js_uint_range() - -> Result<(), Box> { - let mut registry = BrowserAuthorityRegistry::new(); - let session = registry.register_session("session-a")?; - let context = registry.register_context(session, "context-a")?; - - let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - MAX_WEBDRIVER_BIDI_JS_UINT, - ®istry, - session, - context, - "context-a", - ); - assert!(command.is_ok()); - assert_eq!( - command.as_ref().map(|command| command.command_id()).ok(), - Some(MAX_WEBDRIVER_BIDI_JS_UINT) - ); - assert_eq!( - command - .as_ref() - .map(|command| command.browser_session()) - .ok(), - Some(session) - ); - assert_eq!( - command - .as_ref() - .map(|command| command.browsing_context()) - .ok(), - Some(context) - ); - assert_eq!( - command - .as_ref() - .map(|command| command.external_context()) - .ok(), - Some("context-a") - ); - - let range = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - MAX_WEBDRIVER_BIDI_JS_UINT + 1, - ®istry, - session, - context, - "context-a", - ); - assert_eq!( - range.err().map(|error| error.to_string()).as_deref(), - Some("WebDriver BiDi navigation subscription command id is outside the js-uint range") - ); - - let mismatch = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - 1, - ®istry, - session, - context, - "context-b", - ); - assert_eq!( - mismatch.err().map(|error| error.to_string()).as_deref(), - Some( - "WebDriver BiDi navigation subscription context does not match registered authority" - ) - ); - Ok(()) - } - - #[test] - fn serialization_is_narrow_exact_and_json_escapes_the_registered_context() - -> Result<(), Box> { - let mut registry = BrowserAuthorityRegistry::new(); - let session = registry.register_session("session-a")?; - let context = registry.register_context(session, "context-a")?; - let command = WebDriverBiDiNavigationCommittedSubscriptionCommand { - command_id: 42, - browser_session: session, - browsing_context: context, - external_context: "context-\"a\\b".to_owned(), - }; - assert_eq!( - command.serialized(), - r#"{"id":42,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-\"a\\b"]}}"# - ); - Ok(()) - } - - #[test] - fn command_errors_have_stable_messages_and_typed_sources() { - let range = WebDriverBiDiNavigationCommittedSubscriptionCommandError::CommandIdOutOfRange { - command_id: MAX_WEBDRIVER_BIDI_JS_UINT + 1, - maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, - }; - assert!(range.source().is_none()); - - let context = WebDriverBiDiNavigationCommittedSubscriptionCommandError::ContextBinding { - source: BrowserRegistryError::UnknownBrowserSession, - }; - assert!(context.source().is_some()); - - let correlation = WebDriverBiDiNavigationCommittedSubscriptionCommandError::Correlation { - source: WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding, - }; - assert!(correlation.source().is_some()); - - let frame = WebDriverBiDiNavigationCommittedSubscriptionCommandError::FrameWrite { - source: WebDriverBiDiWebSocketFrameError::FrameWriteFailed { - bytes_written: 0, - source: io::Error::other("test frame failure"), - }, - }; - assert!(frame.source().is_some()); - } -} From 2ea92c1adcf08aaeab6e9024e01c3666e387dfd0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 14:13:30 +0900 Subject: [PATCH 11/36] test(network): accept bounded extended BiDi command frames --- ..._bidi_navigation_committed_subscription.rs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index 6fd490dfe..fce7dd656 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -49,13 +49,20 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { "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 length = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required 64-bit framing", + )); + } + }; let mut mask = [0_u8; 4]; stream.read_exact(&mut mask)?; let mut payload = vec![0_u8; length]; @@ -108,11 +115,7 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() let mut correlation = WebDriverBiDiCommandCorrelation::new(); let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - 7, - ®istry, - session, - context, - CONTEXT_ID, + 7, ®istry, session, context, CONTEXT_ID, )?; assert_eq!(command.command_id(), 7); assert_eq!(command.browser_session(), session); From b50aa56eb7a3f703d62d8b6da23389bc2e3031d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 14:19:39 +0900 Subject: [PATCH 12/36] test(network): cover subscription constructor authority failure --- ..._bidi_navigation_committed_subscription.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index fce7dd656..b5fa98a04 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -186,3 +186,33 @@ fn subscription_constructor_rejects_out_of_range_command_id_without_source() assert!(error.source().is_none()); Ok(()) } + +#[test] +fn subscription_constructor_rejects_mismatched_registered_context() -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + + let result = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + "different-context", + ); + let error = match result { + Ok(_) => { + return Err(io::Error::other( + "mismatched session.subscribe context was unexpectedly accepted", + ) + .into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi navigation subscription context does not match registered authority" + ); + assert!(error.source().is_some()); + Ok(()) +} From efc6e8cef641184938e5ceb6ae9b75a03b8c0a67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:06:57 +0900 Subject: [PATCH 13/36] test(network): require typed subscribe result --- ...er_bidi_navigation_committed_subscription.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index b5fa98a04..4e5936ed4 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -9,8 +9,8 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, - WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -141,13 +141,12 @@ fn navigation_committed_subscription_round_trips_on_the_registered_context() .into()); } }; - let envelope = WebDriverBiDiJsonEnvelope::parse(&text)?; - let completed = correlation.correlate_response(&envelope)?; - assert_eq!(completed.command_id(), 7); - assert_eq!( - completed.outcome(), - WebDriverBiDiCorrelatedResponseOutcome::Success - ); + let result = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &text, + &mut correlation, + )?; + assert_eq!(result.command_id(), 7); + assert_eq!(result.subscription_id(), "subscription-a"); assert_eq!(correlation.outstanding_count(), 0); server From 7912633caccb0897f86312150d024aa109d13342 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:12:29 +0900 Subject: [PATCH 14/36] feat(network): retain typed BiDi subscription id --- ...igation_committed_subscription_response.rs | 745 ++++++++++++++++++ 1 file changed, 745 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs new file mode 100644 index 000000000..2b97b6811 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -0,0 +1,745 @@ +use std::{error::Error, fmt}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiWebSocketTextMessage, +}; + +/// Maximum decoded UTF-8 bytes retained from a WebDriver BiDi `session.Subscription` identifier. +/// +/// WebDriver BiDi defines the identifier as opaque text without a protocol size ceiling. OriginWeave +/// therefore applies a reviewed local retention bound while preserving the identifier byte-for-byte +/// for later typed subscription lifecycle work. The value is never included in `Debug` output. +pub const MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES: usize = 4_096; + +/// Typed, correlated successful result of one context-scoped WebDriver BiDi `session.subscribe`. +/// +/// This value retains only the exact correlated command id and the bounded opaque subscription +/// identifier returned by the remote end. It does not expose a generic JSON result, grant event, +/// browser, policy, origin, secret, or Agent authority, or prove that any subscribed event has fired. +#[derive(Eq, PartialEq)] +pub struct WebDriverBiDiNavigationCommittedSubscriptionResult { + command_id: u64, + subscription_id: String, +} + +impl fmt::Debug for WebDriverBiDiNavigationCommittedSubscriptionResult { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiNavigationCommittedSubscriptionResult") + .field("command_id", &self.command_id) + .field("subscription_id_len", &self.subscription_id.len()) + .finish() + } +} + +impl WebDriverBiDiNavigationCommittedSubscriptionResult { + /// Parse one bounded local-end message and consume its exact outstanding command on success. + /// + /// Common WebDriver BiDi envelope validation runs first. A successful envelope then undergoes + /// command-specific projection of the required `result.subscription` text before correlation is + /// consumed, so malformed or ambiguous success bodies cannot silently retire a command id. A + /// correlatable protocol-error response consumes its matching id and returns a typed remote + /// failure retaining only the protocol error code. Events, null-id errors, malformed envelopes, + /// and unknown ids fail closed without consuming unrelated outstanding correlation state. + pub fn parse_and_correlate( + message: &WebDriverBiDiWebSocketTextMessage, + correlation: &mut WebDriverBiDiCommandCorrelation, + ) -> Result { + let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { source } + })?; + + match envelope.kind() { + WebDriverBiDiJsonEnvelopeKind::Success => { + let projected = SubscriptionProjection::parse(message.as_str())?; + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source, + } + })?; + Ok(Self { + command_id: completed.command_id(), + subscription_id: projected.subscription_id, + }) + } + WebDriverBiDiJsonEnvelopeKind::Error => { + let error_code = retain_validated_error_code(envelope.error_code())?; + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source, + } + })?; + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { + command_id: completed.command_id(), + error_code, + }, + ) + } + WebDriverBiDiJsonEnvelopeKind::Event => Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + }, + ), + } + } + + /// Return the exact local command identifier consumed by this result. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Borrow the bounded opaque subscription identifier returned by the remote end. + #[must_use] + pub fn subscription_id(&self) -> &str { + &self.subscription_id + } +} + +/// Fail-closed failures while admitting one typed WebDriver BiDi `session.subscribe` response. +#[derive(Debug)] +pub enum WebDriverBiDiNavigationCommittedSubscriptionResponseError { + /// Common local-end JSON envelope validation failed. + Envelope { + /// Exact common-envelope validation failure. + source: WebDriverBiDiJsonEnvelopeError, + }, + /// The successful result object omits the required `subscription` member. + MissingSubscription, + /// The successful result object's `subscription` member is not JSON text. + InvalidSubscription, + /// The successful result repeats the `subscription` member and is ambiguous. + DuplicateSubscription, + /// The decoded subscription identifier exceeds the reviewed local retention bound. + SubscriptionTooLarge { + /// Maximum decoded identifier length admitted in bytes. + maximum_bytes: usize, + }, + /// A validated success envelope could not be projected through the command-specific parser. + InvalidResultProjection, + /// Exact command-response correlation failed without consuming unrelated state. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// The remote end returned a correlatable WebDriver BiDi protocol error for this command. + RemoteProtocolError { + /// Exact local command identifier consumed by the protocol-error response. + command_id: u64, + /// Protocol error code retained from the already validated common envelope. + error_code: String, + }, +} + +impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Envelope { .. } => { + formatter.write_str("WebDriver BiDi session.subscribe envelope is invalid") + } + Self::MissingSubscription => formatter + .write_str("WebDriver BiDi session.subscribe result is missing subscription"), + Self::InvalidSubscription => formatter + .write_str("WebDriver BiDi session.subscribe result subscription is invalid"), + Self::DuplicateSubscription => formatter.write_str( + "WebDriver BiDi session.subscribe result contains duplicate subscription", + ), + Self::SubscriptionTooLarge { .. } => formatter.write_str( + "WebDriver BiDi session.subscribe result subscription exceeds the size bound", + ), + Self::InvalidResultProjection => formatter + .write_str("WebDriver BiDi session.subscribe result projection is invalid"), + Self::Correlation { .. } => formatter + .write_str("WebDriver BiDi session.subscribe response correlation failed"), + Self::RemoteProtocolError { .. } => { + formatter.write_str("WebDriver BiDi session.subscribe returned a protocol error") + } + } + } +} + +impl Error for WebDriverBiDiNavigationCommittedSubscriptionResponseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Envelope { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::MissingSubscription + | Self::InvalidSubscription + | Self::DuplicateSubscription + | Self::SubscriptionTooLarge { .. } + | Self::InvalidResultProjection + | Self::RemoteProtocolError { .. } => None, + } + } +} + +fn retain_validated_error_code( + error_code: Option<&str>, +) -> Result { + error_code.map(str::to_owned).ok_or( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, + }, + ) +} + +struct SubscriptionProjection { + subscription_id: String, +} + +impl SubscriptionProjection { + fn parse( + text: &str, + ) -> Result { + let mut cursor = ProjectionCursor::new(text); + cursor.skip_whitespace(); + if !cursor.consume_byte(b'{') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + cursor.skip_whitespace(); + if cursor.consume_byte(b'}') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + + loop { + cursor.skip_whitespace(); + let key = cursor.parse_string().ok_or( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + )?; + cursor.skip_whitespace(); + if !cursor.consume_byte(b':') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + cursor.skip_whitespace(); + if key == "result" { + return cursor.parse_result_object(); + } + if !cursor.skip_value() { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + cursor.skip_whitespace(); + if cursor.consume_byte(b'}') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + if !cursor.consume_byte(b',') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + } + } +} + +struct ProjectionCursor<'a> { + input: &'a str, + index: usize, +} + +impl<'a> ProjectionCursor<'a> { + const fn new(input: &'a str) -> Self { + Self { input, index: 0 } + } + + fn current_byte(&self) -> Option { + self.input.as_bytes().get(self.index).copied() + } + + fn consume_byte(&mut self, expected: u8) -> bool { + if self.current_byte() == Some(expected) { + self.index += 1; + true + } else { + false + } + } + + fn skip_whitespace(&mut self) { + while matches!(self.current_byte(), Some(b' ' | b'\t' | b'\n' | b'\r')) { + self.index += 1; + } + } + + fn parse_result_object( + &mut self, + ) -> Result + { + if !self.consume_byte(b'{') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + self.skip_whitespace(); + let mut subscription_id = None; + if self.consume_byte(b'}') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::MissingSubscription, + ); + } + + loop { + self.skip_whitespace(); + let key = self.parse_string().ok_or( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + )?; + self.skip_whitespace(); + if !self.consume_byte(b':') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + self.skip_whitespace(); + if key == "subscription" { + if subscription_id.is_some() { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::DuplicateSubscription, + ); + } + let parsed = self.parse_string().ok_or( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidSubscription, + )?; + if parsed.len() > MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::SubscriptionTooLarge { + maximum_bytes: MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES, + }, + ); + } + subscription_id = Some(parsed); + } else if !self.skip_value() { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + self.skip_whitespace(); + if self.consume_byte(b'}') { + break; + } + if !self.consume_byte(b',') { + return Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + ); + } + } + + Ok(SubscriptionProjection { + subscription_id: subscription_id.ok_or( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::MissingSubscription, + )?, + }) + } + + fn consume_literal(&mut self, literal: &[u8]) -> bool { + let end = self.index.saturating_add(literal.len()); + if self.input.as_bytes().get(self.index..end) == Some(literal) { + self.index = end; + true + } else { + false + } + } + + fn skip_value(&mut self) -> bool { + self.skip_whitespace(); + match self.current_byte() { + Some(b'"') => self.parse_string().is_some(), + Some(b'{') => self.skip_object(), + Some(b'[') => self.skip_array(), + Some(b't') => self.consume_literal(b"true"), + Some(b'f') => self.consume_literal(b"false"), + Some(b'n') => self.consume_literal(b"null"), + Some(b'-' | b'0'..=b'9') => self.skip_number(), + _ => false, + } + } + + fn skip_object(&mut self) -> bool { + if !self.consume_byte(b'{') { + return false; + } + self.skip_whitespace(); + if self.consume_byte(b'}') { + return true; + } + loop { + self.skip_whitespace(); + if self.parse_string().is_none() { + return false; + } + self.skip_whitespace(); + if !self.consume_byte(b':') { + return false; + } + if !self.skip_value() { + return false; + } + self.skip_whitespace(); + if self.consume_byte(b'}') { + return true; + } + if !self.consume_byte(b',') { + return false; + } + } + } + + fn skip_array(&mut self) -> bool { + if !self.consume_byte(b'[') { + return false; + } + self.skip_whitespace(); + if self.consume_byte(b']') { + return true; + } + loop { + if !self.skip_value() { + return false; + } + self.skip_whitespace(); + if self.consume_byte(b']') { + return true; + } + if !self.consume_byte(b',') { + return false; + } + } + } + + fn skip_number(&mut self) -> bool { + let start = self.index; + while matches!( + self.current_byte(), + Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9') + ) { + self.index += 1; + } + self.index > start + } + + fn parse_string(&mut self) -> Option { + if !self.consume_byte(b'"') { + return None; + } + let mut output = String::new(); + loop { + let byte = self.current_byte()?; + match byte { + b'"' => { + self.index += 1; + return Some(output); + } + b'\\' => { + self.index += 1; + if !self.parse_escape(&mut output) { + return None; + } + } + 0x00..=0x1f => return None, + _ if byte.is_ascii() => { + output.push(char::from(byte)); + self.index += 1; + } + _ => { + let width = byte.leading_ones() as usize; + let end = self.index + width; + output.push_str(&self.input[self.index..end]); + self.index = end; + } + } + } + } + + fn parse_escape(&mut self, output: &mut String) -> bool { + let Some(escape) = self.current_byte() else { + return false; + }; + self.index += 1; + match escape { + b'"' => output.push('"'), + b'\\' => output.push('\\'), + b'/' => output.push('/'), + b'b' => output.push('\u{0008}'), + b'f' => output.push('\u{000c}'), + b'n' => output.push('\n'), + b'r' => output.push('\r'), + b't' => output.push('\t'), + b'u' => return self.parse_unicode_escape(output), + _ => return false, + } + true + } + + fn parse_unicode_escape(&mut self, output: &mut String) -> bool { + let Some(first) = self.parse_hex_u16() else { + return false; + }; + if (0xd800..=0xdbff).contains(&first) { + if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { + return false; + } + let Some(second) = self.parse_hex_u16() else { + return false; + }; + if !(0xdc00..=0xdfff).contains(&second) { + return false; + } + output.push_str(&String::from_utf16_lossy(&[first, second])); + true + } else if (0xdc00..=0xdfff).contains(&first) { + false + } else { + output.push_str(&String::from_utf16_lossy(&[first])); + true + } + } + + fn parse_hex_u16(&mut self) -> Option { + let mut value = 0_u16; + for _ in 0..4 { + let byte = self.current_byte()?; + let digit = match byte { + b'0'..=b'9' => u16::from(byte - b'0'), + b'a'..=b'f' => u16::from(byte - b'a' + 10), + b'A'..=b'F' => u16::from(byte - b'A' + 10), + _ => return None, + }; + value = (value << 4) | digit; + self.index += 1; + } + Some(value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn projection_accepts_subscription_unknown_metadata_and_escaped_keys() { + let projected = SubscriptionProjection::parse( + r#"{"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"extra":{},"sub\u0073cription":"sub-\ud83d\ude80"}}"#, + ); + assert!(projected.is_ok()); + assert_eq!( + projected.ok().map(|value| value.subscription_id), + Some("sub-🚀".to_owned()) + ); + + let direct_utf8 = SubscriptionProjection::parse( + "\n\t { \r\n \"result\" : { \"subscription\" : \"구독-a\" } }", + ); + assert_eq!( + direct_utf8.ok().map(|value| value.subscription_id), + Some("구독-a".to_owned()) + ); + } + + #[test] + fn projection_rejects_missing_invalid_duplicate_and_oversized_subscription() { + let cases = [ + (r#"{"result":{}}"#.to_owned(), "missing"), + (r#"{"result":{"subscription":false}}"#.to_owned(), "invalid"), + ( + r#"{"result":{"subscription":"a","subscription":"b"}}"#.to_owned(), + "duplicate", + ), + ( + format!( + "{{\"result\":{{\"subscription\":\"{}\"}}}}", + "x".repeat(MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES + 1) + ), + "oversized", + ), + (r#"{"x":1}"#.to_owned(), "missing result"), + ]; + + for (document, label) in cases { + assert!(SubscriptionProjection::parse(&document).is_err(), "{label}"); + } + } + + #[test] + fn projection_cursor_rejects_malformed_private_inputs_without_panicking() { + let malformed = [ + "", + "[]", + "{}", + r#"{"x":}"#, + r#"{"x" 1}"#, + r#"{"x":1 ?}"#, + r#"{?}"#, + r#"{"result":[]}"#, + r#"{"result":{?}}"#, + r#"{"result":{"subscription" "x"}}"#, + r#"{"result":{"subscription":"x" "extra":1}}"#, + r#"{"result":{"subscription":"x","extra":?}}"#, + r#"{"result":{"subscription":"\uD800"}}"#, + r#"{"result":{"subscription":"\q"}}"#, + ]; + for document in malformed { + assert!(SubscriptionProjection::parse(document).is_err(), "{document}"); + } + } + + #[test] + fn projection_cursor_defensive_helpers_cover_hostile_dispatch_edges() { + let mut object = ProjectionCursor::new("[]"); + assert!(!object.skip_object()); + let mut object = ProjectionCursor::new("{}"); + assert!(object.skip_object()); + let mut object = ProjectionCursor::new("{?}"); + assert!(!object.skip_object()); + let mut object = ProjectionCursor::new(r#"{"x" 1}"#); + assert!(!object.skip_object()); + let mut object = ProjectionCursor::new(r#"{"x":?}"#); + assert!(!object.skip_object()); + let mut object = ProjectionCursor::new(r#"{"x":1 ?}"#); + assert!(!object.skip_object()); + let mut object = ProjectionCursor::new(r#"{"x":1,"y":2}"#); + assert!(object.skip_object()); + + let mut array = ProjectionCursor::new("{}"); + assert!(!array.skip_array()); + let mut array = ProjectionCursor::new("[]"); + assert!(array.skip_array()); + let mut array = ProjectionCursor::new("[?]"); + assert!(!array.skip_array()); + let mut array = ProjectionCursor::new("[1 ?]"); + assert!(!array.skip_array()); + + for document in [r#""x""#, "{}", "[]", "true", "false", "null", "-2.5e+3"] { + let mut value = ProjectionCursor::new(document); + assert!(value.skip_value(), "{document}"); + } + let mut value = ProjectionCursor::new("?"); + assert!(!value.skip_value()); + + let mut number = ProjectionCursor::new("x"); + assert!(!number.skip_number()); + let mut number = ProjectionCursor::new("+1"); + assert!(number.skip_number()); + + let mut string = ProjectionCursor::new("x"); + assert!(string.parse_string().is_none()); + let mut string = ProjectionCursor::new("\"unterminated"); + assert!(string.parse_string().is_none()); + let mut string = ProjectionCursor::new("\"\u{0001}\""); + assert!(string.parse_string().is_none()); + let mut string = ProjectionCursor::new("\"é\""); + assert_eq!(string.parse_string().as_deref(), Some("é")); + + let mut output = String::new(); + let mut escape = ProjectionCursor::new(""); + assert!(!escape.parse_escape(&mut output)); + for sequence in ["\"", "\\", "/", "b", "f", "n", "r", "t"] { + let mut output = String::new(); + let mut escape = ProjectionCursor::new(sequence); + assert!(escape.parse_escape(&mut output), "{sequence:?}"); + } + let mut output = String::new(); + let mut escape = ProjectionCursor::new("q"); + assert!(!escape.parse_escape(&mut output)); + + for sequence in ["0000", "aBcD", "Ff09"] { + let mut hex = ProjectionCursor::new(sequence); + assert!(hex.parse_hex_u16().is_some()); + } + let mut hex = ProjectionCursor::new("xyz1"); + assert!(hex.parse_hex_u16().is_none()); + let mut hex = ProjectionCursor::new("0"); + assert!(hex.parse_hex_u16().is_none()); + + let unicode_cases = [ + ("0041", true), + ("d83d\\ude80", true), + ("d83d", false), + ("d83d\\u0041", false), + ("dc00", false), + ("zzzz", false), + ]; + for (sequence, expected) in unicode_cases { + let mut output = String::new(); + let mut unicode = ProjectionCursor::new(sequence); + assert_eq!(unicode.parse_unicode_escape(&mut output), expected); + } + } + + #[test] + fn response_errors_have_stable_messages_and_typed_sources() { + let envelope = WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert_eq!( + envelope.to_string(), + "WebDriver BiDi session.subscribe envelope is invalid" + ); + assert!(envelope.source().is_some()); + + let correlation = + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.subscribe response correlation failed" + ); + assert!(correlation.source().is_some()); + + let source_free = [ + WebDriverBiDiNavigationCommittedSubscriptionResponseError::MissingSubscription, + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidSubscription, + WebDriverBiDiNavigationCommittedSubscriptionResponseError::DuplicateSubscription, + WebDriverBiDiNavigationCommittedSubscriptionResponseError::SubscriptionTooLarge { + maximum_bytes: MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES, + }, + WebDriverBiDiNavigationCommittedSubscriptionResponseError::InvalidResultProjection, + WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { + command_id: 7, + error_code: "invalid argument".to_owned(), + }, + ]; + for error in source_free { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } + } + + #[test] + fn result_debug_redacts_opaque_subscription_identifier() { + let result = WebDriverBiDiNavigationCommittedSubscriptionResult { + command_id: 7, + subscription_id: "sensitive-subscription".to_owned(), + }; + let debug = format!("{result:?}"); + assert!(debug.contains("command_id")); + assert!(debug.contains("subscription_id_len")); + assert!(!debug.contains("sensitive-subscription")); + assert_eq!(result.command_id(), 7); + assert_eq!(result.subscription_id(), "sensitive-subscription"); + } + + #[test] + fn retain_error_code_fails_closed_when_common_invariant_is_absent() { + assert_eq!( + retain_validated_error_code(Some("invalid argument")).as_deref(), + Ok("invalid argument") + ); + assert!(retain_validated_error_code(None).is_err()); + } +} From 089711372aa2361fb26fd0cadf86b671b6872f3b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:13:58 +0900 Subject: [PATCH 15/36] feat(network): expose typed BiDi subscription result --- crates/originweave-network/src/lib.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 991525cdf..f34660d65 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -10,16 +10,16 @@ //! classifies complete local-end JSON envelopes, tracks bounded command-response //! correlation, transports a narrowly typed pointer click, admits its typed //! correlated protocol acknowledgment, sends a context-bound subscription for -//! committed-navigation events, admits a bounded navigation-committed -//! post-condition observation for one exact registered context and URL, rotates -//! the matched context's document epoch only from an exact caller-captured -//! pre-action epoch, derives and binds the committed HTTP(S) URL's canonical -//! origin to that newly advanced document, sends narrowly typed `session.status` -//! and `session.end` commands, admits typed correlated status and end responses, -//! observes bounded peer Close or clean-EOF transport cessation, and keeps -//! protocol/transport evidence separate from explicit operational teardown -//! observations without exposing generic JSON bodies or granting browser, TLS, -//! policy, secret, process, profile, or Agent authority. +//! committed-navigation events, retains its typed bounded correlated subscription +//! identifier, admits a bounded navigation-committed post-condition observation +//! for one exact registered context and URL, rotates the matched context's document +//! epoch only from an exact caller-captured pre-action epoch, derives and binds the +//! committed HTTP(S) URL's canonical origin to that newly advanced document, sends +//! narrowly typed `session.status` and `session.end` commands, admits typed +//! correlated status and end responses, observes bounded peer Close or clean-EOF +//! transport cessation, and keeps protocol/transport evidence separate from +//! explicit operational teardown observations without exposing generic JSON bodies +//! or granting browser, TLS, policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -30,6 +30,7 @@ mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; mod webdriver_bidi_navigation_committed_postcondition; mod webdriver_bidi_navigation_committed_subscription; +mod webdriver_bidi_navigation_committed_subscription_response; mod webdriver_bidi_navigation_document_advance; mod webdriver_bidi_navigation_document_origin; mod webdriver_bidi_pointer_click_response; @@ -75,6 +76,11 @@ pub use webdriver_bidi_navigation_committed_subscription::{ WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionCommandError, }; +pub use webdriver_bidi_navigation_committed_subscription_response::{ + MAX_WEBDRIVER_BIDI_SUBSCRIPTION_IDENTIFIER_BYTES, + WebDriverBiDiNavigationCommittedSubscriptionResponseError, + WebDriverBiDiNavigationCommittedSubscriptionResult, +}; pub use webdriver_bidi_navigation_document_advance::{ WebDriverBiDiNavigationCommittedDocumentAdvance, WebDriverBiDiNavigationCommittedDocumentAdvanceError, From 082a783c98a01570523f09bd5f2e3fea02dcdf25 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:19:16 +0900 Subject: [PATCH 16/36] style(network): apply canonical rustfmt --- ...igation_committed_subscription_response.rs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index 2b97b6811..b416c5ee8 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -154,10 +154,12 @@ impl fmt::Display for WebDriverBiDiNavigationCommittedSubscriptionResponseError Self::SubscriptionTooLarge { .. } => formatter.write_str( "WebDriver BiDi session.subscribe result subscription exceeds the size bound", ), - Self::InvalidResultProjection => formatter - .write_str("WebDriver BiDi session.subscribe result projection is invalid"), - Self::Correlation { .. } => formatter - .write_str("WebDriver BiDi session.subscribe response correlation failed"), + Self::InvalidResultProjection => { + formatter.write_str("WebDriver BiDi session.subscribe result projection is invalid") + } + Self::Correlation { .. } => { + formatter.write_str("WebDriver BiDi session.subscribe response correlation failed") + } Self::RemoteProtocolError { .. } => { formatter.write_str("WebDriver BiDi session.subscribe returned a protocol error") } @@ -593,7 +595,10 @@ mod tests { r#"{"result":{"subscription":"\q"}}"#, ]; for document in malformed { - assert!(SubscriptionProjection::parse(document).is_err(), "{document}"); + assert!( + SubscriptionProjection::parse(document).is_err(), + "{document}" + ); } } @@ -691,10 +696,9 @@ mod tests { ); assert!(envelope.source().is_some()); - let correlation = - WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { - source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, - }; + let correlation = WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; assert_eq!( correlation.to_string(), "WebDriver BiDi session.subscribe response correlation failed" From 2f2f394f4f999eb0f9c4a3ce0bfdc096da05462f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:23:23 +0900 Subject: [PATCH 17/36] fix(network): make subscription response errors comparable --- ...webdriver_bidi_navigation_committed_subscription_response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index b416c5ee8..0ff50e3ae 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -104,7 +104,7 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { } /// Fail-closed failures while admitting one typed WebDriver BiDi `session.subscribe` response. -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub enum WebDriverBiDiNavigationCommittedSubscriptionResponseError { /// Common local-end JSON envelope validation failed. Envelope { From 57a0b8dd7b13a8453aaa12897026d4951e14c9ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:49:11 +0900 Subject: [PATCH 18/36] test(network): cover subscription response failure boundary --- ...ommitted_subscription_response_failures.rs | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs new file mode 100644 index 000000000..5472abc2d --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs @@ -0,0 +1,202 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiNavigationCommittedSubscriptionResponseError, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +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"; +const MALFORMED_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":"#; +const MISSING_SUBSCRIPTION_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"extra":1}}"#; +const UNKNOWN_SUCCESS_RESPONSE: &[u8] = + br#"{"type":"success","id":8,"result":{"subscription":"subscription-b"}}"#; +const MATCHED_ERROR_RESPONSE: &[u8] = br#"{"type":"error","id":7,"error":"invalid argument","message":"denied"}"#; +const UNKNOWN_ERROR_RESPONSE: &[u8] = br#"{"type":"error","id":8,"error":"invalid argument","message":"denied"}"#; +const NAVIGATION_EVENT: &[u8] = br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{}}"#; + +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 write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Result<()> { + if document.len() <= 125 { + let length = u8::try_from(document.len()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "short frame length exceeds u8") + })?; + stream.write_all(&[0x81, length])?; + } else { + let length = u16::try_from(document.len()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "test JSON document exceeds two-byte frame length", + ) + })?; + stream.write_all(&[0x81, 126])?; + stream.write_all(&length.to_be_bytes())?; + } + stream.write_all(document) +} + +fn read_text_over_loopback( + document: &'static [u8], +) -> 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)?; + write_unmasked_text_frame(&mut stream, document) + }); + + 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 (_established, frame) = established.read_frame(Duration::from_millis(500))?; + + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "subscription response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("subscription response test server panicked"))??; + Ok(text) +} + +#[test] +fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() +-> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + + let malformed = read_text_over_loopback(MALFORMED_SUCCESS_RESPONSE)?; + assert_eq!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &malformed, + &mut correlation, + ), + Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let missing = read_text_over_loopback(MISSING_SUBSCRIPTION_RESPONSE)?; + assert_eq!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &missing, + &mut correlation, + ), + Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::MissingSubscription) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let unknown = read_text_over_loopback(UNKNOWN_SUCCESS_RESPONSE)?; + assert_eq!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &unknown, + &mut correlation, + ), + Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }) + ); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + + let unknown = read_text_over_loopback(UNKNOWN_ERROR_RESPONSE)?; + assert_eq!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &unknown, + &mut correlation, + ), + Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }) + ); + assert_eq!(correlation.outstanding_count(), 1); + + let matched = read_text_over_loopback(MATCHED_ERROR_RESPONSE)?; + assert_eq!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &matched, + &mut correlation, + ), + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { + command_id: 7, + error_code: "invalid argument".to_owned(), + } + ) + ); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn event_response_is_rejected_without_consuming_outstanding_command() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + let event = read_text_over_loopback(NAVIGATION_EVENT)?; + + assert_eq!( + WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &event, + &mut correlation, + ), + Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + }) + ); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From 4bbde31a93c2f952e052c2a7853ecdde65da5f30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:54:05 +0900 Subject: [PATCH 19/36] test(network): close subscription projection coverage gaps --- ...iver_bidi_navigation_committed_subscription_response.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index 0ff50e3ae..2f9f73fe0 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -556,6 +556,10 @@ mod tests { fn projection_rejects_missing_invalid_duplicate_and_oversized_subscription() { let cases = [ (r#"{"result":{}}"#.to_owned(), "missing"), + ( + r#"{"result":{"extra":1}}"#.to_owned(), + "missing after metadata", + ), (r#"{"result":{"subscription":false}}"#.to_owned(), "invalid"), ( r#"{"result":{"subscription":"a","subscription":"b"}}"#.to_owned(), @@ -634,6 +638,8 @@ mod tests { } let mut value = ProjectionCursor::new("?"); assert!(!value.skip_value()); + let mut literal = ProjectionCursor::new("tru?"); + assert!(!literal.consume_literal(b"true")); let mut number = ProjectionCursor::new("x"); assert!(!number.skip_number()); @@ -674,6 +680,7 @@ mod tests { ("0041", true), ("d83d\\ude80", true), ("d83d", false), + ("d83d\\u0", false), ("d83d\\u0041", false), ("dc00", false), ("zzzz", false), From e6646c391ef1625377a9b16364c227e26717a5a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 15:57:19 +0900 Subject: [PATCH 20/36] style(network): apply canonical rustfmt --- ...ommitted_subscription_response_failures.rs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs index 5472abc2d..82dea5dfe 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription_response_failures.rs @@ -9,8 +9,7 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiNavigationCommittedSubscriptionResponseError, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiNavigationCommittedSubscriptionResponseError, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, @@ -21,13 +20,15 @@ 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"; const MALFORMED_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":"#; -const MISSING_SUBSCRIPTION_RESPONSE: &[u8] = - br#"{"type":"success","id":7,"result":{"extra":1}}"#; +const MISSING_SUBSCRIPTION_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"extra":1}}"#; const UNKNOWN_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":{"subscription":"subscription-b"}}"#; -const MATCHED_ERROR_RESPONSE: &[u8] = br#"{"type":"error","id":7,"error":"invalid argument","message":"denied"}"#; -const UNKNOWN_ERROR_RESPONSE: &[u8] = br#"{"type":"error","id":8,"error":"invalid argument","message":"denied"}"#; -const NAVIGATION_EVENT: &[u8] = br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{}}"#; +const MATCHED_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":7,"error":"invalid argument","message":"denied"}"#; +const UNKNOWN_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":8,"error":"invalid argument","message":"denied"}"#; +const NAVIGATION_EVENT: &[u8] = + br#"{"type":"event","method":"browsingContext.navigationCommitted","params":{}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -118,9 +119,11 @@ fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() &malformed, &mut correlation, ), - Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { - source: WebDriverBiDiJsonEnvelopeError::InvalidJson, - }) + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + } + ) ); assert_eq!(correlation.outstanding_count(), 1); @@ -140,9 +143,11 @@ fn malformed_and_invalid_success_responses_preserve_outstanding_correlation() &unknown, &mut correlation, ), - Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { - source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, - }) + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + } + ) ); assert_eq!(correlation.outstanding_count(), 1); Ok(()) @@ -159,9 +164,11 @@ fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Bo &unknown, &mut correlation, ), - Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { - source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, - }) + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + } + ) ); assert_eq!(correlation.outstanding_count(), 1); @@ -183,7 +190,8 @@ fn protocol_error_consumes_only_its_exact_outstanding_command() -> Result<(), Bo } #[test] -fn event_response_is_rejected_without_consuming_outstanding_command() -> Result<(), Box> { +fn event_response_is_rejected_without_consuming_outstanding_command() -> Result<(), Box> +{ let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(7)?; let event = read_text_over_loopback(NAVIGATION_EVENT)?; @@ -193,9 +201,11 @@ fn event_response_is_rejected_without_consuming_outstanding_command() -> Result< &event, &mut correlation, ), - Err(WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { - source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, - }) + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + } + ) ); assert_eq!(correlation.outstanding_count(), 1); Ok(()) From 6902fa391c229d333239b86d13ab39955a3ef16f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 16:10:17 +0900 Subject: [PATCH 21/36] test(network): exercise UTF-8 subscription extension --- .../tests/webdriver_bidi_navigation_committed_subscription.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index 4e5936ed4..41da00094 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -20,8 +20,8 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const CONTEXT_ID: &str = "context-\"a\\b"; 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"; -const SUBSCRIBE_RESPONSE: &[u8] = - br#"{"type":"success","id":7,"result":{"subscription":"subscription-a"}}"#; +const SUBSCRIBE_RESPONSE: &[u8] = r#"{"type":"success","id":7,"result":{"subscription":"subscription-a","vendorNote":"café"}}"# + .as_bytes(); fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; From 65f934a8c84b0dd1605c5eeebed3a6f26a6206e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 16:12:39 +0900 Subject: [PATCH 22/36] style(network): apply canonical subscription test formatting --- .../webdriver_bidi_navigation_committed_subscription.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs index 41da00094..4b5a7ad39 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_subscription.rs @@ -20,8 +20,9 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const CONTEXT_ID: &str = "context-\"a\\b"; 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"; -const SUBSCRIBE_RESPONSE: &[u8] = r#"{"type":"success","id":7,"result":{"subscription":"subscription-a","vendorNote":"café"}}"# - .as_bytes(); +const SUBSCRIBE_RESPONSE: &[u8] = + r#"{"type":"success","id":7,"result":{"subscription":"subscription-a","vendorNote":"café"}}"# + .as_bytes(); fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; From 16b20ba8061a4513aae8c9def6bc5f6517a206d7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 16:21:43 +0900 Subject: [PATCH 23/36] test(network): cover malformed surrogate separator --- .../webdriver_bidi_navigation_committed_subscription_response.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index 2f9f73fe0..d9ba6a1c2 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -680,6 +680,7 @@ mod tests { ("0041", true), ("d83d\\ude80", true), ("d83d", false), + ("d83d\\x", false), ("d83d\\u0", false), ("d83d\\u0041", false), ("dc00", false), From 93e2e32e3e4ad71bd54d9382950227e968e55312 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 16:28:58 +0900 Subject: [PATCH 24/36] fix(network): isolate validated protocol-error projection --- ...igation_committed_subscription_response.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs index d9ba6a1c2..961ea8ec7 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_subscription_response.rs @@ -67,20 +67,19 @@ impl WebDriverBiDiNavigationCommittedSubscriptionResult { }) } WebDriverBiDiJsonEnvelopeKind::Error => { - let error_code = retain_validated_error_code(envelope.error_code())?; - let completed = correlation - .correlate_response(&envelope) - .map_err(|source| { + retain_validated_error_code(envelope.error_code()).and_then(|error_code| { + let completed = correlation.correlate_response(&envelope).map_err(|source| { WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { source, } })?; - Err( - WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { - command_id: completed.command_id(), - error_code, - }, - ) + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::RemoteProtocolError { + command_id: completed.command_id(), + error_code, + }, + ) + }) } WebDriverBiDiJsonEnvelopeKind::Event => Err( WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { From b20d35180eef45d70e2370abc7b74cd5aef06d81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:12:08 +0900 Subject: [PATCH 25/36] test(network): require typed navigation unsubscribe lifecycle --- ...r_bidi_navigation_committed_unsubscribe.rs | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs new file mode 100644 index 000000000..6b5da155c --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -0,0 +1,202 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; +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"; +const SUBSCRIBE_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"subscription":"sub-\"\\\n\u0001-구독"}}"#; +const UNSUBSCRIBE_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":{}}"#; + +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 = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required 64-bit 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) +} + +fn write_server_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + if payload.len() > 125 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "test server payload unexpectedly exceeded short-frame encoding", + )); + } + stream.write_all(&[0x81, payload.len() as u8])?; + stream.write_all(payload) +} + +#[test] +fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() +-> 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 subscribe = read_masked_text_frame(&mut stream)?; + if subscribe + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "unexpected session.subscribe command: {}", + String::from_utf8_lossy(&subscribe) + ), + )); + } + write_server_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; + + let unsubscribe = read_masked_text_frame(&mut stream)?; + if unsubscribe + != br#"{"id":8,"method":"session.unsubscribe","params":{"subscriptions":["sub-\"\\\n\u0001-구독"]}}"# + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "unexpected session.unsubscribe command: {}", + String::from_utf8_lossy(&unsubscribe) + ), + )); + } + write_server_text_frame(&mut stream, UNSUBSCRIBE_RESPONSE) + }); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + + 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 subscribe = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, ®istry, session, context, CONTEXT_ID, + )?; + let established = subscribe.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + + let (established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "session.subscribe response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &text, + &mut correlation, + )?; + assert_eq!(subscription.subscription_id(), "sub-\"\\\n\u{0001}-구독"); + + let unsubscribe = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + assert_eq!(unsubscribe.command_id(), 8); + let established = unsubscribe.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 1); + + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "session.unsubscribe response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let result = WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &text, + &mut correlation, + )?; + assert_eq!(result.command_id(), 8); + assert_eq!(correlation.outstanding_count(), 0); + + server + .join() + .map_err(|_| io::Error::other("session.unsubscribe command test server panicked"))??; + Ok(()) +} From e24f996a125401c7d6995fe14cbd58e4841caf1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:16:22 +0900 Subject: [PATCH 26/36] test(network): preserve opaque utf8 subscription text --- .../webdriver_bidi_navigation_committed_unsubscribe.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs index 6b5da155c..05e228f74 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -21,7 +21,8 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const CONTEXT_ID: &str = "context-a"; 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"; -const SUBSCRIBE_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"subscription":"sub-\"\\\n\u0001-구독"}}"#; +const SUBSCRIBE_RESPONSE: &str = + r#"{"type":"success","id":7,"result":{"subscription":"sub-\"\\\n\u0001-구독"}}"#; const UNSUBSCRIBE_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":{}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { @@ -107,11 +108,12 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() ), )); } - write_server_text_frame(&mut stream, SUBSCRIBE_RESPONSE)?; + write_server_text_frame(&mut stream, SUBSCRIBE_RESPONSE.as_bytes())?; let unsubscribe = read_masked_text_frame(&mut stream)?; if unsubscribe - != br#"{"id":8,"method":"session.unsubscribe","params":{"subscriptions":["sub-\"\\\n\u0001-구독"]}}"# + != r#"{"id":8,"method":"session.unsubscribe","params":{"subscriptions":["sub-\"\\\n\u0001-구독"]}}"# + .as_bytes() { return Err(io::Error::new( io::ErrorKind::InvalidData, From 23e772ac015d5ed3872138f714a02d35005f154f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:19:02 +0900 Subject: [PATCH 27/36] feat(network): serialize typed navigation unsubscribe --- ...r_bidi_navigation_committed_unsubscribe.rs | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs new file mode 100644 index 000000000..329706397 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -0,0 +1,219 @@ +use std::{error::Error, fmt, time::Duration}; + +use crate::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, + WebDriverBiDiWebSocketMaskKey, +}; + +const SESSION_UNSUBSCRIBE_METHOD: &str = "session.unsubscribe"; + +/// One bounded WebDriver BiDi `session.unsubscribe` command for a validated subscription receipt. +/// +/// The command deliberately accepts only the typed opaque identifier returned by OriginWeave's +/// `session.subscribe` response boundary. It cannot introduce arbitrary event names, contexts, +/// user contexts, or ambient subscription identifiers. Writing the frame does not prove remote +/// teardown; callers must admit and correlate the later protocol response separately. +#[derive(Clone, Eq, PartialEq)] +pub struct WebDriverBiDiNavigationCommittedUnsubscribeCommand { + command_id: u64, + subscription_id: String, +} + +impl fmt::Debug for WebDriverBiDiNavigationCommittedUnsubscribeCommand { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiNavigationCommittedUnsubscribeCommand") + .field("command_id", &self.command_id) + .field("subscription_id_len", &self.subscription_id.len()) + .finish() + } +} + +impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { + /// Construct one unsubscribe command from an already validated typed subscription receipt. + pub fn new( + command_id: u64, + subscription: &WebDriverBiDiNavigationCommittedSubscriptionResult, + ) -> Result { + if command_id > MAX_WEBDRIVER_BIDI_JS_UINT { + return Err( + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id: MAX_WEBDRIVER_BIDI_JS_UINT, + }, + ); + } + Ok(Self { + command_id, + subscription_id: subscription.subscription_id().to_owned(), + }) + } + + /// 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 unsubscribe command on an established verified BiDi stream. + /// + /// Correlation registration occurs before the first possible remote side effect. A local + /// registration failure therefore writes nothing. Once registered, a frame-write failure keeps + /// the identifier outstanding because the peer may have received a partial or complete command; + /// silently retiring the id would make later response correlation or identifier reuse unsafe. + pub fn send( + self, + established: WebDriverBiDiWebSocketEstablished, + correlation: &mut WebDriverBiDiCommandCorrelation, + masking_key: WebDriverBiDiWebSocketMaskKey, + frame_timeout: Duration, + ) -> Result< + WebDriverBiDiWebSocketEstablished, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, + > { + correlation + .register_command(self.command_id) + .map_err(|source| { + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::Correlation { source } + })?; + let message = self.serialized(); + established + .write_text_frame(&message, masking_key, frame_timeout) + .map_err(|source| { + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { source } + }) + } + + fn serialized(&self) -> String { + serialize_unsubscribe_command(self.command_id, &self.subscription_id) + } +} + +/// Fail-closed errors while constructing or sending one typed `session.unsubscribe` command. +#[derive(Debug)] +pub enum WebDriverBiDiNavigationCommittedUnsubscribeCommandError { + /// 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 WebDriverBiDiNavigationCommittedUnsubscribeCommandError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::CommandIdOutOfRange { .. } => formatter.write_str( + "WebDriver BiDi session.unsubscribe command id is outside the js-uint range", + ), + Self::Correlation { .. } => formatter + .write_str("WebDriver BiDi session.unsubscribe command correlation was rejected"), + Self::FrameWrite { .. } => formatter + .write_str("WebDriver BiDi session.unsubscribe command frame write failed"), + } + } +} + +impl Error for WebDriverBiDiNavigationCommittedUnsubscribeCommandError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::CommandIdOutOfRange { .. } => None, + Self::Correlation { source } => Some(source), + Self::FrameWrite { source } => Some(source), + } + } +} + +fn serialize_unsubscribe_command(command_id: u64, subscription_id: &str) -> String { + let mut message = format!( + "{{\"id\":{command_id},\"method\":\"{SESSION_UNSUBSCRIBE_METHOD}\",\"params\":{{\"subscriptions\":[\"" + ); + push_json_string_content(&mut message, subscription_id); + message.push_str("\"]}}"); + message +} + +fn push_json_string_content(output: &mut String, input: &str) { + for character in input.chars() { + match character { + '"' => output.push_str("\\\""), + '\\' => output.push_str("\\\\"), + '\u{0008}' => output.push_str("\\b"), + '\u{000c}' => output.push_str("\\f"), + '\n' => output.push_str("\\n"), + '\r' => output.push_str("\\r"), + '\t' => output.push_str("\\t"), + character if character <= '\u{001f}' => { + let code = character as usize; + let digits = b"0123456789abcdef"; + output.push_str("\\u00"); + output.push(char::from(digits[(code >> 4) & 0x0f])); + output.push(char::from(digits[code & 0x0f])); + } + character => output.push(character), + } + } +} + +#[cfg(test)] +mod tests { + use std::io; + + use super::*; + + #[test] + fn serializer_preserves_utf8_and_escapes_every_json_control_class() { + let input = "quote\" slash\\ back\u{0008} form\u{000c} line\n return\r tab\t nul\u{0000} unit\u{0001} 구독"; + assert_eq!( + serialize_unsubscribe_command(42, input), + r#"{"id":42,"method":"session.unsubscribe","params":{"subscriptions":["quote\" slash\\ back\b form\f line\n return\r tab\t nul\u0000 unit\u0001 구독"]}}"# + ); + } + + #[test] + fn command_errors_have_stable_messages_and_typed_sources() { + let range = WebDriverBiDiNavigationCommittedUnsubscribeCommandError::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.unsubscribe command id is outside the js-uint range" + ); + assert!(range.source().is_none()); + + let correlation = WebDriverBiDiNavigationCommittedUnsubscribeCommandError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandAlreadyOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.unsubscribe command correlation was rejected" + ); + assert!(correlation.source().is_some()); + + let frame = WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { + source: WebDriverBiDiWebSocketFrameError::FrameWriteFailed { + bytes_written: 0, + source: io::Error::other("test frame failure"), + }, + }; + assert_eq!( + frame.to_string(), + "WebDriver BiDi session.unsubscribe command frame write failed" + ); + assert!(frame.source().is_some()); + } +} From e11315dec7189f918000cfb81a1530e89abb2d77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:20:02 +0900 Subject: [PATCH 28/36] feat(network): correlate typed navigation unsubscribe response --- ...vigation_committed_unsubscribe_response.rs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs new file mode 100644 index 000000000..00bdf6731 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe_response.rs @@ -0,0 +1,139 @@ +use std::{error::Error, fmt}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiWebSocketTextMessage, +}; + +/// Typed protocol acknowledgment for one correlated WebDriver BiDi `session.unsubscribe` command. +/// +/// WebDriver BiDi defines `session.UnsubscribeResult` as the extensible `EmptyResult` object. The +/// common local-end envelope parser validates the complete JSON document and requires a success +/// result object, so this boundary retains only the matched command identifier. A successful value +/// proves protocol acknowledgment only; it does not itself prove that no already-in-flight event can +/// arrive or grant any replacement browser, policy, origin, secret, or Agent authority. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiNavigationCommittedUnsubscribeResult { + command_id: u64, +} + +impl WebDriverBiDiNavigationCommittedUnsubscribeResult { + /// Parse one bounded local-end message and consume its exact outstanding command on response. + /// + /// Complete JSON and common WebDriver BiDi envelope validation occur before correlation state + /// can be consumed. A correlatable protocol-error response consumes its matching identifier and + /// returns a typed remote failure. Events, null-id errors, malformed envelopes, and unknown ids + /// fail closed without consuming unrelated outstanding command state. + pub fn parse_and_correlate( + message: &WebDriverBiDiWebSocketTextMessage, + correlation: &mut WebDriverBiDiCommandCorrelation, + ) -> Result { + let envelope = WebDriverBiDiJsonEnvelope::parse(message).map_err(|source| { + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Envelope { source } + })?; + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { source } + })?; + + match completed.outcome() { + WebDriverBiDiCorrelatedResponseOutcome::Success => Ok(Self { + command_id: completed.command_id(), + }), + WebDriverBiDiCorrelatedResponseOutcome::Error => Err( + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }, + ), + } + } + + /// Return the exact local command identifier consumed by this protocol acknowledgment. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } +} + +/// Fail-closed failures while admitting one typed WebDriver BiDi `session.unsubscribe` response. +#[derive(Debug)] +pub enum WebDriverBiDiNavigationCommittedUnsubscribeResponseError { + /// Common local-end JSON envelope validation failed before correlation state was touched. + Envelope { + /// Exact common-envelope validation failure. + source: WebDriverBiDiJsonEnvelopeError, + }, + /// Exact command-response correlation failed without consuming unrelated state. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// The remote end returned a correlatable WebDriver BiDi protocol error for this command. + RemoteProtocolError { + /// Exact local command identifier consumed by the protocol-error response. + command_id: u64, + }, +} + +impl fmt::Display for WebDriverBiDiNavigationCommittedUnsubscribeResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Envelope { .. } => { + formatter.write_str("WebDriver BiDi session.unsubscribe envelope is invalid") + } + Self::Correlation { .. } => formatter + .write_str("WebDriver BiDi session.unsubscribe response correlation failed"), + Self::RemoteProtocolError { .. } => { + formatter.write_str("WebDriver BiDi session.unsubscribe returned a protocol error") + } + } + } +} + +impl Error for WebDriverBiDiNavigationCommittedUnsubscribeResponseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Envelope { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::RemoteProtocolError { .. } => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_errors_have_stable_messages_and_typed_sources() { + let envelope = WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert_eq!( + envelope.to_string(), + "WebDriver BiDi session.unsubscribe envelope is invalid" + ); + assert!(envelope.source().is_some()); + + let correlation = WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.unsubscribe response correlation failed" + ); + assert!(correlation.source().is_some()); + + let remote = + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::RemoteProtocolError { + command_id: 8, + }; + assert_eq!( + remote.to_string(), + "WebDriver BiDi session.unsubscribe returned a protocol error" + ); + assert!(remote.source().is_none()); + } +} From 3a43c057bd970afc3c86fda2eecfb7298721a5e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:22:12 +0900 Subject: [PATCH 29/36] feat(network): expose typed navigation unsubscribe boundary --- crates/originweave-network/src/lib.rs | 30 +++++++++++++++++++-------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index f34660d65..2dd949f68 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -11,15 +11,17 @@ //! correlation, transports a narrowly typed pointer click, admits its typed //! correlated protocol acknowledgment, sends a context-bound subscription for //! committed-navigation events, retains its typed bounded correlated subscription -//! identifier, admits a bounded navigation-committed post-condition observation -//! for one exact registered context and URL, rotates the matched context's document -//! epoch only from an exact caller-captured pre-action epoch, derives and binds the -//! committed HTTP(S) URL's canonical origin to that newly advanced document, sends -//! narrowly typed `session.status` and `session.end` commands, admits typed -//! correlated status and end responses, observes bounded peer Close or clean-EOF -//! transport cessation, and keeps protocol/transport evidence separate from -//! explicit operational teardown observations without exposing generic JSON bodies -//! or granting browser, TLS, policy, secret, process, profile, or Agent authority. +//! identifier, explicitly tears down that exact subscription by identifier, admits +//! its typed correlated unsubscribe acknowledgment, admits a bounded +//! navigation-committed post-condition observation for one exact registered context +//! and URL, rotates the matched context's document epoch only from an exact +//! caller-captured pre-action epoch, derives and binds the committed HTTP(S) URL's +//! canonical origin to that newly advanced document, sends narrowly typed +//! `session.status` and `session.end` commands, admits typed correlated status and +//! end responses, observes bounded peer Close or clean-EOF transport cessation, and +//! keeps protocol/transport evidence separate from explicit operational teardown +//! observations without exposing generic JSON bodies or granting browser, TLS, +//! policy, secret, process, profile, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -31,6 +33,8 @@ mod webdriver_bidi_json_envelope; mod webdriver_bidi_navigation_committed_postcondition; mod webdriver_bidi_navigation_committed_subscription; mod webdriver_bidi_navigation_committed_subscription_response; +mod webdriver_bidi_navigation_committed_unsubscribe; +mod webdriver_bidi_navigation_committed_unsubscribe_response; mod webdriver_bidi_navigation_document_advance; mod webdriver_bidi_navigation_document_origin; mod webdriver_bidi_pointer_click_response; @@ -81,6 +85,14 @@ pub use webdriver_bidi_navigation_committed_subscription_response::{ WebDriverBiDiNavigationCommittedSubscriptionResponseError, WebDriverBiDiNavigationCommittedSubscriptionResult, }; +pub use webdriver_bidi_navigation_committed_unsubscribe::{ + WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, +}; +pub use webdriver_bidi_navigation_committed_unsubscribe_response::{ + WebDriverBiDiNavigationCommittedUnsubscribeResponseError, + WebDriverBiDiNavigationCommittedUnsubscribeResult, +}; pub use webdriver_bidi_navigation_document_advance::{ WebDriverBiDiNavigationCommittedDocumentAdvance, WebDriverBiDiNavigationCommittedDocumentAdvanceError, From 88abc1b7af330d617196cbce120d83771d78713b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:26:47 +0900 Subject: [PATCH 30/36] style(network): apply canonical unsubscribe formatting --- ...bdriver_bidi_navigation_committed_unsubscribe.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs index 329706397..658d338dd 100644 --- a/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/src/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -81,9 +81,11 @@ impl WebDriverBiDiNavigationCommittedUnsubscribeCommand { let message = self.serialized(); established .write_text_frame(&message, masking_key, frame_timeout) - .map_err(|source| { - WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { source } - }) + .map_err( + |source| WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { + source, + }, + ) } fn serialized(&self) -> String { @@ -121,8 +123,9 @@ impl fmt::Display for WebDriverBiDiNavigationCommittedUnsubscribeCommandError { ), Self::Correlation { .. } => formatter .write_str("WebDriver BiDi session.unsubscribe command correlation was rejected"), - Self::FrameWrite { .. } => formatter - .write_str("WebDriver BiDi session.unsubscribe command frame write failed"), + Self::FrameWrite { .. } => { + formatter.write_str("WebDriver BiDi session.unsubscribe command frame write failed") + } } } } From af5acf29fec987d098e9185b4a0e98277f83a678 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:41:08 +0900 Subject: [PATCH 31/36] test(network): cover unsubscribe failure contracts --- ...vigation_committed_unsubscribe_failures.rs | 440 ++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs new file mode 100644 index 000000000..5b52062cb --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -0,0 +1,440 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, + WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResult, + WebDriverBiDiNavigationCommittedUnsubscribeCommand, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError, + WebDriverBiDiNavigationCommittedUnsubscribeResponseError, + WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CONTEXT_ID: &str = "context-a"; +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"; +const SUBSCRIBE_RESPONSE: &str = + r#"{"type":"success","id":7,"result":{"subscription":"sub-\"\\\n\u0001-구독"}}"#; +const MALFORMED_UNSUBSCRIBE_RESPONSE: &[u8] = br#"{"type":"success","id":8,"result":"#; +const UNKNOWN_UNSUBSCRIBE_RESPONSE: &[u8] = br#"{"type":"success","id":9,"result":{}}"#; +const MATCHED_UNSUBSCRIBE_ERROR: &[u8] = + br#"{"type":"error","id":8,"error":"invalid argument","message":"denied"}"#; + +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 = match header[1] & 0x7f { + length @ 0..=125 => usize::from(length), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "test command unexpectedly required 64-bit 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) +} + +fn write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Result<()> { + if document.len() <= 125 { + let length = u8::try_from(document.len()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidData, "short frame length exceeds u8") + })?; + stream.write_all(&[0x81, length])?; + } else { + let length = u16::try_from(document.len()).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "test document exceeds two-byte frame length", + ) + })?; + stream.write_all(&[0x81, 126])?; + stream.write_all(&length.to_be_bytes())?; + } + stream.write_all(document) +} + +fn require_no_client_command(stream: &mut TcpStream) -> io::Result<()> { + stream.set_read_timeout(Some(Duration::from_secs(2)))?; + let mut byte = [0_u8; 1]; + match stream.read(&mut byte) { + Ok(0) => Ok(()), + Ok(_) => Err(io::Error::new( + io::ErrorKind::InvalidData, + "unsubscribe command was written despite local rejection", + )), + Err(source) + if matches!( + source.kind(), + io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted + ) => + { + Ok(()) + } + Err(source) => Err(source), + } +} + +fn spawn_no_command_server(listener: TcpListener) -> thread::JoinHandle> { + thread::spawn(move || { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + require_no_client_command(&mut stream) + }) +} + +fn establish_websocket( + local_addr: SocketAddr, +) -> Result> { + 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)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn obtain_subscription_receipt( +) -> 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 subscribe = read_masked_text_frame(&mut stream)?; + if subscribe + != br#"{"id":7,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"# + { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "unexpected session.subscribe command: {}", + String::from_utf8_lossy(&subscribe) + ), + )); + } + write_unmasked_text_frame(&mut stream, SUBSCRIBE_RESPONSE.as_bytes()) + }); + + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, CONTEXT_ID)?; + let established = establish_websocket(local_addr)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let subscribe = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 7, + ®istry, + session, + context, + CONTEXT_ID, + )?; + let established = subscribe.send( + ®istry, + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "session.subscribe response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + let subscription = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &text, + &mut correlation, + )?; + + server + .join() + .map_err(|_| io::Error::other("subscription receipt test server panicked"))??; + Ok(subscription) +} + +fn read_text_over_loopback( + document: &'static [u8], +) -> 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)?; + write_unmasked_text_frame(&mut stream, document) + }); + + let established = establish_websocket(local_addr)?; + let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); + let text = match assembler.push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + other => { + return Err(io::Error::other(format!( + "session.unsubscribe response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("unsubscribe response test server panicked"))??; + Ok(text) +} + +#[test] +fn command_validation_and_debug_are_public_and_subscription_safe() -> Result<(), Box> { + let subscription = obtain_subscription_receipt()?; + let range = match WebDriverBiDiNavigationCommittedUnsubscribeCommand::new( + MAX_WEBDRIVER_BIDI_JS_UINT + 1, + &subscription, + ) { + Ok(_) => { + return Err(io::Error::other("out-of-range unsubscribe command id was accepted").into()); + } + Err(error) => error, + }; + assert_eq!( + range.to_string(), + "WebDriver BiDi session.unsubscribe command id is outside the js-uint range" + ); + assert!(range.source().is_none()); + assert!(matches!( + &range, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::CommandIdOutOfRange { + command_id, + maximum_command_id, + } if *command_id == MAX_WEBDRIVER_BIDI_JS_UINT + 1 + && *maximum_command_id == MAX_WEBDRIVER_BIDI_JS_UINT + )); + + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let debug = format!("{command:?}"); + assert!(debug.contains("command_id: 8")); + assert!(debug.contains(&format!( + "subscription_id_len: {}", + subscription.subscription_id().len() + ))); + assert!(!debug.contains(subscription.subscription_id())); + Ok(()) +} + +#[test] +fn duplicate_command_id_is_rejected_before_unsubscribe_write() -> Result<(), Box> { + let subscription = obtain_subscription_receipt()?; + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_no_command_server(listener); + let established = establish_websocket(local_addr)?; + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(8)?; + let result = command.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::from_millis(500), + ); + let error = match result { + Ok(_) => { + return Err(io::Error::other("duplicate command id sent unsubscribe command").into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi session.unsubscribe command correlation was rejected" + ); + assert!(error.source().is_some()); + assert!(matches!( + &error, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::Correlation { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("duplicate-command test server panicked"))??; + Ok(()) +} + +#[test] +fn invalid_frame_timeout_consumes_transport_and_retains_unsubscribe_correlation( +) -> Result<(), Box> { + let subscription = obtain_subscription_receipt()?; + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + let server = spawn_no_command_server(listener); + let established = establish_websocket(local_addr)?; + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let result = command.send( + established, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]), + Duration::ZERO, + ); + let error = match result { + Ok(_) => { + return Err(io::Error::other("zero frame timeout sent unsubscribe command").into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi session.unsubscribe command frame write failed" + ); + assert!(error.source().is_some()); + assert!(matches!( + &error, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + server + .join() + .map_err(|_| io::Error::other("frame-write test server panicked"))??; + Ok(()) +} + +#[test] +fn malformed_and_unknown_unsubscribe_responses_preserve_outstanding_correlation( +) -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(8)?; + + let malformed = read_text_over_loopback(MALFORMED_UNSUBSCRIBE_RESPONSE)?; + let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &malformed, + &mut correlation, + ) { + Ok(_) => { + return Err(io::Error::other("malformed unsubscribe response was accepted").into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi session.unsubscribe envelope is invalid" + ); + assert!(error.source().is_some()); + assert!(matches!( + &error, + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Envelope { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + + let unknown = read_text_over_loopback(UNKNOWN_UNSUBSCRIBE_RESPONSE)?; + let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &unknown, + &mut correlation, + ) { + Ok(_) => { + return Err(io::Error::other("unknown unsubscribe response id was accepted").into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi session.unsubscribe response correlation failed" + ); + assert!(error.source().is_some()); + assert!(matches!( + &error, + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::Correlation { .. } + )); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn matched_unsubscribe_protocol_error_consumes_only_its_command() -> Result<(), Box> { + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(8)?; + let matched = read_text_over_loopback(MATCHED_UNSUBSCRIBE_ERROR)?; + + let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( + &matched, + &mut correlation, + ) { + Ok(_) => { + return Err(io::Error::other("protocol-error unsubscribe response was accepted").into()); + } + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "WebDriver BiDi session.unsubscribe returned a protocol error" + ); + assert!(error.source().is_none()); + assert!(matches!( + &error, + WebDriverBiDiNavigationCommittedUnsubscribeResponseError::RemoteProtocolError { + command_id: 8, + } + )); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} From ad66e8938e1d9594687cacd367c63d8d8e77cc18 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 31 Aug 2026 17:45:16 +0900 Subject: [PATCH 32/36] test(network): apply canonical rustfmt --- ...vigation_committed_unsubscribe_failures.rs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs index 5b52062cb..df54f2ee0 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -146,8 +146,8 @@ fn establish_websocket( .read_opening_response(Duration::from_millis(500))?) } -fn obtain_subscription_receipt( -) -> Result> { +fn obtain_subscription_receipt() +-> Result> { let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { @@ -176,11 +176,7 @@ fn obtain_subscription_receipt( let established = establish_websocket(local_addr)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); let subscribe = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( - 7, - ®istry, - session, - context, - CONTEXT_ID, + 7, ®istry, session, context, CONTEXT_ID, )?; let established = subscribe.send( ®istry, @@ -250,7 +246,9 @@ fn command_validation_and_debug_are_public_and_subscription_safe() -> Result<(), &subscription, ) { Ok(_) => { - return Err(io::Error::other("out-of-range unsubscribe command id was accepted").into()); + return Err( + io::Error::other("out-of-range unsubscribe command id was accepted").into(), + ); } Err(error) => error, }; @@ -320,8 +318,8 @@ fn duplicate_command_id_is_rejected_before_unsubscribe_write() -> Result<(), Box } #[test] -fn invalid_frame_timeout_consumes_transport_and_retains_unsubscribe_correlation( -) -> Result<(), Box> { +fn invalid_frame_timeout_consumes_transport_and_retains_unsubscribe_correlation() +-> Result<(), Box> { let subscription = obtain_subscription_receipt()?; let listener = TcpListener::bind(("127.0.0.1", 0))?; let local_addr = listener.local_addr()?; @@ -360,8 +358,8 @@ fn invalid_frame_timeout_consumes_transport_and_retains_unsubscribe_correlation( } #[test] -fn malformed_and_unknown_unsubscribe_responses_preserve_outstanding_correlation( -) -> Result<(), Box> { +fn malformed_and_unknown_unsubscribe_responses_preserve_outstanding_correlation() +-> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command(8)?; @@ -420,7 +418,9 @@ fn matched_unsubscribe_protocol_error_consumes_only_its_command() -> Result<(), &mut correlation, ) { Ok(_) => { - return Err(io::Error::other("protocol-error unsubscribe response was accepted").into()); + return Err( + io::Error::other("protocol-error unsubscribe response was accepted").into(), + ); } Err(error) => error, }; From 3f22de94b63da83eaa8b5b1270912b21a3ecd006 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 12:32:36 +0900 Subject: [PATCH 33/36] test(network): cover unsubscribe frame rejection --- ...vigation_committed_unsubscribe_failures.rs | 93 +++++++++++++++++-- 1 file changed, 85 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs index 517a9d524..9b50f5ffd 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -17,9 +17,9 @@ use originweave_network::{ WebDriverBiDiNavigationCommittedUnsubscribeResponseError, WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -84,6 +84,29 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } +fn read_empty_masked_pong_frame( + stream: &mut TcpStream, + expected_masking_key: [u8; 4], +) -> io::Result<()> { + let mut frame = [0_u8; 6]; + stream.read_exact(&mut frame)?; + let expected = [ + 0x8a, + 0x80, + expected_masking_key[0], + expected_masking_key[1], + expected_masking_key[2], + expected_masking_key[3], + ]; + if frame != expected { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "expected one empty masked client pong frame", + )); + } + Ok(()) +} + fn write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Result<()> { if document.len() <= 125 { let length = u8::try_from(document.len()).map_err(|_| { @@ -289,7 +312,8 @@ fn duplicate_command_id_is_rejected_before_unsubscribe_write() -> Result<(), Box let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; + correlation + .register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; let result = command.send( established, &mut correlation, @@ -359,11 +383,60 @@ fn invalid_frame_timeout_fails_before_unsubscribe_correlation_or_write() Ok(()) } +#[test] +fn adjacent_mask_key_reuse_is_rejected_inside_unsubscribe_send_and_retires_correlation() +-> Result<(), Box> { + let subscription = obtain_subscription_receipt()?; + 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)?; + read_empty_masked_pong_frame(&mut stream, [5, 6, 7, 8])?; + require_no_client_command(&mut stream) + }); + + let masking_key = WebDriverBiDiWebSocketMaskKey::new([5, 6, 7, 8]); + let established = establish_websocket(local_addr)?.write_pong_frame( + &[], + masking_key, + Duration::from_millis(500), + )?; + let command = WebDriverBiDiNavigationCommittedUnsubscribeCommand::new(8, &subscription)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let result = command.send( + established, + &mut correlation, + masking_key, + Duration::from_millis(500), + ); + let error = match result { + Ok(_) => { + return Err(io::Error::other("reused masking key sent unsubscribe command").into()); + } + Err(error) => error, + }; + assert!(matches!( + &error, + WebDriverBiDiNavigationCommittedUnsubscribeCommandError::FrameWrite { + source: WebDriverBiDiWebSocketFrameError::MalformedFrame { .. }, + } + )); + assert_eq!(correlation.outstanding_count(), 0); + + server + .join() + .map_err(|_| io::Error::other("mask-reuse test server panicked"))??; + Ok(()) +} + #[test] fn malformed_and_unknown_unsubscribe_responses_preserve_outstanding_correlation() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; + correlation + .register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; let malformed = read_text_over_loopback(MALFORMED_UNSUBSCRIBE_RESPONSE)?; let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( @@ -412,14 +485,17 @@ fn malformed_and_unknown_unsubscribe_responses_preserve_outstanding_correlation( #[test] fn unsubscribe_response_cannot_consume_subscription_command_kind() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedSubscription)?; + correlation + .register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedSubscription)?; let matched = read_text_over_loopback(MATCHED_UNSUBSCRIBE_SUCCESS)?; let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( &matched, &mut correlation, ) { - Ok(_) => return Err(io::Error::other("unsubscribe consumed subscription correlation").into()), + Ok(_) => { + return Err(io::Error::other("unsubscribe consumed subscription correlation").into()); + } Err(error) => error, }; assert!(matches!( @@ -438,7 +514,8 @@ fn unsubscribe_response_cannot_consume_subscription_command_kind() -> Result<(), #[test] fn matched_unsubscribe_protocol_error_consumes_only_its_command() -> Result<(), Box> { let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; + correlation + .register_command_for(8, WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe)?; let matched = read_text_over_loopback(MATCHED_UNSUBSCRIBE_ERROR)?; let error = match WebDriverBiDiNavigationCommittedUnsubscribeResult::parse_and_correlate( From 90395f81f9a4e7a92644acb9f566da2f491cd146 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:29:08 +0900 Subject: [PATCH 34/36] test(network): exercise existing raw subscription boundary Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae (cherry picked from commit 122ca13939d1ce1e199cba94540945919bddc1c3) Signed-off-by: Seongho Bae --- ...cription_response_connection_provenance.rs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_subscription_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_subscription_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_subscription_response_connection_provenance.rs new file mode 100644 index 000000000..169d7f29b --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_subscription_response_connection_provenance.rs @@ -0,0 +1,206 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{SocketAddr, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiNavigationCommittedSubscriptionResponseError, + WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, +}; + +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"; +const SUBSCRIBE_SUCCESS_RESPONSE: &[u8] = + br#"{"type":"success","id":42,"result":{"subscription":"subscription-a"}}"#; +const SUBSCRIBE_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":42,"error":"invalid argument","message":"blocked","stacktrace":"remote"}"#; + +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 marker = header[1] & 0x7f; + let length = match marker { + 0..=125 => usize::from(marker), + 126 => { + let mut extended = [0_u8; 2]; + stream.read_exact(&mut extended)?; + usize::from(u16::from_be_bytes(extended)) + } + 127 => { + let mut extended = [0_u8; 8]; + stream.read_exact(&mut extended)?; + let length = u64::from_be_bytes(extended); + usize::try_from(length).map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidData, + "subscription frame length exceeds usize", + ) + })? + } + _ => unreachable!(), + }; + 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) +} + +fn establish(local_addr: SocketAddr) -> Result> { + 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()?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new( + connection, + WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?, + )? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +fn read_response( + established: WebDriverBiDiWebSocketEstablished, +) -> Result> { + let (_, frame) = established.read_frame(Duration::from_millis(500))?; + let text = match WebDriverBiDiWebSocketMessageAssembler::new().push_frame(frame)? { + WebDriverBiDiWebSocketMessageAssembly::Text(message) => message, + other => { + return Err(io::Error::other(format!( + "replacement subscription connection produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + Ok(text) +} + +fn assert_replacement_rejected(foreign_response: &'static [u8]) -> Result<(), Box> { + let original_listener = TcpListener::bind(("127.0.0.1", 0))?; + let original_addr = original_listener.local_addr()?; + let expected_json = br#"{"id":42,"method":"session.subscribe","params":{"events":["browsingContext.navigationCommitted"],"contexts":["context-a"]}}"#.to_vec(); + let original_server = thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = original_listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + let command = read_masked_text_frame(&mut stream)?; + if command != expected_json { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected subscription command on original connection", + )); + } + let (mut replacement, _) = original_listener.accept()?; + read_opening_request(&mut replacement)?; + replacement.write_all(OPENING_RESPONSE)?; + replacement.write_all(&[0x81, foreign_response.len() as u8])?; + replacement.write_all(foreign_response)?; + stream.write_all(&[0x81, SUBSCRIBE_SUCCESS_RESPONSE.len() as u8])?; + stream.write_all(SUBSCRIBE_SUCCESS_RESPONSE) + }); + + let original = establish(original_addr)?; + let mut registry = BrowserAuthorityRegistry::new(); + let session = registry.register_session(SESSION_ID)?; + let context = registry.register_context(session, "context-a")?; + let command = WebDriverBiDiNavigationCommittedSubscriptionCommand::new( + 42, + ®istry, + session, + context, + "context-a", + )?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(43, WebDriverBiDiCommandKind::SessionStatus)?; + let original = command.send( + ®istry, + original, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + assert_eq!(correlation.outstanding_count(), 2); + + let replacement_response = read_response(establish(original_addr)?)?; + let parsed = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &replacement_response, + &mut correlation, + ); + + let original_response = read_response(original)?; + original_server + .join() + .map_err(|_| io::Error::other("original subscription server panicked"))??; + assert!( + matches!( + parsed, + Err( + WebDriverBiDiNavigationCommittedSubscriptionResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 42 + } + } + ) + ), + "replacement response must fail for exact connection mismatch: {parsed:?}" + ); + assert_eq!(correlation.outstanding_count(), 2); + let accepted = WebDriverBiDiNavigationCommittedSubscriptionResult::parse_and_correlate( + &original_response, + &mut correlation, + )?; + assert_eq!(accepted.command_id(), 42); + assert_eq!(accepted.subscription_id(), "subscription-a"); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn replacement_success_cannot_consume_original_subscription_command() -> Result<(), Box> +{ + assert_replacement_rejected(SUBSCRIBE_SUCCESS_RESPONSE) +} + +#[test] +fn replacement_error_cannot_consume_original_subscription_command() -> Result<(), Box> { + assert_replacement_rejected(SUBSCRIBE_ERROR_RESPONSE) +} From cb0c426103180138283e1097e60f38650bfbd821 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:30:57 +0900 Subject: [PATCH 35/36] test(network): read subscription receipts through sealed connection reader Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- ...ver_bidi_navigation_committed_unsubscribe.rs | 17 +++++++++++------ ...navigation_committed_unsubscribe_failures.rs | 16 ++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs index 05e228f74..08cfe8ba9 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe.rs @@ -8,13 +8,14 @@ use std::{ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint}; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -153,10 +154,13 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() Duration::from_millis(500), )?; - let (established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let (established, text) = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), other => { return Err(io::Error::other(format!( "session.subscribe response produced unexpected assembly state: {other:?}" @@ -181,6 +185,7 @@ fn validated_subscription_can_be_unsubscribed_without_losing_opaque_text() assert_eq!(correlation.outstanding_count(), 1); let (_established, frame) = established.read_frame(Duration::from_millis(500))?; + let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); let text = match assembler.push_frame(frame)? { WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, other => { diff --git a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs index 9b50f5ffd..989a2da7e 100644 --- a/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs +++ b/crates/originweave-network/tests/webdriver_bidi_navigation_committed_unsubscribe_failures.rs @@ -10,7 +10,7 @@ use originweave_core::{BrowserAuthorityRegistry, WebDriverBiDiWebSocketEndpoint} use originweave_network::{ MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, - WebDriverBiDiNavigationCommittedSubscriptionCommand, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiNavigationCommittedSubscriptionCommand, WebDriverBiDiNavigationCommittedSubscriptionResult, WebDriverBiDiNavigationCommittedUnsubscribeCommand, WebDriverBiDiNavigationCommittedUnsubscribeCommandError, @@ -19,7 +19,8 @@ use originweave_network::{ WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketMessageReader, + WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -210,10 +211,13 @@ fn obtain_subscription_receipt() WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), Duration::from_millis(500), )?; - let (_established, frame) = established.read_frame(Duration::from_millis(500))?; - let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new(); - let text = match assembler.push_frame(frame)? { - WebDriverBiDiWebSocketMessageAssembly::Text(text) => text, + let (_established, text) = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { + established, + message, + } => (established, message), other => { return Err(io::Error::other(format!( "session.subscribe response produced unexpected assembly state: {other:?}" From 4868d3e9f19133ac3382ee8532878aef27468893 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:31:37 +0900 Subject: [PATCH 36/36] docs(network): record sealed subscription parent adoption Commit-Message-Assisted-by: Codex (via Codex) Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../webdriver-bidi-navigation-unsubscribe.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db4fe2fb5..4542111c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Carried replacement-connection subscription-reply rejection into unsubscribe preparation, preserving opaque identifiers and existing teardown checks without claiming that pending events have drained. - Reject navigation-subscription replies received on replacement connections while keeping the original request available for its own reply; a successful subscription still does not prove that a navigation occurred. - Carried replacement-connection click-reply rejection into the navigation-subscription stack while preserving deadline rejection, unrelated pending requests and conservative handling of uncertain writes. - Reject invalid navigation-subscription deadlines before reserving a pending request, preserving existing requests and leaving the rejected identifier reusable without sending subscription bytes. diff --git a/docs/traceability/webdriver-bidi-navigation-unsubscribe.md b/docs/traceability/webdriver-bidi-navigation-unsubscribe.md index b59e2f2ea..a60b80a87 100644 --- a/docs/traceability/webdriver-bidi-navigation-unsubscribe.md +++ b/docs/traceability/webdriver-bidi-navigation-unsubscribe.md @@ -48,6 +48,20 @@ OriginWeave uses only the by-id form and accepts the identifier only through its ## Authority and follow-up +### Subscription receipt parent adoption + +Ordinary merge `9e85cadc` adopts #277 `46ae62aa31e35c702cd61c16322d05c7a9c35da1` +without changing either unsubscribe production module. Canonical regression replay +`90395f81` reproduced both replacement success and error consuming the original +subscription (0/2 passing). Adoption exposed two fixture calls that still supplied +raw messages; `cb0c4261` uses the existing sealed reader and preserves its returned +connection for the subsequent unsubscribe exchange. The escaped opaque identifier, +deadline/no-byte checks and all original unsubscribe assertions remain intact. +Unsubscribe dispatch and response provenance remain separate unfinished boundaries; +this integration must not be interpreted as authenticated subscription teardown. +Exact local and hosted gates must be revalidated for this combined head; neither +parent coverage nor predecessor screenshots establish its acceptance. + This adapter performs no policy authorization, destination approval, browser authentication, action dispatch, semantic observation, or durable evidence escalation. The browser-domain owner remains OriginWeave; WebDriver BiDi remains an adapter. Integration into protected main remains parent-first and non-destructive, and exact-head hosted evidence does not transfer from predecessor heads. ### References — APA 7th