aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/tests/keybindings_settings.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-02 02:45:20 +0300
committerGitHub <noreply@github.com>2026-09-02 02:45:20 +0300
commitcc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (patch)
tree4d37c760099070f1a1488b61c7b7271875aafef8 /src/client/shell/tests/keybindings_settings.rs
parent18e69891dca486d669a584facd80644bb51f54a2 (diff)
feat: add stable client endpoint compatibility (#3509)
* feat: add stable client endpoint compatibility * fix: keep portable-pty patch checks composable * ci: run maintenance checks in pull requests * fix: reject endpoint errors from terminal clients * feat: keep compatible servers running across updates
Diffstat (limited to 'src/client/shell/tests/keybindings_settings.rs')
-rw-r--r--src/client/shell/tests/keybindings_settings.rs147
1 files changed, 141 insertions, 6 deletions
diff --git a/src/client/shell/tests/keybindings_settings.rs b/src/client/shell/tests/keybindings_settings.rs
index 5d2d8243..5b0fced7 100644
--- a/src/client/shell/tests/keybindings_settings.rs
+++ b/src/client/shell/tests/keybindings_settings.rs
@@ -437,6 +437,60 @@ fn plugin_command_carries_client_owned_selection_coordinates() {
}
#[test]
+fn unavailable_endpoint_method_is_disabled_without_disconnect() {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ state.set_pane_surface(surface());
+ state.set_endpoint_methods(Some(vec!["pane.focus".into()]));
+ let mut outcome = ClientShellInput::default();
+
+ state.push_endpoint_method(
+ crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget {
+ workspace_id: "missing".into(),
+ }),
+ &mut outcome,
+ );
+
+ assert!(outcome.actions.is_empty());
+ assert!(outcome.repaint);
+ let notice = state
+ .visible_endpoint_notice
+ .as_ref()
+ .expect("unsupported action notice");
+ assert_eq!(notice.key.kind, ClientEndpointNoticeKind::Unsupported);
+ assert_eq!(notice.key.code, "workspace.focus");
+ assert!(notice.body.contains("This server"));
+ assert!(state.endpoint_error.is_none());
+
+ let mut repeated = ClientShellInput::default();
+ state.push_endpoint_method(
+ crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget {
+ workspace_id: "missing".into(),
+ }),
+ &mut repeated,
+ );
+ assert!(repeated.actions.is_empty());
+ assert!(!repeated.repaint);
+
+ state.compose(106, 20).expect("endpoint notice frame");
+ assert!(!state.hits.notification_toast.is_empty());
+
+ state.handle_input_bytes(b"x");
+ assert!(state.visible_endpoint_notice.is_some());
+
+ let hit = state.hits.notification_toast;
+ let dismissed =
+ state.handle_raw_events(vec![RawInputEvent::Mouse(crossterm::event::MouseEvent {
+ kind: MouseEventKind::Down(MouseButton::Left),
+ column: hit.x,
+ row: hit.y,
+ modifiers: KeyModifiers::empty(),
+ })]);
+ assert!(dismissed.repaint);
+ assert!(state.visible_endpoint_notice.is_none());
+}
+
+#[test]
fn generic_endpoint_failures_and_control_errors_are_visible() {
let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
state.set_snapshot(Box::new(snapshot()));
@@ -461,17 +515,98 @@ fn generic_endpoint_failures_and_control_errors_are_visible() {
);
assert!(repaint);
assert!(actions.is_empty());
+ let notice = state
+ .visible_endpoint_notice
+ .as_ref()
+ .expect("rejected action notice");
+ assert_eq!(notice.key.kind, ClientEndpointNoticeKind::Rejected);
+ assert_eq!(notice.key.code, "workspace.focus:not_found");
+ assert_eq!(notice.body, "workspace no longer exists");
+ assert!(state.endpoint_error.is_none());
+
+ assert!(state.receive_endpoint_error("Paste rejected: too large".into()));
+ let notice = state
+ .visible_endpoint_notice
+ .as_ref()
+ .expect("paste rejection notice");
+ assert_eq!(notice.key.kind, ClientEndpointNoticeKind::Rejected);
+ assert_eq!(notice.key.code, "paste_rejected");
+ assert_eq!(notice.body, "Paste rejected: too large");
+ assert!(state.receive_endpoint_error("Paste rejected again".into()));
assert_eq!(
- state.endpoint_error.as_deref(),
- Some("workspace no longer exists")
+ state
+ .visible_endpoint_notice
+ .as_ref()
+ .map(|notice| notice.body.as_str()),
+ Some("Paste rejected again")
);
+}
- assert!(state.receive_endpoint_error("Paste rejected: too large".into()));
+#[test]
+fn endpoint_timeout_is_a_deduplicated_server_notice() {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ let mut outcome = ClientShellInput::default();
+ state.push_endpoint_method(
+ crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget {
+ workspace_id: "work".into(),
+ }),
+ &mut outcome,
+ );
+ let request_id = match &outcome.actions[..] {
+ [ClientShellAction::Endpoint { request, .. }] => request.id.clone(),
+ other => panic!("expected endpoint request, got {other:?}"),
+ };
+
+ let (repaint, actions) = state.handle_endpoint_result(
+ "boot-1",
+ &request_id,
+ Err(ClientShellEndpointError {
+ code: Some("endpoint_timeout".into()),
+ message: "transport timeout".into(),
+ }),
+ );
+
+ assert!(repaint);
+ assert!(actions.is_empty());
+ let notice = state
+ .visible_endpoint_notice
+ .as_ref()
+ .expect("timeout notice");
+ assert_eq!(notice.key.kind, ClientEndpointNoticeKind::Timeout);
+ assert_eq!(notice.key.code, "workspace.focus");
+ assert!(notice.body.contains("workspace.focus"));
+ assert!(state.endpoint_error.is_none());
+}
+
+#[test]
+fn endpoint_notice_dedupe_resets_for_a_new_server_boot() {
+ let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default()));
+ state.set_snapshot(Box::new(snapshot()));
+ state.set_endpoint_methods(Some(Vec::new()));
+ let method = || {
+ crate::api::schema::Method::WorkspaceFocus(crate::api::schema::WorkspaceTarget {
+ workspace_id: "work".into(),
+ })
+ };
+ let mut first = ClientShellInput::default();
+ state.push_endpoint_method(method(), &mut first);
+ assert!(first.repaint);
+
+ let mut next_snapshot = snapshot();
+ next_snapshot.boot_id = "boot-2".into();
+ state.set_snapshot(Box::new(next_snapshot));
+ let mut second = ClientShellInput::default();
+ state.push_endpoint_method(method(), &mut second);
+
+ assert!(second.repaint);
assert_eq!(
- state.endpoint_error.as_deref(),
- Some("Paste rejected: too large")
+ state
+ .visible_endpoint_notice
+ .as_ref()
+ .map(|notice| notice.key.boot_id.as_str()),
+ Some("boot-2")
);
- assert!(!state.receive_endpoint_error("Paste rejected: too large".into()));
}
#[test]