fix(interaction): honor scroll cancellation and visible bounds

This commit is contained in:
drakezhang
2026-09-10 00:05:48 +08:00
parent 29355e041d
commit 61c97c9cb5
13 changed files with 955 additions and 189 deletions
+32
View File
@@ -264,6 +264,38 @@ fn parses_scroll_to_target() {
assert_eq!(args.target.as_deref(), Some("@e2"));
}
#[test]
fn parses_scroll_to_explicit_target_tab_and_timeout() {
for flag in ["--ref", "--selector"] {
let cli = parse(&[
"bsk",
"scroll-to",
flag,
"e3",
"--session",
"s1",
"--tab-id",
"42",
"--timeout",
"5s",
]);
let Command::ScrollTo(args) = cli.command else {
panic!("expected scroll-to command");
};
assert_eq!(args.tab_id, Some(42));
assert_eq!(args.timeout, 5_000);
assert_eq!(
if flag == "--ref" {
args.ref_
} else {
args.selector
}
.as_deref(),
Some("e3")
);
}
}
#[test]
fn parses_focus_target() {
let cli = parse(&["bsk", "focus", "@e2", "--session", "s1"]);
+23 -13
View File
@@ -293,6 +293,15 @@ async fn session_user_interrupt_event_cancels_inflight_with_user_aborted() {
/// catch a tool dispatched arbitrarily later.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn user_interrupt_rejects_next_mutating_tool_call_when_session_was_idle() {
assert_idle_interrupt_rejects(Method::ToolClick).await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn user_interrupt_rejects_scroll_to_before_forwarding() {
assert_idle_interrupt_rejects(Method::ToolScrollTo).await;
}
async fn assert_idle_interrupt_rejects(method: Method) {
let (handle, sock) = spawn_daemon().await;
let mut ws = connect_ext(handle.ws_addr()).await;
let _ = handshake_as_ext(&mut ws).await;
@@ -301,10 +310,11 @@ async fn user_interrupt_rejects_next_mutating_tool_call_when_session_was_idle()
// Fake extension: answer tool.session_start, observe other
// requests but never reply. We expect the daemon to reject the
// tool call BEFORE it ever forwards a tool.click frame.
// tool call BEFORE it ever forwards a browser-input frame.
let ws_sink_for_responder = Arc::clone(&ws_sink);
let observed_click_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let observed_click_count_clone = Arc::clone(&observed_click_count);
let observed_call_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let observed_call_count_clone = Arc::clone(&observed_call_count);
let forwarded_method = method.clone();
let responder = tokio::spawn(async move {
let mut ws_stream = ws_stream;
while let Some(Ok(msg)) = ws_stream.next().await {
@@ -329,8 +339,8 @@ async fn user_interrupt_rejects_next_mutating_tool_call_when_session_was_idle()
g.send(Message::Text(serde_json::to_string(&reply).unwrap()))
.await
.unwrap();
} else if req.method == Method::ToolClick {
observed_click_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
} else if req.method == forwarded_method {
observed_call_count_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
// Deliberately do NOT reply — daemon should reject before
// ever forwarding to us.
let _ = req;
@@ -377,12 +387,12 @@ async fn user_interrupt_rejects_next_mutating_tool_call_when_session_was_idle()
let state = handle.state();
wait_for_session_interrupt_pending(&state, &start.session_id).await;
// The CLI now issues a tool.click (mutating). The daemon must
// The CLI now issues a browser-input (mutating). The daemon must
// reject it WITHOUT forwarding to the extension.
let click_outcome = ipc
let outcome = ipc
.call::<_, serde_json::Value>(
"click-after-interrupt",
Method::ToolClick,
"input-after-interrupt",
method,
Some(json!({
"session_id": start.session_id,
"ref": "fake-ref-1",
@@ -391,7 +401,7 @@ async fn user_interrupt_rejects_next_mutating_tool_call_when_session_was_idle()
)
.await
.unwrap();
let err = click_outcome.expect_err("tool.click must be rejected");
let err = outcome.expect_err("browser-input must be rejected");
assert_eq!(
err.code,
ErrorCode::UserAborted,
@@ -399,11 +409,11 @@ async fn user_interrupt_rejects_next_mutating_tool_call_when_session_was_idle()
err
);
// Crucially: the extension never saw a tool.click frame.
// Crucially: the extension never saw a browser-input frame.
assert_eq!(
observed_click_count.load(std::sync::atomic::Ordering::SeqCst),
observed_call_count.load(std::sync::atomic::Ordering::SeqCst),
0,
"tool.click must NOT have been forwarded to the extension"
"browser-input must NOT have been forwarded to the extension"
);
responder.abort();