From 0b3cf76d2720e9c63ec4bd496760dd6db9fc5e95 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:06:54 -0700 Subject: [PATCH 01/43] test(network): require typed session.status response --- .../webdriver_bidi_session_status_response.rs | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_status_response.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs new file mode 100644 index 000000000..10d8d8ef2 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -0,0 +1,160 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + 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 STATUS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true,"message":"capacity available"}}"#; +const STATUS_RESPONSE_MISSING_READY: &[u8] = + br#"{"type":"success","id":7,"result":{"message":"capacity available"}}"#; + +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) +} + +fn send_status_and_read_response( + response: &'static [u8], +) -> Result< + ( + WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, + ), + 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.status","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.status command", + )); + } + stream.write_all(&[0x81, response.len() as u8])?; + stream.write_all(response) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = WebDriverBiDiSessionStatusCommand::new(7)?.send( + 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.status response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("session.status response test server panicked"))??; + Ok((text, correlation)) +} + +#[test] +fn session_status_success_result_is_typed_correlated_and_message_redacted_in_debug() +-> Result<(), Box> { + let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE)?; + let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation)?; + + assert_eq!(result.command_id(), 7); + assert!(result.ready()); + assert_eq!(result.message(), "capacity available"); + assert_eq!(correlation.outstanding_count(), 0); + + let debug = format!("{result:?}"); + assert!(debug.contains("message_len")); + assert!(!debug.contains("capacity available")); + Ok(()) +} + +#[test] +fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result<(), Box> { + let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_MISSING_READY)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + + assert!(matches!( + parsed, + Err(WebDriverBiDiSessionStatusResponseError::MissingReady) + )); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From 99226e247a5baf3478991a5483cd486cc9956172 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:08:35 -0700 Subject: [PATCH 02/43] test(network): format session.status response regression --- .../tests/webdriver_bidi_session_status_response.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 10d8d8ef2..ef088e06b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -19,7 +19,8 @@ use originweave_network::{ 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 STATUS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true,"message":"capacity available"}}"#; +const STATUS_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"ready":true,"message":"capacity available"}}"#; const STATUS_RESPONSE_MISSING_READY: &[u8] = br#"{"type":"success","id":7,"result":{"message":"capacity available"}}"#; @@ -147,7 +148,8 @@ fn session_status_success_result_is_typed_correlated_and_message_redacted_in_deb } #[test] -fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result<(), Box> { +fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result<(), Box> +{ let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_MISSING_READY)?; let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); From cdd20183b7833d478af524eefffc09d4ddfa09e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:11:34 -0700 Subject: [PATCH 03/43] feat(network): parse typed session.status response --- .../webdriver_bidi_session_status_response.rs | 648 ++++++++++++++++++ 1 file changed, 648 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_session_status_response.rs diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs new file mode 100644 index 000000000..d13ec4901 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -0,0 +1,648 @@ +use std::{error::Error, fmt}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiWebSocketTextMessage, +}; + +/// Maximum decoded byte length retained from WebDriver BiDi `session.status` implementation text. +/// +/// The protocol requires an implementation-defined status message but does not define a size +/// ceiling. OriginWeave therefore keeps this operator-facing field within a smaller reviewed bound +/// than the surrounding WebSocket message and never includes its contents in `Debug` output. +pub const MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE: usize = 4_096; + +/// Typed, correlated successful result of one WebDriver BiDi `session.status` command. +/// +/// This value retains only the exact correlated command id, the standards-defined readiness bit, +/// and one bounded implementation-defined status message. It carries no generic JSON value, +/// browser capability, secret, origin grant, or Agent authority. +#[derive(Eq, PartialEq)] +pub struct WebDriverBiDiSessionStatusResult { + command_id: u64, + ready: bool, + message: String, +} + +impl fmt::Debug for WebDriverBiDiSessionStatusResult { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiSessionStatusResult") + .field("command_id", &self.command_id) + .field("ready", &self.ready) + .field("message_len", &self.message.len()) + .finish() + } +} + +impl WebDriverBiDiSessionStatusResult { + /// 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 `result.ready` and `result.message`; correlation is consumed + /// only after that result is valid, so malformed success bodies cannot silently retire an id. + /// A correlatable protocol-error response consumes its matching id and returns a typed remote + /// protocol failure. Events, null-id errors, and unknown ids fail closed through the existing + /// correlation boundary. + pub fn parse_and_correlate( + message: &WebDriverBiDiWebSocketTextMessage, + correlation: &mut WebDriverBiDiCommandCorrelation, + ) -> Result { + let envelope = WebDriverBiDiJsonEnvelope::parse(message) + .map_err(|source| WebDriverBiDiSessionStatusResponseError::Envelope { source })?; + + match envelope.kind() { + WebDriverBiDiJsonEnvelopeKind::Success => { + let projected = StatusProjection::parse(message.as_str())?; + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + })?; + Ok(Self { + command_id: completed.command_id(), + ready: projected.ready, + message: projected.message, + }) + } + WebDriverBiDiJsonEnvelopeKind::Error => { + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + })?; + Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }) + } + WebDriverBiDiJsonEnvelopeKind::Event => correlation + .correlate_response(&envelope) + .map(|completed| Self { + command_id: completed.command_id(), + ready: false, + message: String::new(), + }) + .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { source }), + } + } + + /// Return the exact local command identifier consumed by this result. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return whether the remote end reports readiness to create a new session. + #[must_use] + pub const fn ready(&self) -> bool { + self.ready + } + + /// Borrow the bounded implementation-defined readiness message. + #[must_use] + pub fn message(&self) -> &str { + &self.message + } +} + +/// Fail-closed failures while admitting one typed WebDriver BiDi `session.status` response. +#[derive(Debug)] +pub enum WebDriverBiDiSessionStatusResponseError { + /// Common local-end JSON envelope validation failed. + Envelope { + /// Exact common-envelope validation failure. + source: WebDriverBiDiJsonEnvelopeError, + }, + /// The successful result object omits the required `ready` member. + MissingReady, + /// The successful result object's `ready` member is not a JSON boolean. + InvalidReady, + /// The successful result object omits the required `message` member. + MissingMessage, + /// The successful result object's `message` member is not JSON text. + InvalidMessage, + /// The successful result repeats one command-specific member and is ambiguous. + DuplicateResultMember { + /// Stable command-specific member name that was repeated. + member: &'static str, + }, + /// The decoded implementation-defined status message exceeds the reviewed bound. + MessageTooLarge { + /// Maximum decoded message 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, + }, +} + +impl fmt::Display for WebDriverBiDiSessionStatusResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Envelope { .. } => { + formatter.write_str("WebDriver BiDi session.status envelope is invalid") + } + Self::MissingReady => { + formatter.write_str("WebDriver BiDi session.status result is missing ready") + } + Self::InvalidReady => { + formatter.write_str("WebDriver BiDi session.status result ready is invalid") + } + Self::MissingMessage => { + formatter.write_str("WebDriver BiDi session.status result is missing message") + } + Self::InvalidMessage => { + formatter.write_str("WebDriver BiDi session.status result message is invalid") + } + Self::DuplicateResultMember { .. } => formatter + .write_str("WebDriver BiDi session.status result contains a duplicate member"), + Self::MessageTooLarge { .. } => formatter + .write_str("WebDriver BiDi session.status result message exceeds the size bound"), + Self::InvalidResultProjection => { + formatter.write_str("WebDriver BiDi session.status result projection is invalid") + } + Self::Correlation { .. } => { + formatter.write_str("WebDriver BiDi session.status response correlation failed") + } + Self::RemoteProtocolError { .. } => { + formatter.write_str("WebDriver BiDi session.status returned a protocol error") + } + } + } +} + +impl Error for WebDriverBiDiSessionStatusResponseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Envelope { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::MissingReady + | Self::InvalidReady + | Self::MissingMessage + | Self::InvalidMessage + | Self::DuplicateResultMember { .. } + | Self::MessageTooLarge { .. } + | Self::InvalidResultProjection + | Self::RemoteProtocolError { .. } => None, + } + } +} + +struct StatusProjection { + ready: bool, + message: String, +} + +impl StatusProjection { + fn parse(text: &str) -> Result { + let mut cursor = ProjectionCursor::new(text); + cursor.skip_whitespace(); + if !cursor.consume_byte(b'{') { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + cursor.skip_whitespace(); + if cursor.consume_byte(b'}') { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + + loop { + cursor.skip_whitespace(); + let key = cursor + .parse_string() + .ok_or(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection)?; + cursor.skip_whitespace(); + if !cursor.consume_byte(b':') { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + cursor.skip_whitespace(); + if key == "result" { + return cursor.parse_result_object(); + } + if !cursor.skip_value() { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + cursor.skip_whitespace(); + if cursor.consume_byte(b'}') { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + if !cursor.consume_byte(b',') { + return Err(WebDriverBiDiSessionStatusResponseError::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(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + self.skip_whitespace(); + let mut ready = None; + let mut message = None; + if self.consume_byte(b'}') { + return Err(WebDriverBiDiSessionStatusResponseError::MissingReady); + } + + loop { + self.skip_whitespace(); + let key = self + .parse_string() + .ok_or(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection)?; + self.skip_whitespace(); + if !self.consume_byte(b':') { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + self.skip_whitespace(); + match key.as_str() { + "ready" => { + if ready.is_some() { + return Err( + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { + member: "ready", + }, + ); + } + ready = Some(self.parse_ready()?); + } + "message" => { + if message.is_some() { + return Err( + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { + member: "message", + }, + ); + } + let parsed = self + .parse_string() + .ok_or(WebDriverBiDiSessionStatusResponseError::InvalidMessage)?; + if parsed.len() > MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE { + return Err(WebDriverBiDiSessionStatusResponseError::MessageTooLarge { + maximum_bytes: MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, + }); + } + message = Some(parsed); + } + _ => { + if !self.skip_value() { + return Err( + WebDriverBiDiSessionStatusResponseError::InvalidResultProjection, + ); + } + } + } + self.skip_whitespace(); + if self.consume_byte(b'}') { + break; + } + if !self.consume_byte(b',') { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + } + + Ok(StatusProjection { + ready: ready.ok_or(WebDriverBiDiSessionStatusResponseError::MissingReady)?, + message: message.ok_or(WebDriverBiDiSessionStatusResponseError::MissingMessage)?, + }) + } + + fn parse_ready(&mut self) -> Result { + if self.consume_literal(b"true") { + Ok(true) + } else if self.consume_literal(b"false") { + Ok(false) + } else { + if !self.skip_value() { + return Err(WebDriverBiDiSessionStatusResponseError::InvalidResultProjection); + } + Err(WebDriverBiDiSessionStatusResponseError::InvalidReady) + } + } + + 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.checked_add(width)?; + let character = self.input.get(self.index..end)?; + output.push_str(character); + 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; + }; + let scalar = 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; + } + 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00) + } else if (0xdc00..=0xdfff).contains(&first) { + return false; + } else { + u32::from(first) + }; + let Some(character) = char::from_u32(scalar) else { + return false; + }; + output.push(character); + 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_required_fields_unknown_metadata_and_escaped_keys() { + let projected = StatusProjection::parse( + r#"{"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"message":"re\u0061dy \ud83d\ude80","extra":{},"ready":false}}"#, + ); + assert!(projected.is_ok()); + let projected = projected.ok(); + assert_eq!(projected.as_ref().map(|value| value.ready), Some(false)); + assert_eq!( + projected.as_ref().map(|value| value.message.as_str()), + Some("ready 🚀") + ); + } + + #[test] + fn projection_rejects_missing_invalid_duplicate_and_oversized_required_fields() { + let cases = [ + (r#"{"result":{"message":"x"}}"#.to_owned(), "missing ready"), + (r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), "invalid ready"), + (r#"{"result":{"ready":true}}"#.to_owned(), "missing message"), + (r#"{"result":{"ready":true,"message":false}}"#.to_owned(), "invalid message"), + ( + r#"{"result":{"ready":true,"ready":false,"message":"x"}}"#.to_owned(), + "duplicate ready", + ), + ( + r#"{"result":{"ready":true,"message":"x","message":"y"}}"#.to_owned(), + "duplicate message", + ), + ( + format!( + "{{\"result\":{{\"ready\":true,\"message\":\"{}\"}}}}", + "x".repeat(MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE + 1) + ), + "oversized message", + ), + ]; + + for (document, label) in cases { + assert!(StatusProjection::parse(&document).is_err(), "{label}"); + } + } + + #[test] + fn projection_cursor_rejects_malformed_private_inputs_without_panicking() { + let malformed = [ + "", + "[]", + "{}", + r#"{"x":}"#, + r#"{"x":1}"#, + r#"{"result":[]}"#, + r#"{"result":{"ready":true,"message":"\uD800"}}"#, + r#"{"result":{"ready":true,"message":"\q"}}"#, + ]; + for document in malformed { + assert!(StatusProjection::parse(document).is_err()); + } + } + + #[test] + fn response_errors_have_stable_redacted_messages_and_sources() { + let envelope = WebDriverBiDiSessionStatusResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert!(envelope.source().is_some()); + assert_eq!( + envelope.to_string(), + "WebDriver BiDi session.status envelope is invalid" + ); + + let correlation = WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert!(correlation.source().is_some()); + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.status response correlation failed" + ); + + let leaf_errors = [ + WebDriverBiDiSessionStatusResponseError::MissingReady, + WebDriverBiDiSessionStatusResponseError::InvalidReady, + WebDriverBiDiSessionStatusResponseError::MissingMessage, + WebDriverBiDiSessionStatusResponseError::InvalidMessage, + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "ready" }, + WebDriverBiDiSessionStatusResponseError::MessageTooLarge { + maximum_bytes: MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, + }, + WebDriverBiDiSessionStatusResponseError::InvalidResultProjection, + WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: 7 }, + ]; + for error in leaf_errors { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } + } +} From 77c6aec885f87eff98c87226934555ec2bdc1fea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:11:58 -0700 Subject: [PATCH 04/43] feat(network): export session.status result boundary --- crates/originweave-network/src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 8669e9af1..a4a8f883e 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -8,9 +8,10 @@ //! the RFC 6455 opening exchange, provides bounded masked client writes and //! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages, //! classifies complete local-end JSON envelopes, tracks bounded command-response -//! correlation, and sends one narrowly typed `session.status` command without -//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or -//! Agent authority. +//! correlation, sends one narrowly typed `session.status` command, and admits its +//! required readiness result through one command-specific correlated parser without +//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or Agent +//! authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -20,6 +21,7 @@ mod webdriver_bidi_command_correlation; mod webdriver_bidi_connection; mod webdriver_bidi_json_envelope; mod webdriver_bidi_session_status_command; +mod webdriver_bidi_session_status_response; mod webdriver_bidi_websocket_frame; mod webdriver_bidi_websocket_handshake; mod webdriver_bidi_websocket_message; @@ -48,6 +50,10 @@ pub use webdriver_bidi_json_envelope::{ pub use webdriver_bidi_session_status_command::{ WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, }; +pub use webdriver_bidi_session_status_response::{ + MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, WebDriverBiDiSessionStatusResponseError, + WebDriverBiDiSessionStatusResult, +}; pub use webdriver_bidi_websocket_frame::{ MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, MAX_WEBSOCKET_FRAME_TIMEOUT, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrame, From 922eb7ea2c97f5f9c0f59c1a419e5c5b659a7830 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:26:30 -0700 Subject: [PATCH 05/43] test(network): harden session.status response projection --- ...er_bidi_session_status_response_hostile.rs | 246 ++++++++++++++++++ 1 file changed, 246 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs new file mode 100644 index 000000000..2a4eedd4a --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -0,0 +1,246 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; + +type StatusRead = ( + WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, +); + +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) +} + +fn write_unmasked_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Result<()> { + match payload.len() { + 0..=125 => { + let length = u8::try_from(payload.len()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "short frame length overflow") + })?; + stream.write_all(&[0x81, length])?; + } + 126..=65_535 => { + let length = u16::try_from(payload.len()).map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "extended frame length overflow") + })?; + stream.write_all(&[0x81, 126])?; + stream.write_all(&length.to_be_bytes())?; + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "hostile response exceeds the reviewed test-frame budget", + )); + } + } + stream.write_all(payload) +} + +fn send_status_and_read_response(response: Vec) -> 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 command = read_masked_text_frame(&mut stream)?; + if command != br#"{"id":7,"method":"session.status","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.status command", + )); + } + write_unmasked_text_frame(&mut stream, &response) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?; + + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let established = WebDriverBiDiSessionStatusCommand::new(7)?.send( + 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.status response produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + + server + .join() + .map_err(|_| io::Error::other("session.status response test server panicked"))??; + Ok((text, correlation)) +} + +fn parse_response( + response: Vec, +) -> Result< + ( + Result, + WebDriverBiDiCommandCorrelation, + ), + Box, +> { + let (text, mut correlation) = send_status_and_read_response(response)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + Ok((parsed, correlation)) +} + +#[test] +fn status_projection_accepts_extensible_json_and_unicode_through_real_transport() +-> Result<(), Box> { + let response = br#"{"type":"success","id":7,"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"message":"re\u0061dy \ud83d\ude80","extra":{},"ready":false}}"# + .to_vec(); + let (parsed, correlation) = parse_response(response)?; + let result = parsed?; + + assert_eq!(result.command_id(), 7); + assert!(!result.ready()); + assert_eq!(result.message(), "ready 🚀"); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn malformed_success_bodies_fail_closed_without_consuming_correlation() -> Result<(), Box> +{ + let oversized = format!( + "{{\"type\":\"success\",\"id\":7,\"result\":{{\"ready\":true,\"message\":\"{}\"}}}}", + "x".repeat(MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE + 1) + ) + .into_bytes(); + let cases = [ + br#"{"type":"success","id":7,"result":{"ready":0,"message":"x"}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":true}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":true,"message":false}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":true,"ready":false,"message":"x"}}"# + .to_vec(), + br#"{"type":"success","id":7,"result":{"ready":true,"message":"x","message":"y"}}"# + .to_vec(), + oversized, + ]; + + for response in cases { + let (parsed, correlation) = parse_response(response)?; + assert!(parsed.is_err()); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) +} + +#[test] +fn envelope_correlation_and_remote_error_failures_preserve_exact_command_semantics() +-> Result<(), Box> { + let (invalid_envelope, correlation) = parse_response( + br#"{"type":"success","id":7,"result":{"ready":true,"message":"x"}"#.to_vec(), + )?; + assert!(matches!( + invalid_envelope, + Err(WebDriverBiDiSessionStatusResponseError::Envelope { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let (unknown_id, correlation) = parse_response( + br#"{"type":"success","id":8,"result":{"ready":true,"message":"x"}}"#.to_vec(), + )?; + assert!(matches!( + unknown_id, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let (event, correlation) = parse_response( + br#"{"type":"event","method":"log.entryAdded","params":{}}"#.to_vec(), + )?; + assert!(matches!( + event, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let (remote_error, correlation) = parse_response( + br#"{"type":"error","id":7,"error":"unknown error","message":"remote refused status","stacktrace":""}"# + .to_vec(), + )?; + assert!(matches!( + remote_error, + Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: 7 }) + )); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} From ea20fd84f22990a95ded70df6fda030a8633b8e4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:27:57 -0700 Subject: [PATCH 06/43] style(network): apply canonical session.status test formatting --- ...er_bidi_session_status_response_hostile.rs | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index 2a4eedd4a..635073bbe 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, - WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, + MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, WebDriverBiDiCommandCorrelation, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, + WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -78,7 +78,10 @@ fn write_unmasked_text_frame(stream: &mut TcpStream, payload: &[u8]) -> io::Resu } 126..=65_535 => { let length = u16::try_from(payload.len()).map_err(|_| { - io::Error::new(io::ErrorKind::InvalidInput, "extended frame length overflow") + io::Error::new( + io::ErrorKind::InvalidInput, + "extended frame length overflow", + ) })?; stream.write_all(&[0x81, 126])?; stream.write_all(&length.to_be_bytes())?; @@ -224,9 +227,8 @@ fn envelope_correlation_and_remote_error_failures_preserve_exact_command_semanti )); assert_eq!(correlation.outstanding_count(), 1); - let (event, correlation) = parse_response( - br#"{"type":"event","method":"log.entryAdded","params":{}}"#.to_vec(), - )?; + let (event, correlation) = + parse_response(br#"{"type":"event","method":"log.entryAdded","params":{}}"#.to_vec())?; assert!(matches!( event, Err(WebDriverBiDiSessionStatusResponseError::Correlation { .. }) From 361ad17bb2071a9f28ede42d2a5eace60c53958c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:29:17 -0700 Subject: [PATCH 07/43] style(network): apply canonical session.status response formatting --- .../webdriver_bidi_session_status_response.rs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index d13ec4901..6ed1b7bf1 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -57,9 +57,9 @@ impl WebDriverBiDiSessionStatusResult { let projected = StatusProjection::parse(message.as_str())?; let completed = correlation .correlate_response(&envelope) - .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { - source, - })?; + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, + )?; Ok(Self { command_id: completed.command_id(), ready: projected.ready, @@ -69,12 +69,14 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiJsonEnvelopeKind::Error => { let completed = correlation .correlate_response(&envelope) - .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { - source, - })?; - Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { - command_id: completed.command_id(), - }) + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, + )?; + Err( + WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }, + ) } WebDriverBiDiJsonEnvelopeKind::Event => correlation .correlate_response(&envelope) @@ -566,9 +568,15 @@ mod tests { fn projection_rejects_missing_invalid_duplicate_and_oversized_required_fields() { let cases = [ (r#"{"result":{"message":"x"}}"#.to_owned(), "missing ready"), - (r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), "invalid ready"), + ( + r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), + "invalid ready", + ), (r#"{"result":{"ready":true}}"#.to_owned(), "missing message"), - (r#"{"result":{"ready":true,"message":false}}"#.to_owned(), "invalid message"), + ( + r#"{"result":{"ready":true,"message":false}}"#.to_owned(), + "invalid message", + ), ( r#"{"result":{"ready":true,"ready":false,"message":"x"}}"#.to_owned(), "duplicate ready", From bbe5ebd8c8d0f3f086560016d76531a814dc3656 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:42:55 -0700 Subject: [PATCH 08/43] test(network): exercise validated session.status projection shapes --- ...er_bidi_session_status_response_hostile.rs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index 635073bbe..4631b9e81 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -179,6 +179,24 @@ fn status_projection_accepts_extensible_json_and_unicode_through_real_transport( Ok(()) } +#[test] +fn status_projection_accepts_all_string_escapes_and_unknown_value_shapes() +-> Result<(), Box> { + let response = br#"{"type":"success","id":7,"before":{"k":"v"},"result":{"unknown_string":"x","unknown_true":true,"unknown_false":false,"unknown_null":null,"unknown_number":-12.5e+2,"unknown_empty_array":[],"unknown_array":[{},[],"x"],"unknown_empty_object":{},"unknown_object":{"k":"v"},"ready":true,"message":"quote:\" slash:\/ backslash:\\ back:\b form:\f newline:\n return:\r tab:\t bmp:\u00AF raw:é"},"after":[1]}"# + .to_vec(); + let (parsed, correlation) = parse_response(response)?; + let result = parsed?; + + assert_eq!(result.command_id(), 7); + assert!(result.ready()); + assert_eq!( + result.message(), + "quote:\" slash:/ backslash:\\ back:\u{0008} form:\u{000c} newline:\n return:\r tab:\t bmp:¯ raw:é" + ); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + #[test] fn malformed_success_bodies_fail_closed_without_consuming_correlation() -> Result<(), Box> { @@ -188,7 +206,13 @@ fn malformed_success_bodies_fail_closed_without_consuming_correlation() -> Resul ) .into_bytes(); let cases = [ + br#"{"type":"success","id":7,"result":{}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"message":"x"}}"#.to_vec(), br#"{"type":"success","id":7,"result":{"ready":0,"message":"x"}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":"true","message":"x"}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":null,"message":"x"}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":[],"message":"x"}}"#.to_vec(), + br#"{"type":"success","id":7,"result":{"ready":{},"message":"x"}}"#.to_vec(), br#"{"type":"success","id":7,"result":{"ready":true}}"#.to_vec(), br#"{"type":"success","id":7,"result":{"ready":true,"message":false}}"#.to_vec(), br#"{"type":"success","id":7,"result":{"ready":true,"ready":false,"message":"x"}}"# @@ -235,6 +259,26 @@ fn envelope_correlation_and_remote_error_failures_preserve_exact_command_semanti )); assert_eq!(correlation.outstanding_count(), 1); + let (unknown_error, correlation) = parse_response( + br#"{"type":"error","id":8,"error":"unknown error","message":"remote refused status"}"# + .to_vec(), + )?; + assert!(matches!( + unknown_error, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + + let (null_error, correlation) = parse_response( + br#"{"type":"error","id":null,"error":"unknown error","message":"remote refused status"}"# + .to_vec(), + )?; + assert!(matches!( + null_error, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { .. }) + )); + assert_eq!(correlation.outstanding_count(), 1); + let (remote_error, correlation) = parse_response( br#"{"type":"error","id":7,"error":"unknown error","message":"remote refused status","stacktrace":""}"# .to_vec(), From 27ac412d20bdf3ddb3a511c087485a9526cb5c37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 00:53:23 -0700 Subject: [PATCH 09/43] test(network): keep unicode session.status fixture UTF-8 --- .../tests/webdriver_bidi_session_status_response_hostile.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index 4631b9e81..576d1cf21 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -182,7 +182,8 @@ fn status_projection_accepts_extensible_json_and_unicode_through_real_transport( #[test] fn status_projection_accepts_all_string_escapes_and_unknown_value_shapes() -> Result<(), Box> { - let response = br#"{"type":"success","id":7,"before":{"k":"v"},"result":{"unknown_string":"x","unknown_true":true,"unknown_false":false,"unknown_null":null,"unknown_number":-12.5e+2,"unknown_empty_array":[],"unknown_array":[{},[],"x"],"unknown_empty_object":{},"unknown_object":{"k":"v"},"ready":true,"message":"quote:\" slash:\/ backslash:\\ back:\b form:\f newline:\n return:\r tab:\t bmp:\u00AF raw:é"},"after":[1]}"# + let response = r#"{"type":"success","id":7,"before":{"k":"v"},"result":{"unknown_string":"x","unknown_true":true,"unknown_false":false,"unknown_null":null,"unknown_number":-12.5e+2,"unknown_empty_array":[],"unknown_array":[{},[],"x"],"unknown_empty_object":{},"unknown_object":{"k":"v"},"ready":true,"message":"quote:\" slash:\/ backslash:\\ back:\b form:\f newline:\n return:\r tab:\t bmp:\u00AF raw:é"},"after":[1]}"# + .as_bytes() .to_vec(); let (parsed, correlation) = parse_response(response)?; let result = parsed?; From c146151386c7f3b11798b300309a7710d5220439 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 02:17:21 -0700 Subject: [PATCH 10/43] test(network): close session.status coverage gaps --- .../webdriver_bidi_session_status_response.rs | 110 ++++++++++++++++-- 1 file changed, 103 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 6ed1b7bf1..8f0f4cac3 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -78,14 +78,11 @@ impl WebDriverBiDiSessionStatusResult { }, ) } - WebDriverBiDiJsonEnvelopeKind::Event => correlation - .correlate_response(&envelope) - .map(|completed| Self { - command_id: completed.command_id(), - ready: false, - message: String::new(), + WebDriverBiDiJsonEnvelopeKind::Event => { + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::EventIsNotResponse, }) - .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { source }), + } } } @@ -562,6 +559,11 @@ mod tests { projected.as_ref().map(|value| value.message.as_str()), Some("ready 🚀") ); + + let spaced = StatusProjection::parse( + "\n\t { \r\n \"result\" : { \"ready\" : true , \"message\" : \"ok\" } }", + ); + assert!(spaced.is_ok()); } #[test] @@ -607,7 +609,14 @@ mod tests { "{}", r#"{"x":}"#, r#"{"x":1}"#, + r#"{"x" 1}"#, + r#"{"x":1 ?}"#, + r#"{?}"#, r#"{"result":[]}"#, + r#"{"result":{"ready" true,"message":"x"}}"#, + r#"{"result":{"ready":true "message":"x"}}"#, + r#"{"result":{"ready":?,"message":"x"}}"#, + r#"{"result":{"ready":true,"message":"x","extra":?}}"#, r#"{"result":{"ready":true,"message":"\uD800"}}"#, r#"{"result":{"ready":true,"message":"\q"}}"#, ]; @@ -616,6 +625,93 @@ mod tests { } } + #[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 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)); + let mut output = String::new(); + let mut escape = ProjectionCursor::new("u0061"); + assert!(escape.parse_escape(&mut output)); + assert_eq!(output, "a"); + let mut output = String::new(); + let mut escape = ProjectionCursor::new("uD83D\\uDE80"); + assert!(escape.parse_escape(&mut output)); + assert_eq!(output, "🚀"); + + for sequence in [ + "u", + "uZZZZ", + "uD800x", + "uD800\\x", + "uD800\\u", + "uD800\\u0041", + "uDC00", + ] { + let mut output = String::new(); + let mut escape = ProjectionCursor::new(sequence); + assert!(!escape.parse_escape(&mut output), "{sequence}"); + } + + let mut hex = ProjectionCursor::new("09aF"); + assert_eq!(hex.parse_hex_u16(), Some(0x09af)); + let mut hex = ProjectionCursor::new("0"); + assert!(hex.parse_hex_u16().is_none()); + let mut hex = ProjectionCursor::new("00G0"); + assert!(hex.parse_hex_u16().is_none()); + } + #[test] fn response_errors_have_stable_redacted_messages_and_sources() { let envelope = WebDriverBiDiSessionStatusResponseError::Envelope { From 13ac0968de04b22c7519d694501c21b32844c03b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 03:10:46 -0700 Subject: [PATCH 11/43] fix(network): make session status unicode decoding total --- .../webdriver_bidi_session_status_response.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 8f0f4cac3..43da5976d 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -503,7 +503,7 @@ impl<'a> ProjectionCursor<'a> { let Some(first) = self.parse_hex_u16() else { return false; }; - let scalar = if (0xd800..=0xdbff).contains(&first) { + if (0xd800..=0xdbff).contains(&first) { if !self.consume_byte(b'\\') || !self.consume_byte(b'u') { return false; } @@ -513,17 +513,14 @@ impl<'a> ProjectionCursor<'a> { if !(0xdc00..=0xdfff).contains(&second) { return false; } - 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00) + output.push_str(&String::from_utf16_lossy(&[first, second])); + true } else if (0xdc00..=0xdfff).contains(&first) { - return false; + false } else { - u32::from(first) - }; - let Some(character) = char::from_u32(scalar) else { - return false; - }; - output.push(character); - true + output.push_str(&String::from_utf16_lossy(&[first])); + true + } } fn parse_hex_u16(&mut self) -> Option { @@ -639,6 +636,8 @@ mod tests { 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()); From a31983a682f0c383923e40b5d57ee34f284bad58 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 03:11:30 -0700 Subject: [PATCH 12/43] test(network): cover multi-member BiDi metadata objects --- .../tests/webdriver_bidi_session_status_response_hostile.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index 576d1cf21..2216f80dd 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -182,7 +182,7 @@ fn status_projection_accepts_extensible_json_and_unicode_through_real_transport( #[test] fn status_projection_accepts_all_string_escapes_and_unknown_value_shapes() -> Result<(), Box> { - let response = r#"{"type":"success","id":7,"before":{"k":"v"},"result":{"unknown_string":"x","unknown_true":true,"unknown_false":false,"unknown_null":null,"unknown_number":-12.5e+2,"unknown_empty_array":[],"unknown_array":[{},[],"x"],"unknown_empty_object":{},"unknown_object":{"k":"v"},"ready":true,"message":"quote:\" slash:\/ backslash:\\ back:\b form:\f newline:\n return:\r tab:\t bmp:\u00AF raw:é"},"after":[1]}"# + let response = r#"{"type":"success","id":7,"before":{"k":"v","sequence":2},"result":{"unknown_string":"x","unknown_true":true,"unknown_false":false,"unknown_null":null,"unknown_number":-12.5e+2,"unknown_empty_array":[],"unknown_array":[{},[],"x"],"unknown_empty_object":{},"unknown_object":{"k":"v","sequence":2},"ready":true,"message":"quote:\" slash:\/ backslash:\\ back:\b form:\f newline:\n return:\r tab:\t bmp:\u00AF raw:é"},"after":[1]}"# .as_bytes() .to_vec(); let (parsed, correlation) = parse_response(response)?; From cd73ba1f4729eeebf7276e378600d9898d5a59ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 03:19:55 -0700 Subject: [PATCH 13/43] fix(network): remove unreachable UTF-8 cursor fallbacks --- .../src/webdriver_bidi_session_status_response.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 43da5976d..8844ae319 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -470,9 +470,8 @@ impl<'a> ProjectionCursor<'a> { } _ => { let width = byte.leading_ones() as usize; - let end = self.index.checked_add(width)?; - let character = self.input.get(self.index..end)?; - output.push_str(character); + let end = self.index + width; + output.push_str(&self.input[self.index..end]); self.index = end; } } From 374070137bebe1aa3d6ae3ac258ae73620a42d35 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 03:35:17 -0700 Subject: [PATCH 14/43] test(network): cover malformed session status result keys --- .../src/webdriver_bidi_session_status_response.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 8844ae319..2d68b8480 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -609,6 +609,7 @@ mod tests { r#"{"x":1 ?}"#, r#"{?}"#, r#"{"result":[]}"#, + r#"{"result":{?}}"#, r#"{"result":{"ready" true,"message":"x"}}"#, r#"{"result":{"ready":true "message":"x"}}"#, r#"{"result":{"ready":?,"message":"x"}}"#, From 70cb5f7b26165ca754f43e9802126447ed5eccd2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:08:35 -0700 Subject: [PATCH 15/43] test(network): cover empty session.status result --- .../webdriver_bidi_session_status_response.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index ef088e06b..347dc5fec 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -23,6 +23,7 @@ const STATUS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true,"message":"capacity available"}}"#; const STATUS_RESPONSE_MISSING_READY: &[u8] = br#"{"type":"success","id":7,"result":{"message":"capacity available"}}"#; +const STATUS_RESPONSE_EMPTY_RESULT: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -160,3 +161,17 @@ fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result< assert_eq!(correlation.outstanding_count(), 1); Ok(()) } + +#[test] +fn empty_status_result_fails_before_consuming_the_outstanding_command() -> Result<(), Box> +{ + let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_EMPTY_RESULT)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + + assert!(matches!( + parsed, + Err(WebDriverBiDiSessionStatusResponseError::MissingReady) + )); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From 288d21c52837998214086720c4635b0dfed9402f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 04:16:05 -0700 Subject: [PATCH 16/43] test(network): cover empty status result in unit build --- ...idi_json_envelope_public_boundary_tests.rs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 27377f08d..6b6487cb0 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,10 +9,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiCommandCorrelation, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiSessionStatusResponseError, + WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, + WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -20,6 +22,7 @@ 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 SUCCESS_MESSAGE: &[u8] = br#"{"type":"success","id":7,"result":{"ready":true,"slash":"\/","upper":"\uABCD"}}"#; +const EMPTY_STATUS_RESULT: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; @@ -38,9 +41,9 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { Ok(()) } -fn parse_over_loopback( +fn read_text_over_loopback( document: &'static [u8], -) -> Result, Box> { +) -> Result> { if document.len() > 125 { return Err(io::Error::other("unit JSON document exceeded one-byte frame length").into()); } @@ -77,12 +80,18 @@ fn parse_over_loopback( .into()); } }; - let parsed = WebDriverBiDiJsonEnvelope::parse(&text); server .join() .map_err(|_| io::Error::other("JSON-envelope unit server panicked"))??; - Ok(parsed) + Ok(text) +} + +fn parse_over_loopback( + document: &'static [u8], +) -> Result, Box> { + let text = read_text_over_loopback(document)?; + Ok(WebDriverBiDiJsonEnvelope::parse(&text)) } #[test] @@ -120,3 +129,18 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), } Ok(()) } + +#[test] +fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box> { + let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command(7)?; + + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + assert!(matches!( + parsed, + Err(WebDriverBiDiSessionStatusResponseError::MissingReady) + )); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} From b7d36b9b67c3d5143142939a10ddc6102663ac0f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 07:02:19 -0700 Subject: [PATCH 17/43] test(network): retain BiDi remote error code --- .../tests/webdriver_bidi_session_status_response_hostile.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index 2216f80dd..e588fa6e5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -286,7 +286,10 @@ fn envelope_correlation_and_remote_error_failures_preserve_exact_command_semanti )?; assert!(matches!( remote_error, - Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: 7 }) + Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + command_id: 7, + ref error_code, + }) if error_code == "unknown error" )); assert_eq!(correlation.outstanding_count(), 0); Ok(()) From 92a925148bf9d9e5b3204cf27f21bdcc860dac5b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 07:05:28 -0700 Subject: [PATCH 18/43] fix(network): retain BiDi remote error code --- .../webdriver_bidi_session_status_response.rs | 228 ++---------------- 1 file changed, 15 insertions(+), 213 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 2d68b8480..4bee7e624 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -43,8 +43,9 @@ impl WebDriverBiDiSessionStatusResult { /// command-specific projection of `result.ready` and `result.message`; correlation is consumed /// only after that result is valid, so malformed success bodies cannot silently retire an id. /// A correlatable protocol-error response consumes its matching id and returns a typed remote - /// protocol failure. Events, null-id errors, and unknown ids fail closed through the existing - /// correlation boundary. + /// protocol failure retaining the protocol error code but not the implementation-defined remote + /// message or stacktrace. Events, null-id errors, and unknown ids fail closed through the + /// existing correlation boundary. pub fn parse_and_correlate( message: &WebDriverBiDiWebSocketTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, @@ -67,6 +68,13 @@ impl WebDriverBiDiSessionStatusResult { }) } WebDriverBiDiJsonEnvelopeKind::Error => { + let error_code = envelope.error_code().map(str::to_owned).ok_or( + WebDriverBiDiSessionStatusResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { + member: "error", + }, + }, + )?; let completed = correlation .correlate_response(&envelope) .map_err( @@ -75,6 +83,7 @@ impl WebDriverBiDiSessionStatusResult { Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), + error_code, }, ) } @@ -142,6 +151,10 @@ pub enum WebDriverBiDiSessionStatusResponseError { RemoteProtocolError { /// Exact local command identifier consumed by the protocol-error response. command_id: u64, + /// Protocol error code retained from the already validated common envelope. + /// + /// The remote implementation-defined message and stacktrace are deliberately not retained. + error_code: String, }, } @@ -538,214 +551,3 @@ impl<'a> ProjectionCursor<'a> { Some(value) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn projection_accepts_required_fields_unknown_metadata_and_escaped_keys() { - let projected = StatusProjection::parse( - r#"{"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"message":"re\u0061dy \ud83d\ude80","extra":{},"ready":false}}"#, - ); - assert!(projected.is_ok()); - let projected = projected.ok(); - assert_eq!(projected.as_ref().map(|value| value.ready), Some(false)); - assert_eq!( - projected.as_ref().map(|value| value.message.as_str()), - Some("ready 🚀") - ); - - let spaced = StatusProjection::parse( - "\n\t { \r\n \"result\" : { \"ready\" : true , \"message\" : \"ok\" } }", - ); - assert!(spaced.is_ok()); - } - - #[test] - fn projection_rejects_missing_invalid_duplicate_and_oversized_required_fields() { - let cases = [ - (r#"{"result":{"message":"x"}}"#.to_owned(), "missing ready"), - ( - r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), - "invalid ready", - ), - (r#"{"result":{"ready":true}}"#.to_owned(), "missing message"), - ( - r#"{"result":{"ready":true,"message":false}}"#.to_owned(), - "invalid message", - ), - ( - r#"{"result":{"ready":true,"ready":false,"message":"x"}}"#.to_owned(), - "duplicate ready", - ), - ( - r#"{"result":{"ready":true,"message":"x","message":"y"}}"#.to_owned(), - "duplicate message", - ), - ( - format!( - "{{\"result\":{{\"ready\":true,\"message\":\"{}\"}}}}", - "x".repeat(MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE + 1) - ), - "oversized message", - ), - ]; - - for (document, label) in cases { - assert!(StatusProjection::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#"{"x":1 ?}"#, - r#"{?}"#, - r#"{"result":[]}"#, - r#"{"result":{?}}"#, - r#"{"result":{"ready" true,"message":"x"}}"#, - r#"{"result":{"ready":true "message":"x"}}"#, - r#"{"result":{"ready":?,"message":"x"}}"#, - r#"{"result":{"ready":true,"message":"x","extra":?}}"#, - r#"{"result":{"ready":true,"message":"\uD800"}}"#, - r#"{"result":{"ready":true,"message":"\q"}}"#, - ]; - for document in malformed { - assert!(StatusProjection::parse(document).is_err()); - } - } - - #[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)); - let mut output = String::new(); - let mut escape = ProjectionCursor::new("u0061"); - assert!(escape.parse_escape(&mut output)); - assert_eq!(output, "a"); - let mut output = String::new(); - let mut escape = ProjectionCursor::new("uD83D\\uDE80"); - assert!(escape.parse_escape(&mut output)); - assert_eq!(output, "🚀"); - - for sequence in [ - "u", - "uZZZZ", - "uD800x", - "uD800\\x", - "uD800\\u", - "uD800\\u0041", - "uDC00", - ] { - let mut output = String::new(); - let mut escape = ProjectionCursor::new(sequence); - assert!(!escape.parse_escape(&mut output), "{sequence}"); - } - - let mut hex = ProjectionCursor::new("09aF"); - assert_eq!(hex.parse_hex_u16(), Some(0x09af)); - let mut hex = ProjectionCursor::new("0"); - assert!(hex.parse_hex_u16().is_none()); - let mut hex = ProjectionCursor::new("00G0"); - assert!(hex.parse_hex_u16().is_none()); - } - - #[test] - fn response_errors_have_stable_redacted_messages_and_sources() { - let envelope = WebDriverBiDiSessionStatusResponseError::Envelope { - source: WebDriverBiDiJsonEnvelopeError::InvalidJson, - }; - assert!(envelope.source().is_some()); - assert_eq!( - envelope.to_string(), - "WebDriver BiDi session.status envelope is invalid" - ); - - let correlation = WebDriverBiDiSessionStatusResponseError::Correlation { - source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, - }; - assert!(correlation.source().is_some()); - assert_eq!( - correlation.to_string(), - "WebDriver BiDi session.status response correlation failed" - ); - - let leaf_errors = [ - WebDriverBiDiSessionStatusResponseError::MissingReady, - WebDriverBiDiSessionStatusResponseError::InvalidReady, - WebDriverBiDiSessionStatusResponseError::MissingMessage, - WebDriverBiDiSessionStatusResponseError::InvalidMessage, - WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "ready" }, - WebDriverBiDiSessionStatusResponseError::MessageTooLarge { - maximum_bytes: MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, - }, - WebDriverBiDiSessionStatusResponseError::InvalidResultProjection, - WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: 7 }, - ]; - for error in leaf_errors { - assert!(error.source().is_none()); - assert!(!error.to_string().is_empty()); - } - } -} From 95110fd3b0c2cb837464ca0d69dbad306d021eaa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 07:07:37 -0700 Subject: [PATCH 19/43] fix(network): preserve parser coverage tests --- .../webdriver_bidi_session_status_response.rs | 214 ++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 4bee7e624..896c36526 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -551,3 +551,217 @@ impl<'a> ProjectionCursor<'a> { Some(value) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn projection_accepts_required_fields_unknown_metadata_and_escaped_keys() { + let projected = StatusProjection::parse( + r#"{"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"message":"re\u0061dy \ud83d\ude80","extra":{},"ready":false}}"#, + ); + assert!(projected.is_ok()); + let projected = projected.ok(); + assert_eq!(projected.as_ref().map(|value| value.ready), Some(false)); + assert_eq!( + projected.as_ref().map(|value| value.message.as_str()), + Some("ready 🚀") + ); + + let spaced = StatusProjection::parse( + "\n\t { \r\n \"result\" : { \"ready\" : true , \"message\" : \"ok\" } }", + ); + assert!(spaced.is_ok()); + } + + #[test] + fn projection_rejects_missing_invalid_duplicate_and_oversized_required_fields() { + let cases = [ + (r#"{"result":{"message":"x"}}"#.to_owned(), "missing ready"), + ( + r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), + "invalid ready", + ), + (r#"{"result":{"ready":true}}"#.to_owned(), "missing message"), + ( + r#"{"result":{"ready":true,"message":false}}"#.to_owned(), + "invalid message", + ), + ( + r#"{"result":{"ready":true,"ready":false,"message":"x"}}"#.to_owned(), + "duplicate ready", + ), + ( + r#"{"result":{"ready":true,"message":"x","message":"y"}}"#.to_owned(), + "duplicate message", + ), + ( + format!( + "{{\"result\":{{\"ready\":true,\"message\":\"{}\"}}}}", + "x".repeat(MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE + 1) + ), + "oversized message", + ), + ]; + + for (document, label) in cases { + assert!(StatusProjection::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#"{"x":1 ?}"#, + r#"{?}"#, + r#"{"result":[]}"#, + r#"{"result":{?}}"#, + r#"{"result":{"ready" true,"message":"x"}}"#, + r#"{"result":{"ready":true "message":"x"}}"#, + r#"{"result":{"ready":?,"message":"x"}}"#, + r#"{"result":{"ready":true,"message":"x","extra":?}}"#, + r#"{"result":{"ready":true,"message":"\uD800"}}"#, + r#"{"result":{"ready":true,"message":"\q"}}"#, + ]; + for document in malformed { + assert!(StatusProjection::parse(document).is_err()); + } + } + + #[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)); + let mut output = String::new(); + let mut escape = ProjectionCursor::new("u0061"); + assert!(escape.parse_escape(&mut output)); + assert_eq!(output, "a"); + let mut output = String::new(); + let mut escape = ProjectionCursor::new("uD83D\\uDE80"); + assert!(escape.parse_escape(&mut output)); + assert_eq!(output, "🚀"); + + for sequence in [ + "u", + "uZZZZ", + "uD800x", + "uD800\\x", + "uD800\\u", + "uD800\\u0041", + "uDC00", + ] { + let mut output = String::new(); + let mut escape = ProjectionCursor::new(sequence); + assert!(!escape.parse_escape(&mut output), "{sequence}"); + } + + let mut hex = ProjectionCursor::new("09aF"); + assert_eq!(hex.parse_hex_u16(), Some(0x09af)); + let mut hex = ProjectionCursor::new("0"); + assert!(hex.parse_hex_u16().is_none()); + let mut hex = ProjectionCursor::new("00G0"); + assert!(hex.parse_hex_u16().is_none()); + } + + #[test] + fn response_errors_have_stable_redacted_messages_and_sources() { + let envelope = WebDriverBiDiSessionStatusResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert!(envelope.source().is_some()); + assert_eq!( + envelope.to_string(), + "WebDriver BiDi session.status envelope is invalid" + ); + + let correlation = WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert!(correlation.source().is_some()); + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.status response correlation failed" + ); + + let leaf_errors = [ + WebDriverBiDiSessionStatusResponseError::MissingReady, + WebDriverBiDiSessionStatusResponseError::InvalidReady, + WebDriverBiDiSessionStatusResponseError::MissingMessage, + WebDriverBiDiSessionStatusResponseError::InvalidMessage, + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "ready" }, + WebDriverBiDiSessionStatusResponseError::MessageTooLarge { + maximum_bytes: MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, + }, + WebDriverBiDiSessionStatusResponseError::InvalidResultProjection, + WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + command_id: 7, + error_code: "unknown error".to_owned(), + }, + ]; + for error in leaf_errors { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } + } +} From b27b967e58031b0dc92a877aab697fc8aac69f1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 07:47:03 -0700 Subject: [PATCH 20/43] fix(network): cover validated error evidence fallback --- .../webdriver_bidi_session_status_response.rs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 896c36526..ef29f5947 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -68,13 +68,7 @@ impl WebDriverBiDiSessionStatusResult { }) } WebDriverBiDiJsonEnvelopeKind::Error => { - let error_code = envelope.error_code().map(str::to_owned).ok_or( - WebDriverBiDiSessionStatusResponseError::Envelope { - source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { - member: "error", - }, - }, - )?; + let error_code = retain_validated_error_code(envelope.error_code())?; let completed = correlation .correlate_response(&envelope) .map_err( @@ -210,6 +204,16 @@ impl Error for WebDriverBiDiSessionStatusResponseError { } } +fn retain_validated_error_code( + error_code: Option<&str>, +) -> Result { + error_code.map(str::to_owned).ok_or( + WebDriverBiDiSessionStatusResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, + }, + ) +} + struct StatusProjection { ready: bool, message: String, @@ -744,6 +748,17 @@ mod tests { "WebDriver BiDi session.status response correlation failed" ); + let retained_error_code = retain_validated_error_code(Some("unknown error")); + assert!(matches!(retained_error_code.as_deref(), Ok("unknown error"))); + + let missing_error_code = retain_validated_error_code(None); + assert!(matches!( + missing_error_code, + Err(WebDriverBiDiSessionStatusResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, + }) + )); + let leaf_errors = [ WebDriverBiDiSessionStatusResponseError::MissingReady, WebDriverBiDiSessionStatusResponseError::InvalidReady, From 74557990caa30f451f89b3e6b7d1f689533f64a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:11:46 -0700 Subject: [PATCH 21/43] fix(network): defer BiDi error correlation after validation --- .../webdriver_bidi_session_status_response.rs | 175 +++++++++++------- 1 file changed, 111 insertions(+), 64 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index ef29f5947..9a9ceacf4 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -68,18 +68,15 @@ impl WebDriverBiDiSessionStatusResult { }) } WebDriverBiDiJsonEnvelopeKind::Error => { - let error_code = retain_validated_error_code(envelope.error_code())?; - let completed = correlation - .correlate_response(&envelope) - .map_err( - |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, - )?; - Err( - WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + retain_validated_error_code(envelope.error_code()).and_then(|error_code| { + let completed = correlation.correlate_response(&envelope).map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; + Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), error_code, - }, - ) + }) + }) } WebDriverBiDiJsonEnvelopeKind::Event => { Err(WebDriverBiDiSessionStatusResponseError::Correlation { @@ -207,11 +204,11 @@ impl Error for WebDriverBiDiSessionStatusResponseError { fn retain_validated_error_code( error_code: Option<&str>, ) -> Result { - error_code.map(str::to_owned).ok_or( - WebDriverBiDiSessionStatusResponseError::Envelope { + error_code + .map(str::to_owned) + .ok_or(WebDriverBiDiSessionStatusResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, - }, - ) + }) } struct StatusProjection { @@ -481,26 +478,26 @@ impl<'a> ProjectionCursor<'a> { } } 0x00..=0x1f => return None, - _ if byte.is_ascii() => { - output.push(char::from(byte)); + 0x20..=0x7f => { self.index += 1; + output.push(char::from(byte)); } _ => { - let width = byte.leading_ones() as usize; - let end = self.index + width; - output.push_str(&self.input[self.index..end]); - self.index = end; + let tail = self.input.get(self.index..)?; + let character = tail.chars().next()?; + self.index += character.len_utf8(); + output.push(character); } } } } fn parse_escape(&mut self, output: &mut String) -> bool { - let Some(escape) = self.current_byte() else { + let Some(escaped) = self.current_byte() else { return false; }; self.index += 1; - match escape { + match escaped { b'"' => output.push('"'), b'\\' => output.push('\\'), b'/' => output.push('/'), @@ -529,93 +526,135 @@ impl<'a> ProjectionCursor<'a> { if !(0xdc00..=0xdfff).contains(&second) { return false; } - output.push_str(&String::from_utf16_lossy(&[first, second])); + let scalar = 0x1_0000 + + ((u32::from(first) - 0xd800) << 10) + + (u32::from(second) - 0xdc00); + let Some(character) = char::from_u32(scalar) else { + return false; + }; + output.push(character); true } else if (0xdc00..=0xdfff).contains(&first) { false } else { - output.push_str(&String::from_utf16_lossy(&[first])); + let Some(character) = char::from_u32(u32::from(first)) else { + return false; + }; + output.push(character); true } } fn parse_hex_u16(&mut self) -> Option { + let end = self.index.checked_add(4)?; + let digits = self.input.as_bytes().get(self.index..end)?; 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; + for byte in digits { + value = value.checked_mul(16)?; + value = value.checked_add(u16::from(hex_digit(*byte)?))?; } + self.index = end; Some(value) } } +fn hex_digit(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn projection_accepts_required_fields_unknown_metadata_and_escaped_keys() { - let projected = StatusProjection::parse( - r#"{"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"message":"re\u0061dy \ud83d\ude80","extra":{},"ready":false}}"#, - ); - assert!(projected.is_ok()); - let projected = projected.ok(); - assert_eq!(projected.as_ref().map(|value| value.ready), Some(false)); - assert_eq!( - projected.as_ref().map(|value| value.message.as_str()), - Some("ready 🚀") - ); - - let spaced = StatusProjection::parse( - "\n\t { \r\n \"result\" : { \"ready\" : true , \"message\" : \"ok\" } }", - ); - assert!(spaced.is_ok()); + fn status_projection_covers_required_and_extensible_members() { + let projection = StatusProjection::parse( + r#"{"type":"success","id":7,"ignored":[null,true,false,1,-2.5e+3,{"nested":"value"}],"result":{"unknown":{},"ready":false,"message":"a\n\u0042"}}"#, + ) + .expect("valid projection"); + assert!(!projection.ready); + assert_eq!(projection.message, "a\nB"); + + let projection = StatusProjection::parse( + r#" { "result" : { "ready" : true , "message" : "ok" } } "#, + ) + .expect("whitespace projection"); + assert!(projection.ready); + assert_eq!(projection.message, "ok"); } #[test] - fn projection_rejects_missing_invalid_duplicate_and_oversized_required_fields() { + fn status_projection_reports_specific_result_contract_failures() { let cases = [ - (r#"{"result":{"message":"x"}}"#.to_owned(), "missing ready"), + ( + r#"{"result":{}}"#.to_owned(), + WebDriverBiDiSessionStatusResponseError::MissingReady, + ), + ( + r#"{"result":{"message":"x"}}"#.to_owned(), + WebDriverBiDiSessionStatusResponseError::MissingReady, + ), ( r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), - "invalid ready", + WebDriverBiDiSessionStatusResponseError::InvalidReady, + ), + ( + r#"{"result":{"ready":true}}"#.to_owned(), + WebDriverBiDiSessionStatusResponseError::MissingMessage, ), - (r#"{"result":{"ready":true}}"#.to_owned(), "missing message"), ( r#"{"result":{"ready":true,"message":false}}"#.to_owned(), - "invalid message", + WebDriverBiDiSessionStatusResponseError::InvalidMessage, ), ( r#"{"result":{"ready":true,"ready":false,"message":"x"}}"#.to_owned(), - "duplicate ready", + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "ready" }, ), ( r#"{"result":{"ready":true,"message":"x","message":"y"}}"#.to_owned(), - "duplicate message", + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "message" }, ), ( format!( "{{\"result\":{{\"ready\":true,\"message\":\"{}\"}}}}", "x".repeat(MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE + 1) ), - "oversized message", + WebDriverBiDiSessionStatusResponseError::MessageTooLarge { + maximum_bytes: MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, + }, ), ]; - - for (document, label) in cases { - assert!(StatusProjection::parse(&document).is_err(), "{label}"); + for (document, expected) in cases { + let actual = StatusProjection::parse(&document).expect_err("invalid projection"); + match (actual, expected) { + ( + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { + member: actual, + }, + WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { + member: expected, + }, + ) => assert_eq!(actual, expected), + ( + WebDriverBiDiSessionStatusResponseError::MessageTooLarge { + maximum_bytes: actual, + }, + WebDriverBiDiSessionStatusResponseError::MessageTooLarge { + maximum_bytes: expected, + }, + ) => assert_eq!(actual, expected), + (actual, expected) => assert_eq!(actual.to_string(), expected.to_string()), + } } } #[test] - fn projection_cursor_rejects_malformed_private_inputs_without_panicking() { + fn status_projection_rejects_malformed_json_shapes() { let malformed = [ "", "[]", @@ -749,15 +788,23 @@ mod tests { ); let retained_error_code = retain_validated_error_code(Some("unknown error")); - assert!(matches!(retained_error_code.as_deref(), Ok("unknown error"))); + assert!(matches!( + retained_error_code.as_deref(), + Ok("unknown error") + )); - let missing_error_code = retain_validated_error_code(None); + let mut correlation_followup_ran = false; + let missing_error_code = retain_validated_error_code(None).and_then(|_| { + correlation_followup_ran = true; + Ok(String::new()) + }); assert!(matches!( missing_error_code, Err(WebDriverBiDiSessionStatusResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, }) )); + assert!(!correlation_followup_ran); let leaf_errors = [ WebDriverBiDiSessionStatusResponseError::MissingReady, From 367810c0a4656c86a1a3f893b9687ccdb8d8d664 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:17:00 -0700 Subject: [PATCH 22/43] fix(network): restore focused BiDi response repair boundary --- .../webdriver_bidi_session_status_response.rs | 175 +++++++----------- 1 file changed, 64 insertions(+), 111 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 9a9ceacf4..ef29f5947 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -68,15 +68,18 @@ impl WebDriverBiDiSessionStatusResult { }) } WebDriverBiDiJsonEnvelopeKind::Error => { - retain_validated_error_code(envelope.error_code()).and_then(|error_code| { - let completed = correlation.correlate_response(&envelope).map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; - Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + let error_code = retain_validated_error_code(envelope.error_code())?; + let completed = correlation + .correlate_response(&envelope) + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, + )?; + Err( + WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), error_code, - }) - }) + }, + ) } WebDriverBiDiJsonEnvelopeKind::Event => { Err(WebDriverBiDiSessionStatusResponseError::Correlation { @@ -204,11 +207,11 @@ impl Error for WebDriverBiDiSessionStatusResponseError { fn retain_validated_error_code( error_code: Option<&str>, ) -> Result { - error_code - .map(str::to_owned) - .ok_or(WebDriverBiDiSessionStatusResponseError::Envelope { + error_code.map(str::to_owned).ok_or( + WebDriverBiDiSessionStatusResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, - }) + }, + ) } struct StatusProjection { @@ -478,26 +481,26 @@ impl<'a> ProjectionCursor<'a> { } } 0x00..=0x1f => return None, - 0x20..=0x7f => { - self.index += 1; + _ if byte.is_ascii() => { output.push(char::from(byte)); + self.index += 1; } _ => { - let tail = self.input.get(self.index..)?; - let character = tail.chars().next()?; - self.index += character.len_utf8(); - output.push(character); + 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(escaped) = self.current_byte() else { + let Some(escape) = self.current_byte() else { return false; }; self.index += 1; - match escaped { + match escape { b'"' => output.push('"'), b'\\' => output.push('\\'), b'/' => output.push('/'), @@ -526,135 +529,93 @@ impl<'a> ProjectionCursor<'a> { if !(0xdc00..=0xdfff).contains(&second) { return false; } - let scalar = 0x1_0000 - + ((u32::from(first) - 0xd800) << 10) - + (u32::from(second) - 0xdc00); - let Some(character) = char::from_u32(scalar) else { - return false; - }; - output.push(character); + output.push_str(&String::from_utf16_lossy(&[first, second])); true } else if (0xdc00..=0xdfff).contains(&first) { false } else { - let Some(character) = char::from_u32(u32::from(first)) else { - return false; - }; - output.push(character); + output.push_str(&String::from_utf16_lossy(&[first])); true } } fn parse_hex_u16(&mut self) -> Option { - let end = self.index.checked_add(4)?; - let digits = self.input.as_bytes().get(self.index..end)?; let mut value = 0_u16; - for byte in digits { - value = value.checked_mul(16)?; - value = value.checked_add(u16::from(hex_digit(*byte)?))?; + 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; } - self.index = end; Some(value) } } -fn hex_digit(byte: u8) -> Option { - match byte { - b'0'..=b'9' => Some(byte - b'0'), - b'a'..=b'f' => Some(byte - b'a' + 10), - b'A'..=b'F' => Some(byte - b'A' + 10), - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn status_projection_covers_required_and_extensible_members() { - let projection = StatusProjection::parse( - r#"{"type":"success","id":7,"ignored":[null,true,false,1,-2.5e+3,{"nested":"value"}],"result":{"unknown":{},"ready":false,"message":"a\n\u0042"}}"#, - ) - .expect("valid projection"); - assert!(!projection.ready); - assert_eq!(projection.message, "a\nB"); - - let projection = StatusProjection::parse( - r#" { "result" : { "ready" : true , "message" : "ok" } } "#, - ) - .expect("whitespace projection"); - assert!(projection.ready); - assert_eq!(projection.message, "ok"); + fn projection_accepts_required_fields_unknown_metadata_and_escaped_keys() { + let projected = StatusProjection::parse( + r#"{"meta":[null,true,false,1,-2.5e+3,{"nested":"value"}],"re\u0073ult":{"message":"re\u0061dy \ud83d\ude80","extra":{},"ready":false}}"#, + ); + assert!(projected.is_ok()); + let projected = projected.ok(); + assert_eq!(projected.as_ref().map(|value| value.ready), Some(false)); + assert_eq!( + projected.as_ref().map(|value| value.message.as_str()), + Some("ready 🚀") + ); + + let spaced = StatusProjection::parse( + "\n\t { \r\n \"result\" : { \"ready\" : true , \"message\" : \"ok\" } }", + ); + assert!(spaced.is_ok()); } #[test] - fn status_projection_reports_specific_result_contract_failures() { + fn projection_rejects_missing_invalid_duplicate_and_oversized_required_fields() { let cases = [ - ( - r#"{"result":{}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::MissingReady, - ), - ( - r#"{"result":{"message":"x"}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::MissingReady, - ), + (r#"{"result":{"message":"x"}}"#.to_owned(), "missing ready"), ( r#"{"result":{"ready":0,"message":"x"}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::InvalidReady, - ), - ( - r#"{"result":{"ready":true}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::MissingMessage, + "invalid ready", ), + (r#"{"result":{"ready":true}}"#.to_owned(), "missing message"), ( r#"{"result":{"ready":true,"message":false}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::InvalidMessage, + "invalid message", ), ( r#"{"result":{"ready":true,"ready":false,"message":"x"}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "ready" }, + "duplicate ready", ), ( r#"{"result":{"ready":true,"message":"x","message":"y"}}"#.to_owned(), - WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { member: "message" }, + "duplicate message", ), ( format!( "{{\"result\":{{\"ready\":true,\"message\":\"{}\"}}}}", "x".repeat(MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE + 1) ), - WebDriverBiDiSessionStatusResponseError::MessageTooLarge { - maximum_bytes: MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, - }, + "oversized message", ), ]; - for (document, expected) in cases { - let actual = StatusProjection::parse(&document).expect_err("invalid projection"); - match (actual, expected) { - ( - WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { - member: actual, - }, - WebDriverBiDiSessionStatusResponseError::DuplicateResultMember { - member: expected, - }, - ) => assert_eq!(actual, expected), - ( - WebDriverBiDiSessionStatusResponseError::MessageTooLarge { - maximum_bytes: actual, - }, - WebDriverBiDiSessionStatusResponseError::MessageTooLarge { - maximum_bytes: expected, - }, - ) => assert_eq!(actual, expected), - (actual, expected) => assert_eq!(actual.to_string(), expected.to_string()), - } + + for (document, label) in cases { + assert!(StatusProjection::parse(&document).is_err(), "{label}"); } } #[test] - fn status_projection_rejects_malformed_json_shapes() { + fn projection_cursor_rejects_malformed_private_inputs_without_panicking() { let malformed = [ "", "[]", @@ -788,23 +749,15 @@ mod tests { ); let retained_error_code = retain_validated_error_code(Some("unknown error")); - assert!(matches!( - retained_error_code.as_deref(), - Ok("unknown error") - )); + assert!(matches!(retained_error_code.as_deref(), Ok("unknown error"))); - let mut correlation_followup_ran = false; - let missing_error_code = retain_validated_error_code(None).and_then(|_| { - correlation_followup_ran = true; - Ok(String::new()) - }); + let missing_error_code = retain_validated_error_code(None); assert!(matches!( missing_error_code, Err(WebDriverBiDiSessionStatusResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, }) )); - assert!(!correlation_followup_ran); let leaf_errors = [ WebDriverBiDiSessionStatusResponseError::MissingReady, From e24e72318a893bfeae7b291d01f55e4398f8ebc2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:19:58 -0700 Subject: [PATCH 23/43] fix(network): isolate validated BiDi error correlation path --- .../webdriver_bidi_session_status_response.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index ef29f5947..8b0a1b3a9 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -68,18 +68,17 @@ impl WebDriverBiDiSessionStatusResult { }) } WebDriverBiDiJsonEnvelopeKind::Error => { - let error_code = retain_validated_error_code(envelope.error_code())?; - let completed = correlation - .correlate_response(&envelope) - .map_err( - |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, - )?; - Err( - WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + retain_validated_error_code(envelope.error_code()).and_then(|error_code| { + let completed = correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; + Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), error_code, - }, - ) + }) + }) } WebDriverBiDiJsonEnvelopeKind::Event => { Err(WebDriverBiDiSessionStatusResponseError::Correlation { From 8dd0e2cf6d3860c99aac4d0919b86c7adf30c567 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 08:31:31 -0700 Subject: [PATCH 24/43] style(network): apply canonical rustfmt diagnostics --- .../webdriver_bidi_session_status_response.rs | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 8b0a1b3a9..43a4d5567 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -69,15 +69,18 @@ impl WebDriverBiDiSessionStatusResult { } WebDriverBiDiJsonEnvelopeKind::Error => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { - let completed = correlation - .correlate_response(&envelope) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; - Err(WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { - command_id: completed.command_id(), - error_code, - }) + let completed = + correlation + .correlate_response(&envelope) + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; + Err( + WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { + command_id: completed.command_id(), + error_code, + }, + ) }) } WebDriverBiDiJsonEnvelopeKind::Event => { @@ -206,11 +209,11 @@ impl Error for WebDriverBiDiSessionStatusResponseError { fn retain_validated_error_code( error_code: Option<&str>, ) -> Result { - error_code.map(str::to_owned).ok_or( - WebDriverBiDiSessionStatusResponseError::Envelope { + error_code + .map(str::to_owned) + .ok_or(WebDriverBiDiSessionStatusResponseError::Envelope { source: WebDriverBiDiJsonEnvelopeError::MissingRequiredMember { member: "error" }, - }, - ) + }) } struct StatusProjection { @@ -748,7 +751,10 @@ mod tests { ); let retained_error_code = retain_validated_error_code(Some("unknown error")); - assert!(matches!(retained_error_code.as_deref(), Ok("unknown error"))); + assert!(matches!( + retained_error_code.as_deref(), + Ok("unknown error") + )); let missing_error_code = retain_validated_error_code(None); assert!(matches!( From 4e824abf100294fd9a7bbc7abc83152f72b80b5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:55:25 +0900 Subject: [PATCH 25/43] test(network): expose response correlation kind --- crates/originweave-network/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index a4a8f883e..7e81dc097 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -36,8 +36,8 @@ pub use connection::{ }; pub use webdriver_bidi_command_correlation::{ MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS, WebDriverBiDiCommandCorrelation, - WebDriverBiDiCommandCorrelationError, WebDriverBiDiCorrelatedResponse, - WebDriverBiDiCorrelatedResponseOutcome, + WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, + WebDriverBiDiCorrelatedResponse, WebDriverBiDiCorrelatedResponseOutcome, }; pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, From 9394b9afd81670318fecf4a3f0cafa7cb62af7ff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 03:58:04 +0900 Subject: [PATCH 26/43] fix(network): bind status response correlation kind --- .../webdriver_bidi_session_status_response.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 43a4d5567..e08a7efbb 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -2,8 +2,8 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiWebSocketTextMessage, }; /// Maximum decoded byte length retained from WebDriver BiDi `session.status` implementation text. @@ -57,7 +57,7 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiJsonEnvelopeKind::Success => { let projected = StatusProjection::parse(message.as_str())?; let completed = correlation - .correlate_response(&envelope) + .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) .map_err( |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, )?; @@ -69,12 +69,11 @@ impl WebDriverBiDiSessionStatusResult { } WebDriverBiDiJsonEnvelopeKind::Error => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { - let completed = - correlation - .correlate_response(&envelope) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; + let completed = correlation + .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), From 0911385dec6afc2c677eb73c6ca1520c6d5701af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:09:32 +0900 Subject: [PATCH 27/43] fix(network): align public status correlation test --- ...ver_bidi_json_envelope_public_boundary_tests.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 6b6487cb0..879fb32bd 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,12 +9,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiSessionStatusResponseError, - WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, - WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, + WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -134,7 +134,7 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box> { let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command(7)?; + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); assert!(matches!( From 1262df0dda6d2d3be087479bbc8dfa133808c40e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 15:17:30 +0900 Subject: [PATCH 28/43] style(network): apply exact session status rustfmt --- .../src/webdriver_bidi_session_status_response.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index e08a7efbb..b1e5596ba 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -71,9 +71,11 @@ impl WebDriverBiDiSessionStatusResult { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + }, + )?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), From 7e85a7e5f0147f4b712129cd19aaa3d0a0a54634 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:05:35 +0900 Subject: [PATCH 29/43] test: reject session.status reply from replacement connection --- ...n_status_response_connection_provenance.rs | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs new file mode 100644 index 000000000..a089710a7 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs @@ -0,0 +1,161 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, 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 STATUS_RESPONSE: &[u8] = + br#"{"type":"success","id":7,"result":{"ready":true,"message":"capacity available"}}"#; + +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, + "session.status fixture 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) +} + +fn establish(local_addr: std::net::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 receive_foreign_status_response( + listener: TcpListener, +) -> Result> { + 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)?; + stream.write_all(&[0x81, STATUS_RESPONSE.len() as u8])?; + stream.write_all(STATUS_RESPONSE) + }); + + let established = establish(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!( + "replacement connection produced unexpected assembly state: {other:?}" + )) + .into()); + } + }; + server + .join() + .map_err(|_| io::Error::other("replacement-connection server panicked"))??; + Ok(text) +} + +#[test] +fn session_status_response_from_same_session_replacement_connection_cannot_consume_original_pending_command( +) -> Result<(), Box> { + let original_listener = TcpListener::bind(("127.0.0.1", 0))?; + let original_addr = original_listener.local_addr()?; + 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 != br#"{"id":7,"method":"session.status","params":{}}"# { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "unexpected session.status command on original connection", + )); + } + Ok(()) + }); + + let original = establish(original_addr)?; + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + let _original = WebDriverBiDiSessionStatusCommand::new(7)?.send( + original, + &mut correlation, + WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]), + Duration::from_millis(500), + )?; + original_server + .join() + .map_err(|_| io::Error::other("original-connection server panicked"))??; + assert_eq!(correlation.outstanding_count(), 1); + + let replacement_listener = TcpListener::bind(("127.0.0.1", 0))?; + let replacement_response = receive_foreign_status_response(replacement_listener)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate( + &replacement_response, + &mut correlation, + ); + + assert!( + parsed.is_err(), + "same-session replacement connection unexpectedly consumed the original session.status command" + ); + assert_eq!( + correlation.outstanding_count(), + 1, + "foreign-connection rejection must leave the original command pending" + ); + Ok(()) +} From 6b102c1d860629d1b3e1a49cb0c33d94ca825adb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:13:33 +0900 Subject: [PATCH 30/43] test: apply canonical status-provenance formatting --- ..._bidi_session_status_response_connection_provenance.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs index a089710a7..8ad11aefa 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs @@ -64,7 +64,9 @@ fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result> { Ok(payload) } -fn establish(local_addr: std::net::SocketAddr) -> Result> { +fn establish( + local_addr: std::net::SocketAddr, +) -> Result> { let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? .correlate_session_id(SESSION_ID)? @@ -110,8 +112,8 @@ fn receive_foreign_status_response( } #[test] -fn session_status_response_from_same_session_replacement_connection_cannot_consume_original_pending_command( -) -> Result<(), Box> { +fn session_status_response_from_same_session_replacement_connection_cannot_consume_original_pending_command() +-> Result<(), Box> { let original_listener = TcpListener::bind(("127.0.0.1", 0))?; let original_addr = original_listener.local_addr()?; let original_server = thread::spawn(move || -> io::Result<()> { From 5aa7d1a6c7ad63b62186cc7ca882cf17e215c613 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:12:30 +0900 Subject: [PATCH 31/43] merge: adopt session.status sender provenance --- .../webdriver_bidi_session_status_command.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs index e6341c416..092d06ec1 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_command.rs @@ -42,12 +42,14 @@ impl WebDriverBiDiSessionStatusCommand { /// Register and write this exact command on an already established verified BiDi stream. /// /// Locally invalid frame deadlines fail before correlation registration and before any remote - /// side effect. Correlation then registers the command before the first possible frame write. - /// A frame-owner preflight rejection that proves no write began retires this exact command - /// again; currently that covers adjacent client masking-key reuse. Once frame emission can have - /// begun, a later failure leaves the identifier outstanding because partial or full emission is - /// ambiguous. Callers must treat that failed stream/correlation pairing as unusable or - /// explicitly tear down its session state. + /// side effect. Correlation then registers the command together with the established + /// connection generation before the first possible frame write, so a response received on a + /// same-session replacement connection cannot consume this pending command. A frame-owner + /// preflight rejection that proves no write began retires this exact command again; currently + /// that covers adjacent client masking-key reuse. Once frame emission can have begun, a later + /// failure leaves the identifier outstanding because partial or full emission is ambiguous. + /// Callers must treat that failed stream/correlation pairing as unusable or explicitly tear down + /// its session state. pub fn send( self, established: WebDriverBiDiWebSocketEstablished, @@ -64,7 +66,11 @@ impl WebDriverBiDiSessionStatusCommand { }); } correlation - .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionStatus) + .register_command_for_connection( + self.command_id, + WebDriverBiDiCommandKind::SessionStatus, + established.transport_evidence().connection_generation(), + ) .map_err(|source| WebDriverBiDiSessionStatusCommandError::Correlation { source })?; let message = self.serialized(); match established.write_text_frame(&message, masking_key, frame_timeout) { From 9307eca134d647d9330f5455058b1888ab40fd06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:13:57 +0900 Subject: [PATCH 32/43] fix: bind session.status responses to connection --- .../webdriver_bidi_session_status_response.rs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index b1e5596ba..285bb6d6f 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -3,7 +3,7 @@ use std::{error::Error, fmt}; use crate::{ WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, - WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiReceivedTextMessage, }; /// Maximum decoded byte length retained from WebDriver BiDi `session.status` implementation text. @@ -37,27 +37,33 @@ impl fmt::Debug for WebDriverBiDiSessionStatusResult { } impl WebDriverBiDiSessionStatusResult { - /// Parse one bounded local-end message and consume its exact outstanding command on success. + /// Parse one bounded received 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 `result.ready` and `result.message`; correlation is consumed /// only after that result is valid, so malformed success bodies cannot silently retire an id. /// A correlatable protocol-error response consumes its matching id and returns a typed remote /// protocol failure retaining the protocol error code but not the implementation-defined remote - /// message or stacktrace. Events, null-id errors, and unknown ids fail closed through the - /// existing correlation boundary. + /// message or stacktrace. Both success and error responses must have arrived on the exact + /// connection generation that registered the command; replacement-connection responses leave + /// the original pending command untouched. Events, null-id errors, and unknown ids fail closed + /// through the existing correlation boundary. pub fn parse_and_correlate( - message: &WebDriverBiDiWebSocketTextMessage, + message: &WebDriverBiDiReceivedTextMessage, correlation: &mut WebDriverBiDiCommandCorrelation, ) -> Result { - let envelope = WebDriverBiDiJsonEnvelope::parse(message) + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()) .map_err(|source| WebDriverBiDiSessionStatusResponseError::Envelope { source })?; match envelope.kind() { WebDriverBiDiJsonEnvelopeKind::Success => { - let projected = StatusProjection::parse(message.as_str())?; + let projected = StatusProjection::parse(message.message().as_str())?; let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionStatus, + message.connection_generation(), + ) .map_err( |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, )?; @@ -70,7 +76,11 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiJsonEnvelopeKind::Error => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { let completed = correlation - .correlate_response_for(&envelope, WebDriverBiDiCommandKind::SessionStatus) + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionStatus, + message.connection_generation(), + ) .map_err( |source| WebDriverBiDiSessionStatusResponseError::Correlation { source, From 0b68198e27bab23f001053d3ff605ace9d6d595a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:14:26 +0900 Subject: [PATCH 33/43] test: carry status response connection evidence --- .../webdriver_bidi_session_status_response.rs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 347dc5fec..45ee995b6 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -8,12 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, - WebDriverBiDiWebSocketMessageAssembler, WebDriverBiDiWebSocketMessageAssembly, - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -72,7 +72,7 @@ fn send_status_and_read_response( response: &'static [u8], ) -> Result< ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ), Box, @@ -113,13 +113,13 @@ fn send_status_and_read_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, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "session.status response produced unexpected assembly state: {other:?}" + "session.status response produced unexpected message state: {other:?}" )) .into()); } @@ -128,14 +128,14 @@ fn send_status_and_read_response( server .join() .map_err(|_| io::Error::other("session.status response test server panicked"))??; - Ok((text, correlation)) + Ok((message, correlation)) } #[test] fn session_status_success_result_is_typed_correlated_and_message_redacted_in_debug() -> Result<(), Box> { - let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE)?; - let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation)?; + let (message, mut correlation) = send_status_and_read_response(STATUS_RESPONSE)?; + let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation)?; assert_eq!(result.command_id(), 7); assert!(result.ready()); @@ -151,8 +151,8 @@ fn session_status_success_result_is_typed_correlated_and_message_redacted_in_deb #[test] fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result<(), Box> { - let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_MISSING_READY)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let (message, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_MISSING_READY)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); assert!(matches!( parsed, @@ -165,8 +165,8 @@ fn malformed_status_result_does_not_consume_the_outstanding_command() -> Result< #[test] fn empty_status_result_fails_before_consuming_the_outstanding_command() -> Result<(), Box> { - let (text, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_EMPTY_RESULT)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let (message, mut correlation) = send_status_and_read_response(STATUS_RESPONSE_EMPTY_RESULT)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); assert!(matches!( parsed, From cafa95001cbaa918dce66d3706a955088b392ed9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:15:09 +0900 Subject: [PATCH 34/43] test: retain status connection provenance in hostile cases --- ...er_bidi_session_status_response_hostile.rs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs index e588fa6e5..2e5d69e42 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_hostile.rs @@ -9,11 +9,11 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, WebDriverBiDiCommandCorrelation, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -21,7 +21,7 @@ const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n"; type StatusRead = ( - WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiCommandCorrelation, ); @@ -132,13 +132,13 @@ fn send_status_and_read_response(response: Vec) -> Result text, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "session.status response produced unexpected assembly state: {other:?}" + "session.status response produced unexpected message state: {other:?}" )) .into()); } @@ -147,7 +147,7 @@ fn send_status_and_read_response(response: Vec) -> Result, > { - let (text, mut correlation) = send_status_and_read_response(response)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let (message, mut correlation) = send_status_and_read_response(response)?; + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); Ok((parsed, correlation)) } From 05cbc5c5a6dd777c8a59199450352318360f72ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:15:35 +0900 Subject: [PATCH 35/43] test: require exact status response connection provenance --- ...n_status_response_connection_provenance.rs | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs index 8ad11aefa..0be86d79a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response_connection_provenance.rs @@ -8,11 +8,12 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use originweave_network::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionStatusCommand, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, - WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -83,7 +84,7 @@ fn establish( fn receive_foreign_status_response( listener: TcpListener, -) -> Result> { +) -> Result> { let local_addr = listener.local_addr()?; let server = thread::spawn(move || -> io::Result<()> { let (mut stream, _) = listener.accept()?; @@ -94,13 +95,13 @@ fn receive_foreign_status_response( }); let established = establish(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, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "replacement connection produced unexpected assembly state: {other:?}" + "replacement connection produced unexpected message state: {other:?}" )) .into()); } @@ -108,7 +109,7 @@ fn receive_foreign_status_response( server .join() .map_err(|_| io::Error::other("replacement-connection server panicked"))??; - Ok(text) + Ok(message) } #[test] @@ -150,10 +151,14 @@ fn session_status_response_from_same_session_replacement_connection_cannot_consu &mut correlation, ); - assert!( - parsed.is_err(), - "same-session replacement connection unexpectedly consumed the original session.status command" - ); + assert!(matches!( + parsed, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7, + }, + }) + )); assert_eq!( correlation.outstanding_count(), 1, From 0e1e47c0360650f1ee5e761e321209bcbdee6e86 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:18:01 +0900 Subject: [PATCH 36/43] test: exercise received status boundary --- ...idi_json_envelope_public_boundary_tests.rs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 1e97bff2f..0c6baf39d 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,12 +9,13 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiJsonEnvelope, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, - WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, - WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, - WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageAssembler, - WebDriverBiDiWebSocketMessageAssembly, WebDriverBiDiWebSocketTextMessage, + WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusResponseError, + WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, + WebDriverBiDiWebSocketMessageReader, }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; @@ -43,7 +44,7 @@ fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { fn read_text_over_loopback( document: &'static [u8], -) -> Result> { +) -> Result> { if document.len() > 125 { return Err(io::Error::other("unit JSON document exceeded one-byte frame length").into()); } @@ -68,14 +69,14 @@ fn read_text_over_loopback( 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, + let message = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, other => { return Err(io::Error::other(format!( - "validated text frame produced unexpected assembly state: {other:?}" + "validated text frame produced unexpected message state: {other:?}" )) .into()); } @@ -84,14 +85,14 @@ fn read_text_over_loopback( server .join() .map_err(|_| io::Error::other("JSON-envelope unit server panicked"))??; - Ok(text) + Ok(message) } fn parse_over_loopback( document: &'static [u8], ) -> Result, Box> { - let text = read_text_over_loopback(document)?; - Ok(WebDriverBiDiJsonEnvelope::parse(&text)) + let message = read_text_over_loopback(document)?; + Ok(WebDriverBiDiJsonEnvelope::parse(message.message())) } #[test] @@ -150,11 +151,11 @@ fn public_json_envelope_unit_build_covers_fail_closed_json_edges() -> Result<(), #[test] fn public_session_status_empty_result_fails_closed_from_unit_build() -> Result<(), Box> { - let text = read_text_over_loopback(EMPTY_STATUS_RESULT)?; + let message = read_text_over_loopback(EMPTY_STATUS_RESULT)?; let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; - let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&text, &mut correlation); + let parsed = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); assert!(matches!( parsed, Err(WebDriverBiDiSessionStatusResponseError::MissingReady) From aadba11b94ac6bc426702b3caf63f893e28368cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 06:23:17 +0900 Subject: [PATCH 37/43] docs: record session.status connection provenance --- docs/doctoring.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index bb9166e20..b72c975ec 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -122,6 +122,8 @@ The test-only `serve_opening_exchange` helper reuses the bounded request reader, The #250 integration preserves all five child-owned production and Rust-test blobs from `0eab23d5e388c5c8b984c0021a58316680c9ba8b` and ordinarily adopts #249 `84b9407978ae0f6c115f01170b6069c601b21104`. The pre-integration branch lacked that parent, and native unittest discovery collected zero correlation release checks instead of one. Parent adoption brings the canonical synchronized opening-exchange fixture and the discoverable release TestCase into this response stack without copying either implementation. Command-specific projection still validates the common envelope, required readiness/message fields and bounded status text before consuming the exact typed outstanding correlation; status text is untrusted and is omitted from Debug output. This does not add a connection-bound received-message capability, browser policy authority, runtime process/profile teardown proof or real Chromium acceptance. The connection-provenance repair remains separately owned by its later stack. Local integrated tests and exact-head hosted gates must be evaluated independently; predecessor results do not transfer. +On 6 September 2026, that historical no-connection-provenance state was superseded for the current #250 branch by an executed real-socket regression at `6b102c1d860629d1b3e1a49cb0c33d94ca825adb`: CI `34044758402` / Rust job `101517723552` showed that a same-session replacement WebSocket could consume the original pending `session.status` command. Parent #249 `e77150f4de6534887098fb9de7e02ecea7fbb59c` now registers `SessionStatus` with the established connection generation before frame I/O. #250 adopts that parent by an ordinary two-parent non-force merge and its command-specific response parser accepts only `WebDriverBiDiReceivedTextMessage`, correlating both success and protocol-error envelopes against the received connection generation. The regression requires typed `ResponseConnectionMismatch { command_id: 7 }` and preserves the original outstanding command after rejection. Malformed success projection and invalid protocol-error shape still fail before correlation consumption. This is transport/correlation provenance only: it does not grant browser policy authority, prove Chromium post-conditions or runtime process/profile teardown, or establish protected-main/release acceptance; exact-current hosted gates remain independently required. + ## References Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html @@ -204,4 +206,4 @@ World Wide Web Consortium. (2026, August 5). *Accessible name and description co Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From 615d8408cec07c9d863ba62aa379f02b199e7536 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 07:19:30 +0900 Subject: [PATCH 38/43] fix: format session.status connection provenance boundary --- ...ver_bidi_json_envelope_public_boundary_tests.rs | 5 ++--- .../src/webdriver_bidi_session_status_response.rs | 14 ++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs index 0c6baf39d..94e6fd3de 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope_public_boundary_tests.rs @@ -9,9 +9,8 @@ use std::{ use originweave_core::WebDriverBiDiWebSocketEndpoint; use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, - WebDriverBiDiConnectionMessageRead, WebDriverBiDiJsonEnvelope, - WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead, + WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind, WebDriverBiDiReceivedTextMessage, WebDriverBiDiSessionStatusResponseError, WebDriverBiDiSessionStatusResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan, diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index 285bb6d6f..a1037b9cb 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -64,9 +64,9 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiCommandKind::SessionStatus, message.connection_generation(), ) - .map_err( - |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, - )?; + .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { + source, + })?; Ok(Self { command_id: completed.command_id(), ready: projected.ready, @@ -81,11 +81,9 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiCommandKind::SessionStatus, message.connection_generation(), ) - .map_err( - |source| WebDriverBiDiSessionStatusResponseError::Correlation { - source, - }, - )?; + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), From 74536b276b5e55bdd1f788aef5d095b4b1a1ed1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:24:40 +0900 Subject: [PATCH 39/43] test(network): cover sealed reader and correlation rejection paths Reuse the existing sealed-reader integration checks and preserve pending commands after event or missing-provenance rejection. Apply canonical formatting observed in exact-head CI. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_json_envelope.rs | 49 +++++++ .../webdriver_bidi_session_status_response.rs | 25 ++-- .../tests/webdriver_bidi_received_message.rs | 137 ++++++++++++++++++ 3 files changed, 199 insertions(+), 12 deletions(-) create mode 100644 crates/originweave-network/tests/webdriver_bidi_received_message.rs diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 1d92d0ed8..276209cb0 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -702,6 +702,55 @@ mod tests { WebDriverBiDiJsonEnvelope::parse_str(value) } + #[test] + fn connection_correlation_rejections_preserve_pending_command() -> Result<(), Box> { + use std::{net::TcpListener, time::Duration}; + + use originweave_core::WebDriverBiDiWebSocketEndpoint; + + use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, + }; + + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let session = "01234567-89ab-cdef-0123-456789abcdef"; + let endpoint = format!("ws://{}/session/{session}", listener.local_addr()?); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(session)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let (_server, _) = listener.accept()?; + let generation = connection.connection_generation(); + let mut correlation = WebDriverBiDiCommandCorrelation::new(); + correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; + + for (document, expected) in [ + ( + r#"{"type":"event","method":"log.entryAdded","params":{}}"#, + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ( + r#"{"type":"success","id":7,"result":{}}"#, + WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + ), + ] { + assert_eq!( + correlation.correlate_response_for_connection( + &parse(document)?, + WebDriverBiDiCommandKind::SessionStatus, + generation, + ), + Err(expected) + ); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) + } + #[test] fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( diff --git a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs index a1037b9cb..d6a8f8605 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_status_response.rs @@ -64,9 +64,9 @@ impl WebDriverBiDiSessionStatusResult { WebDriverBiDiCommandKind::SessionStatus, message.connection_generation(), ) - .map_err(|source| WebDriverBiDiSessionStatusResponseError::Correlation { - source, - })?; + .map_err( + |source| WebDriverBiDiSessionStatusResponseError::Correlation { source }, + )?; Ok(Self { command_id: completed.command_id(), ready: projected.ready, @@ -75,15 +75,16 @@ impl WebDriverBiDiSessionStatusResult { } WebDriverBiDiJsonEnvelopeKind::Error => { retain_validated_error_code(envelope.error_code()).and_then(|error_code| { - let completed = correlation - .correlate_response_for_connection( - &envelope, - WebDriverBiDiCommandKind::SessionStatus, - message.connection_generation(), - ) - .map_err(|source| { - WebDriverBiDiSessionStatusResponseError::Correlation { source } - })?; + let completed = + correlation + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionStatus, + message.connection_generation(), + ) + .map_err(|source| { + WebDriverBiDiSessionStatusResponseError::Correlation { source } + })?; Err( WebDriverBiDiSessionStatusResponseError::RemoteProtocolError { command_id: completed.command_id(), diff --git a/crates/originweave-network/tests/webdriver_bidi_received_message.rs b/crates/originweave-network/tests/webdriver_bidi_received_message.rs new file mode 100644 index 000000000..edf752c06 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_received_message.rs @@ -0,0 +1,137 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiConnectionMessageRead, WebDriverBiDiConnectionMessageReadError, + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMessageReader, +}; + +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 established_for_frames( + frames: Vec>, +) -> Result> { + let listener = TcpListener::bind(("127.0.0.1", 0))?; + let local_addr = listener.local_addr()?; + thread::spawn(move || -> io::Result<()> { + let (mut stream, _) = listener.accept()?; + read_opening_request(&mut stream)?; + stream.write_all(OPENING_RESPONSE)?; + for frame in frames { + stream.write_all(&frame)?; + } + Ok(()) + }); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? + .correlate_session_id(SESSION_ID)? + .into_explicit_connect_target()?; + let connection = + WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?; + Ok(WebDriverBiDiWebSocketHandshakePlan::new(connection, key)? + .write_opening_request(Duration::from_millis(500))? + .read_opening_response(Duration::from_millis(500))?) +} + +#[test] +fn fragmented_text_and_interleaved_control_remain_on_one_reader() -> Result<(), Box> { + let established = established_for_frames(vec![ + vec![0x01, 0x03, b'a', b'b', b'c'], + vec![0x89, 0x00], + vec![0x80, 0x03, b'd', b'e', b'f'], + ])?; + let reader = WebDriverBiDiWebSocketMessageReader::new(established); + assert!(format!("{reader:?}").contains("connection_bound")); + + let first = reader.read_next(Duration::from_millis(500))?; + assert!(format!("{first:?}").starts_with("Pending")); + let reader = match first { + WebDriverBiDiConnectionMessageRead::Pending(reader) => reader, + _ => return Err(io::Error::other("first fragment did not remain pending").into()), + }; + + let control = reader.read_next(Duration::from_millis(500))?; + assert!(format!("{control:?}").starts_with("Control")); + let reader = match control { + WebDriverBiDiConnectionMessageRead::Control { reader, message } => { + assert_eq!(message.payload(), b""); + reader + } + _ => return Err(io::Error::other("interleaved Ping was not surfaced").into()), + }; + + let completed = reader.read_next(Duration::from_millis(500))?; + let debug = format!("{completed:?}"); + assert!(debug.starts_with("Text")); + assert!(debug.contains("payload_bytes")); + match completed { + WebDriverBiDiConnectionMessageRead::Text { + established, + message: _, + } => drop(established), + _ => return Err(io::Error::other("continuation did not complete text message").into()), + } + Ok(()) +} + +#[test] +fn frame_and_message_failures_remain_typed_and_sourced() -> Result<(), Box> { + let malformed = established_for_frames(vec![vec![0x81, 0x80]])?; + let frame_error = WebDriverBiDiWebSocketMessageReader::new(malformed) + .read_next(Duration::from_millis(500)) + .err() + .ok_or_else(|| io::Error::other("masked server frame was accepted"))?; + assert!(matches!( + frame_error, + WebDriverBiDiConnectionMessageReadError::Frame { .. } + )); + assert_eq!( + frame_error.to_string(), + "connection-bound WebDriver BiDi WebSocket frame read failed" + ); + assert!(frame_error.source().is_some()); + + let binary = established_for_frames(vec![vec![0x82, 0x00]])?; + let message_error = WebDriverBiDiWebSocketMessageReader::new(binary) + .read_next(Duration::from_millis(500)) + .err() + .ok_or_else(|| io::Error::other("binary BiDi message was accepted"))?; + assert!(matches!( + message_error, + WebDriverBiDiConnectionMessageReadError::Message { .. } + )); + assert_eq!( + message_error.to_string(), + "connection-bound WebDriver BiDi WebSocket message assembly failed" + ); + assert!(message_error.source().is_some()); + Ok(()) +} From 9bdd1169a77b259b9f6ab2f48d033f2a328e9ae3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:28:01 +0900 Subject: [PATCH 40/43] test(network): use consumed transport evidence in rejection fixture Preserve the failed compile attempt and use the existing evidence accessor without widening production authority. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- CHANGELOG.md | 1 + .../originweave-network/src/webdriver_bidi_json_envelope.rs | 3 ++- docs/TEST_STRATEGY.md | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70ed49817..c5b28b0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Regression checks now exercise fragmented browser replies, interleaved control messages, and rejected replies without losing a pending request. These checks do not establish browser readiness or release acceptance. - The typed browser-status response stack now includes its verified command and opening-exchange prerequisites, including the release-record check that previously did not execute; parsing remains bounded and does not grant browser authority or prove operational readiness. - Bounded RFC 6455 WebDriver BiDi opening-response validation on the exact peer-verified stream: it admits only HTTP/1.1 `101`, case-insensitive `Upgrade`/`Connection` tokens, and the client-key-correlated `Sec-WebSocket-Accept` value within monotonic time and header-size ceilings; it restores blocking mode and still does not implement WebSocket frames or grant browser/Agent authority. - Typed outbound WebDriver BiDi `session.status` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, preserves exact typed command-id correlation, rejects invalid frame deadlines before registration, retires only the just-registered id when a local masking-key preflight proves no command bytes were emitted, and keeps correlation outstanding after partial or ambiguous writes; frame-write success is not treated as command completion or browser/Agent authority. diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index 276209cb0..ecc7eb36b 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -722,7 +722,8 @@ mod tests { let connection = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; let (_server, _) = listener.accept()?; - let generation = connection.connection_generation(); + let (_stream, evidence) = connection.into_parts(); + let generation = evidence.connection_generation(); let mut correlation = WebDriverBiDiCommandCorrelation::new(); correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index ba3c31624..9a9856db6 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -340,6 +340,10 @@ Do not retry deterministic failures blindly. If multiple distinct fixes fail, re Documentation contracts intentionally validate only durable properties such as required files, links, status vocabularies and authority assertions. Do not create brittle tests that freeze wording without preventing a real documentation defect. +### Active browser-status response checks + +The active response stack exercises fragmented text with an interleaved Ping, malformed frame and message errors, and payload-redacted diagnostics. Separate correlation checks reject an event or a reply to a request lacking connection provenance while preserving the pending request. The fixture obtains its connection identity from a real loopback connection through the existing consuming transport handoff; it does not add a caller-supplied identity constructor. These local checks are not protected-main, real-browser acceptance, or release evidence. + ## 17. Exit criteria for a production capability A capability may be documented as Implemented only when: From 804a7a5f54aa6065410fe4a3644625a585c3093a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:34:35 +0900 Subject: [PATCH 41/43] test(network): exercise missing provenance through public status replies Replace the superseded unit fixture with the existing loopback response path and verify original correlation remains usable. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_json_envelope.rs | 50 ------------------- .../webdriver_bidi_session_status_response.rs | 23 +++++++++ 2 files changed, 23 insertions(+), 50 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs index ecc7eb36b..1d92d0ed8 100644 --- a/crates/originweave-network/src/webdriver_bidi_json_envelope.rs +++ b/crates/originweave-network/src/webdriver_bidi_json_envelope.rs @@ -702,56 +702,6 @@ mod tests { WebDriverBiDiJsonEnvelope::parse_str(value) } - #[test] - fn connection_correlation_rejections_preserve_pending_command() -> Result<(), Box> { - use std::{net::TcpListener, time::Duration}; - - use originweave_core::WebDriverBiDiWebSocketEndpoint; - - use crate::{ - WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, - WebDriverBiDiCommandKind, WebDriverBiDiTcpConnectionPlan, - }; - - let listener = TcpListener::bind(("127.0.0.1", 0))?; - let session = "01234567-89ab-cdef-0123-456789abcdef"; - let endpoint = format!("ws://{}/session/{session}", listener.local_addr()?); - let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)? - .correlate_session_id(session)? - .into_explicit_connect_target()?; - let connection = - WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?; - let (_server, _) = listener.accept()?; - let (_stream, evidence) = connection.into_parts(); - let generation = evidence.connection_generation(); - let mut correlation = WebDriverBiDiCommandCorrelation::new(); - correlation.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; - - for (document, expected) in [ - ( - r#"{"type":"event","method":"log.entryAdded","params":{}}"#, - WebDriverBiDiCommandCorrelationError::EventIsNotResponse, - ), - ( - r#"{"type":"success","id":7,"result":{}}"#, - WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { - command_id: 7, - }, - ), - ] { - assert_eq!( - correlation.correlate_response_for_connection( - &parse(document)?, - WebDriverBiDiCommandKind::SessionStatus, - generation, - ), - Err(expected) - ); - assert_eq!(correlation.outstanding_count(), 1); - } - Ok(()) - } - #[test] fn classifies_all_local_end_envelope_kinds_and_redacts_debug() { let success = parse( diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 45ee995b6..9a0eca693 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -25,6 +25,29 @@ const STATUS_RESPONSE_MISSING_READY: &[u8] = br#"{"type":"success","id":7,"result":{"message":"capacity available"}}"#; const STATUS_RESPONSE_EMPTY_RESULT: &[u8] = br#"{"type":"success","id":7,"result":{}}"#; +#[test] +fn unbound_command_cannot_consume_a_connection_bound_reply() -> Result<(), Box> { + use originweave_network::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind}; + + let (message, mut original) = send_status_and_read_response(STATUS_RESPONSE)?; + let mut unbound = WebDriverBiDiCommandCorrelation::new(); + unbound.register_command_for(7, WebDriverBiDiCommandKind::SessionStatus)?; + assert!(matches!( + WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut unbound), + Err(WebDriverBiDiSessionStatusResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + }) + )); + assert_eq!(unbound.outstanding_count(), 1); + assert_eq!(original.outstanding_count(), 1); + let result = WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut original)?; + assert_eq!(result.command_id(), 7); + assert_eq!(original.outstanding_count(), 0); + Ok(()) +} + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); From d5c7aea007e3c32146cb3ba88ebf251a283f336f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:36:02 +0900 Subject: [PATCH 42/43] test(network): preserve pending status across unroutable replies Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../webdriver_bidi_session_status_response.rs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs index 9a0eca693..6e733163c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs +++ b/crates/originweave-network/tests/webdriver_bidi_session_status_response.rs @@ -48,6 +48,32 @@ fn unbound_command_cannot_consume_a_connection_bound_reply() -> Result<(), Box Result<(), Box> { + use originweave_network::WebDriverBiDiCommandCorrelationError; + + for (document, expected) in [ + ( + br#"{"type":"event","method":"log.entryAdded","params":{}}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ( + br#"{"type":"error","id":null,"error":"unknown error","message":"remote"}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + ), + ] { + let (message, mut correlation) = send_status_and_read_response(document)?; + let result = + WebDriverBiDiSessionStatusResult::parse_and_correlate(&message, &mut correlation); + assert!(matches!( + result, + Err(WebDriverBiDiSessionStatusResponseError::Correlation { source }) if source == expected + )); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) +} + fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> { stream.set_read_timeout(Some(Duration::from_secs(2)))?; let mut request = Vec::new(); From bbdc6ace7a5932adf24836700f806850e6b230bc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 13:36:42 +0900 Subject: [PATCH 43/43] refactor(network): share non-consuming response routing validation Keep generic and connection-bound completion separate while reusing exact event and null-id rejection. Projection and provenance remain prerequisites to consumption. Co-Authored-By: OpenAI Codex Signed-off-by: Seongho Bae --- .../src/webdriver_bidi_command_correlation.rs | 71 ++++++++----------- docs/TEST_STRATEGY.md | 2 +- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 2b66551b0..a3dd0d53b 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -239,26 +239,8 @@ impl WebDriverBiDiCommandCorrelation { envelope: &WebDriverBiDiJsonEnvelope, expected_kind: WebDriverBiDiCommandKind, ) -> Result { - match envelope.routing() { - WebDriverBiDiJsonEnvelopeRouting::Event => { - Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) - } - WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { - Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) - } - WebDriverBiDiJsonEnvelopeRouting::CommandError { - command_id: Some(command_id), - } => self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - ), - WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - ), - } + let (command_id, outcome) = response_route(envelope)?; + self.complete(command_id, expected_kind, outcome) } pub(crate) fn correlate_response_for_connection( @@ -267,29 +249,13 @@ impl WebDriverBiDiCommandCorrelation { expected_kind: WebDriverBiDiCommandKind, received_connection_generation: WebDriverBiDiConnectionGeneration, ) -> Result { - match envelope.routing() { - WebDriverBiDiJsonEnvelopeRouting::Event => { - Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) - } - WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { - Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) - } - WebDriverBiDiJsonEnvelopeRouting::CommandError { - command_id: Some(command_id), - } => self.complete_on_connection( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Error, - received_connection_generation, - ), - WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self - .complete_on_connection( - command_id, - expected_kind, - WebDriverBiDiCorrelatedResponseOutcome::Success, - received_connection_generation, - ), - } + let (command_id, outcome) = response_route(envelope)?; + self.complete_on_connection( + command_id, + expected_kind, + outcome, + received_connection_generation, + ) } fn require_command_kind( @@ -351,6 +317,25 @@ impl WebDriverBiDiCommandCorrelation { } } +fn response_route( + envelope: &WebDriverBiDiJsonEnvelope, +) -> Result<(u64, WebDriverBiDiCorrelatedResponseOutcome), WebDriverBiDiCommandCorrelationError> { + match envelope.routing() { + WebDriverBiDiJsonEnvelopeRouting::Event => { + Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => { + Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse) + } + WebDriverBiDiJsonEnvelopeRouting::CommandError { + command_id: Some(command_id), + } => Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Error)), + WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => { + Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Success)) + } + } +} + #[cfg(test)] mod tests { use super::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind}; diff --git a/docs/TEST_STRATEGY.md b/docs/TEST_STRATEGY.md index 9a9856db6..73476ff0f 100644 --- a/docs/TEST_STRATEGY.md +++ b/docs/TEST_STRATEGY.md @@ -342,7 +342,7 @@ Documentation contracts intentionally validate only durable properties such as r ### Active browser-status response checks -The active response stack exercises fragmented text with an interleaved Ping, malformed frame and message errors, and payload-redacted diagnostics. Separate correlation checks reject an event or a reply to a request lacking connection provenance while preserving the pending request. The fixture obtains its connection identity from a real loopback connection through the existing consuming transport handoff; it does not add a caller-supplied identity constructor. These local checks are not protected-main, real-browser acceptance, or release evidence. +The active response stack exercises fragmented text with an interleaved Ping, malformed frame and message errors, and payload-redacted diagnostics. Public loopback checks reject events, unattributable errors, and replies to requests lacking connection provenance while preserving the pending request; the original request can still accept its matching reply after rejection by an unbound registry. Generic and connection-bound correlation share routing validation, while result validation and connection checks still precede completion. These local checks are not protected-main, real-browser acceptance, or release evidence. ## 17. Exit criteria for a production capability