From 3834fd3d0f77a809cf3e394fca31fd036d3173dd Mon Sep 17 00:00:00 2001 From: JJ Liebig Date: Sun, 13 Sep 2026 21:14:00 +0400 Subject: fix: auto-expire worktree action error banners (#4063) * fix: auto-expire worktree action error banners refs #1827 * fix: restart endpoint error timeout on repeated errors Route every transient client-side action error through a setter that installs a fresh deadline, so an identical repeated message cannot inherit an older, nearly-expired deadline. refs #1827 * test: derive endpoint error expiry assertions from stored deadline refs #1827 --- .../shell/tests/agents_worktrees_notifications.rs | 65 ++++++++++++++++++++++ 1 file changed, 65 insertions(+) (limited to 'src/client/shell/tests/agents_worktrees_notifications.rs') diff --git a/src/client/shell/tests/agents_worktrees_notifications.rs b/src/client/shell/tests/agents_worktrees_notifications.rs index 0b936903..c89991b1 100644 --- a/src/client/shell/tests/agents_worktrees_notifications.rs +++ b/src/client/shell/tests/agents_worktrees_notifications.rs @@ -1087,6 +1087,71 @@ fn unavailable_worktree_create_does_not_wedge_the_overlay() { .is_some_and(|notice| notice.key.code == "worktree.create")); } +#[test] +fn worktree_action_errors_expire_without_more_input() { + let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); + state.set_snapshot(Box::new(snapshot())); + state.set_pane_surface(surface()); + + let mut guard = ClientShellInput::default(); + state.record_binding( + crate::input::KeybindMatch::Action(crate::input::KeybindAction::RemoveWorktree), + &mut guard, + ); + let message = "This workspace is not a Herdr-managed worktree checkout."; + assert_eq!(state.endpoint_error.as_deref(), Some(message)); + + let deadline = state.endpoint_error_deadline.expect("deadline"); + assert!(!state.tick_endpoint_error(deadline - std::time::Duration::from_secs(1))); + assert_eq!(state.endpoint_error.as_deref(), Some(message)); + + assert!(state.tick_endpoint_error(deadline + std::time::Duration::from_millis(1))); + assert!(state.endpoint_error.is_none()); + + // A repeated identical message must start a fresh lifetime instead of + // inheriting the earlier deadline. + let before_repeat = std::time::Instant::now(); + state.set_endpoint_error(message); + assert!( + state.endpoint_error_deadline.expect("deadline") + >= before_repeat + std::time::Duration::from_secs(5) + ); +} + +#[test] +fn worktree_prepare_rejection_notice_expires() { + let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); + state.set_snapshot(Box::new(snapshot())); + state.set_pane_surface(surface()); + let mut prepare = ClientShellInput::default(); + state.record_binding( + crate::input::KeybindMatch::Action(crate::input::KeybindAction::NewWorktree), + &mut prepare, + ); + let [ClientShellAction::Endpoint { request, .. }] = &prepare.actions[..] else { + panic!("new worktree should prepare through worktree.list"); + }; + let request_id = request.id.clone(); + state.handle_endpoint_result( + "boot-1", + &request_id, + Err(ClientShellEndpointError { + code: Some("not_git_worktree".into()), + message: "Herdr worktree actions require a workspace inside a Git work tree".into(), + }), + ); + let notice = state + .visible_endpoint_notice + .as_ref() + .expect("rejection notice"); + assert!(notice.key.code.contains("not_git_worktree")); + let deadline = notice.deadline; + + let (_, repaint) = state.tick_notifications(deadline + std::time::Duration::from_millis(1)); + assert!(repaint); + assert!(state.visible_endpoint_notice.is_none()); +} + #[test] fn worktree_open_filters_and_clicks_a_stable_public_entry() { let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); -- cgit v1.2.3