From 992c346af20ee1882e03b8c7237a1f3d34b75c83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:28:06 +0900 Subject: [PATCH 1/5] test(core): require current semantic action observation --- .../semantic_action_current_observation.rs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 crates/originweave-core/tests/semantic_action_current_observation.rs diff --git a/crates/originweave-core/tests/semantic_action_current_observation.rs b/crates/originweave-core/tests/semantic_action_current_observation.rs new file mode 100644 index 000000000..4e4dff01f --- /dev/null +++ b/crates/originweave-core/tests/semantic_action_current_observation.rs @@ -0,0 +1,84 @@ +use std::collections::BTreeSet; + +use originweave_core::{ + BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeActionKind, ObservationChannel, + ObservedNodeHandle, Origin, SemanticNodeActionTarget, SemanticNodeActionTargetError, + SemanticNodeObservation, SemanticNodeObservationInput, +}; + +fn origin(value: &str) -> Result { + Origin::parse(value).map_err(|error| format!("{error:?}")) +} + +fn observation( + node_id: u64, + enabled: bool, + supported_actions: BTreeSet, +) -> Result { + SemanticNodeObservation::new(SemanticNodeObservationInput { + handle: ObservedNodeHandle::new( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + origin("https://app.example")?, + DocumentEpoch::new(3).map_err(|error| error.to_string())?, + node_id, + ) + .map_err(|error| error.to_string())?, + parent: None, + children: Vec::new(), + role: "button".to_owned(), + accessible_name: "Continue".to_owned(), + visible_text: Some("Continue".to_owned()), + enabled, + visible: true, + selected: None, + supported_actions, + evidence_channels: BTreeSet::from([ObservationChannel::Accessibility]), + }) + .map_err(|error| error.to_string()) +} + +#[test] +fn current_semantic_observation_revalidates_exact_target_action_state() -> Result<(), String> { + let initial = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + let target = SemanticNodeActionTarget::from_observation(&initial, NodeActionKind::Click) + .map_err(|error| error.to_string())?; + + let current = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + target + .validate_current_observation(¤t) + .map_err(|error| error.to_string())?; + + let disabled = observation(17, false, BTreeSet::from([NodeActionKind::Click]))?; + assert_eq!( + target.validate_current_observation(&disabled), + Err(SemanticNodeActionTargetError::NodeNotEnabled) + ); + + let action_removed = observation(17, true, BTreeSet::new())?; + assert_eq!( + target.validate_current_observation(&action_removed), + Err(SemanticNodeActionTargetError::UnsupportedAction) + ); + + let other_node = observation(18, true, BTreeSet::from([NodeActionKind::Click]))?; + assert_eq!( + target.validate_current_observation(&other_node), + Err(SemanticNodeActionTargetError::ObservationAuthorityMismatch) + ); + Ok(()) +} + +#[test] +fn scroll_revalidation_preserves_non_enabled_scroll_boundary() -> Result<(), String> { + let initial = observation(17, false, BTreeSet::from([NodeActionKind::ScrollIntoView]))?; + let target = + SemanticNodeActionTarget::from_observation(&initial, NodeActionKind::ScrollIntoView) + .map_err(|error| error.to_string())?; + let current = observation(17, false, BTreeSet::from([NodeActionKind::ScrollIntoView]))?; + + target + .validate_current_observation(¤t) + .map_err(|error| error.to_string())?; + Ok(()) +} From 28f7cef00d3b36438024f04f9d24c87fc9f51e27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:31:42 +0900 Subject: [PATCH 2/5] fix(core): revalidate current semantic action observation --- .../src/semantic_action_target.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/semantic_action_target.rs b/crates/originweave-core/src/semantic_action_target.rs index 9fbdd00fa..3c2ed9868 100644 --- a/crates/originweave-core/src/semantic_action_target.rs +++ b/crates/originweave-core/src/semantic_action_target.rs @@ -60,15 +60,38 @@ impl SemanticNodeActionTarget { current_epoch, ) } + + /// Revalidate this target against one freshly observed exact semantic node. + /// + /// The caller is responsible for obtaining the current observation from a trusted adapter + /// immediately before use. This check prevents an older target from ignoring changed node + /// identity, supported-action, or enabled-state evidence. + pub fn validate_current_observation( + &self, + current_observation: &SemanticNodeObservation, + ) -> Result<(), SemanticNodeActionTargetError> { + if current_observation.handle() != &self.handle { + return Err(SemanticNodeActionTargetError::ObservationAuthorityMismatch); + } + if !current_observation.supported_actions().contains(&self.action) { + return Err(SemanticNodeActionTargetError::UnsupportedAction); + } + if self.action != NodeActionKind::ScrollIntoView && !current_observation.is_enabled() { + return Err(SemanticNodeActionTargetError::NodeNotEnabled); + } + Ok(()) + } } -/// A bounded validation failure when deriving one semantic node action target. +/// A bounded validation failure when deriving or revalidating one semantic node action target. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SemanticNodeActionTargetError { /// The requested action was not advertised by the semantic observation. UnsupportedAction, /// The observation reported the target disabled for an interactive action. NodeNotEnabled, + /// The current observation describes a different OriginWeave-owned node authority. + ObservationAuthorityMismatch, } impl fmt::Display for SemanticNodeActionTargetError { @@ -80,6 +103,9 @@ impl fmt::Display for SemanticNodeActionTargetError { Self::NodeNotEnabled => { formatter.write_str("semantic node is not enabled for the requested action") } + Self::ObservationAuthorityMismatch => { + formatter.write_str("current semantic observation does not match the action target") + } } } } From f021c6e1445bc584e5f79995937c3c3c9d81a432 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:34:34 +0900 Subject: [PATCH 3/5] style(core): apply canonical semantic target formatting --- crates/originweave-core/src/semantic_action_target.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/semantic_action_target.rs b/crates/originweave-core/src/semantic_action_target.rs index 3c2ed9868..ebda26bd0 100644 --- a/crates/originweave-core/src/semantic_action_target.rs +++ b/crates/originweave-core/src/semantic_action_target.rs @@ -73,7 +73,10 @@ impl SemanticNodeActionTarget { if current_observation.handle() != &self.handle { return Err(SemanticNodeActionTargetError::ObservationAuthorityMismatch); } - if !current_observation.supported_actions().contains(&self.action) { + if !current_observation + .supported_actions() + .contains(&self.action) + { return Err(SemanticNodeActionTargetError::UnsupportedAction); } if self.action != NodeActionKind::ScrollIntoView && !current_observation.is_enabled() { From c751865412d5642357203f917f16c6c2bbd12324 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:38:44 +0900 Subject: [PATCH 4/5] test(core): cover observation mismatch error text --- crates/originweave-core/tests/semantic_node_action_target.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/tests/semantic_node_action_target.rs b/crates/originweave-core/tests/semantic_node_action_target.rs index af095a0c1..21b55e3ff 100644 --- a/crates/originweave-core/tests/semantic_node_action_target.rs +++ b/crates/originweave-core/tests/semantic_node_action_target.rs @@ -209,4 +209,8 @@ fn node_action_target_error_is_stable_and_credential_free() { SemanticNodeActionTargetError::NodeNotEnabled.to_string(), "semantic node is not enabled for the requested action" ); + assert_eq!( + SemanticNodeActionTargetError::ObservationAuthorityMismatch.to_string(), + "current semantic observation does not match the action target" + ); } From 2b9461806d3803845638f4fc855ba7c3cb6f9dce Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 10:46:56 -0700 Subject: [PATCH 5/5] test(core): align inherited action-binding formatting --- .../tests/semantic_node_action_binding.rs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_action_binding.rs b/crates/originweave-core/tests/semantic_node_action_binding.rs index 71a34b661..4122a40bf 100644 --- a/crates/originweave-core/tests/semantic_node_action_binding.rs +++ b/crates/originweave-core/tests/semantic_node_action_binding.rs @@ -74,8 +74,9 @@ fn action_request(source: Origin, target: Origin) -> Result Result<(), String> { let fixture = observation_fixture()?; - let target = SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) - .map_err(|error| error.to_string())?; + let target = + SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; let request = action_request( origin("https://app.example")?, origin("https://next.example")?, @@ -92,8 +93,9 @@ fn node_action_binding_preserves_node_target_and_business_request() -> Result<() #[test] fn node_action_binding_rejects_request_from_another_document_origin() -> Result<(), String> { let fixture = observation_fixture()?; - let target = SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) - .map_err(|error| error.to_string())?; + let target = + SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; let request = action_request( origin("https://other.example")?, origin("https://next.example")?, @@ -110,8 +112,9 @@ fn node_action_binding_rejects_request_from_another_document_origin() -> Result< fn node_action_binding_does_not_conflate_source_node_with_navigation_target() -> Result<(), String> { let fixture = observation_fixture()?; - let target = SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) - .map_err(|error| error.to_string())?; + let target = + SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; let destination = origin("https://destination.example")?; let request = action_request(origin("https://app.example")?, destination.clone())?; @@ -123,10 +126,12 @@ fn node_action_binding_does_not_conflate_source_node_with_navigation_target() -> } #[test] -fn node_action_binding_revalidates_registry_owned_authority_before_dispatch() -> Result<(), String> { +fn node_action_binding_revalidates_registry_owned_authority_before_dispatch() -> Result<(), String> +{ let fixture = observation_fixture()?; - let target = SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) - .map_err(|error| error.to_string())?; + let target = + SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; let request = action_request( origin("https://app.example")?, origin("https://next.example")?, @@ -143,8 +148,9 @@ fn node_action_binding_revalidates_registry_owned_authority_before_dispatch() -> #[test] fn node_action_binding_rejects_stale_document_before_dispatch() -> Result<(), String> { let mut fixture = observation_fixture()?; - let target = SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) - .map_err(|error| error.to_string())?; + let target = + SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; let request = action_request( origin("https://app.example")?, origin("https://next.example")?, @@ -166,8 +172,9 @@ fn node_action_binding_rejects_stale_document_before_dispatch() -> Result<(), St #[test] fn node_action_binding_rejects_retired_session_before_dispatch() -> Result<(), String> { let mut fixture = observation_fixture()?; - let target = SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) - .map_err(|error| error.to_string())?; + let target = + SemanticNodeActionTarget::from_observation(&fixture.observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; let request = action_request( origin("https://app.example")?, origin("https://next.example")?,