diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-02 02:45:20 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-02 02:45:20 +0300 |
| commit | cc88b3b8e5bb9f7d9f23ed6ae85a52fd7b5b9ed6 (patch) | |
| tree | 4d37c760099070f1a1488b61c7b7271875aafef8 /src/client/shell/tests/keybindings_settings.rs | |
| parent | 18e69891dca486d669a584facd80644bb51f54a2 (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.rs | 147 |
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] |
