From f845e51c5f31d7b700aeaf65ce7818d9d5f940e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:04:58 +0900 Subject: [PATCH 1/3] test(core): reject disabled semantic actions --- .../tests/semantic_node_action_target.rs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_action_target.rs b/crates/originweave-core/tests/semantic_node_action_target.rs index 33d001551..af095a0c1 100644 --- a/crates/originweave-core/tests/semantic_node_action_target.rs +++ b/crates/originweave-core/tests/semantic_node_action_target.rs @@ -7,6 +7,10 @@ use originweave_core::{ }; fn observation() -> Result { + observation_with_enabled(true) +} + +fn observation_with_enabled(enabled: bool) -> Result { let handle = ObservedNodeHandle::new( BrowserSessionId::new(7).map_err(|error| error.to_string())?, BrowsingContextId::new(11).map_err(|error| error.to_string())?, @@ -23,10 +27,10 @@ fn observation() -> Result { role: "button".to_owned(), accessible_name: "Save draft".to_owned(), visible_text: Some("Save draft".to_owned()), - enabled: true, + enabled, visible: true, selected: None, - supported_actions: BTreeSet::from([NodeActionKind::Click]), + supported_actions: BTreeSet::from([NodeActionKind::Click, NodeActionKind::ScrollIntoView]), evidence_channels: BTreeSet::from([ObservationChannel::Accessibility]), }) .map_err(|error| error.to_string()) @@ -53,6 +57,26 @@ fn unsupported_node_action_fails_closed_without_minting_authority() -> Result<() Ok(()) } +#[test] +fn disabled_interactive_node_action_fails_closed() -> Result<(), String> { + let observed = observation_with_enabled(false)?; + assert_eq!( + SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click).err(), + Some(SemanticNodeActionTargetError::NodeNotEnabled) + ); + Ok(()) +} + +#[test] +fn disabled_node_can_still_be_targeted_for_scroll_only() -> Result<(), String> { + let observed = observation_with_enabled(false)?; + let target = + SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::ScrollIntoView) + .map_err(|error| error.to_string())?; + assert_eq!(target.action(), NodeActionKind::ScrollIntoView); + Ok(()) +} + #[test] fn node_action_target_revalidates_exact_browser_authority() -> Result<(), String> { let observed = observation()?; @@ -181,4 +205,8 @@ fn node_action_target_error_is_stable_and_credential_free() { SemanticNodeActionTargetError::UnsupportedAction.to_string(), "semantic node action is not advertised by the observation" ); + assert_eq!( + SemanticNodeActionTargetError::NodeNotEnabled.to_string(), + "semantic node is not enabled for the requested action" + ); } From bd75a43ddcd0a7afa4f032ecc2b930d742c3ece5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 10:06:27 +0900 Subject: [PATCH 2/3] fix(core): reject disabled semantic actions --- crates/originweave-core/src/semantic_action_target.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/semantic_action_target.rs b/crates/originweave-core/src/semantic_action_target.rs index 85fe59d29..9fbdd00fa 100644 --- a/crates/originweave-core/src/semantic_action_target.rs +++ b/crates/originweave-core/src/semantic_action_target.rs @@ -16,7 +16,7 @@ pub struct SemanticNodeActionTarget { } impl SemanticNodeActionTarget { - /// Construct a target only when the observation advertised the requested node-local action. + /// Construct a target only when the observation advertises a currently coherent node action. pub fn from_observation( observation: &SemanticNodeObservation, action: NodeActionKind, @@ -24,6 +24,9 @@ impl SemanticNodeActionTarget { if !observation.supported_actions().contains(&action) { return Err(SemanticNodeActionTargetError::UnsupportedAction); } + if action != NodeActionKind::ScrollIntoView && !observation.is_enabled() { + return Err(SemanticNodeActionTargetError::NodeNotEnabled); + } Ok(Self { handle: observation.handle().clone(), action, @@ -64,6 +67,8 @@ impl SemanticNodeActionTarget { 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, } impl fmt::Display for SemanticNodeActionTargetError { @@ -72,6 +77,9 @@ impl fmt::Display for SemanticNodeActionTargetError { Self::UnsupportedAction => { formatter.write_str("semantic node action is not advertised by the observation") } + Self::NodeNotEnabled => { + formatter.write_str("semantic node is not enabled for the requested action") + } } } } From 2505cbf04bb1b88062ac35d0335e1bb57932f305 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 10:46:08 -0700 Subject: [PATCH 3/3] 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")?,