diff options
| author | akbash <akbash@herdr.dev> | 2026-09-14 01:11:07 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-14 02:11:07 +0400 |
| commit | 3b478e69dcbe8e416ddd46f0e24156392d4e71da (patch) | |
| tree | e4ae8adfc09dce6eb09bd036fb727cc7f19edff7 /src/client/shell/tests/endpoints.rs | |
| parent | 4e1096b1018496a23fb9de7f9ae737d3792bb38b (diff) | |
fix: scope agent views to the selected machine (#3784)
* fix: scope agent views to the selected machine
refs #3732
* fix: preserve stable agent view ordering
refs #3732
---------
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: JJ Liebig <jonathan.liebig@gmail.com>
Diffstat (limited to 'src/client/shell/tests/endpoints.rs')
| -rw-r--r-- | src/client/shell/tests/endpoints.rs | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/src/client/shell/tests/endpoints.rs b/src/client/shell/tests/endpoints.rs index 2e5e87ac..67663bea 100644 --- a/src/client/shell/tests/endpoints.rs +++ b/src/client/shell/tests/endpoints.rs @@ -40,6 +40,24 @@ fn agent( } } +fn current_workspace_view() -> crate::api::schema::AgentViewSetParams { + use crate::api::schema::{ + AgentViewBuiltinField, AgentViewContext, AgentViewField, AgentViewFilter, AgentViewValue, + }; + + crate::api::schema::AgentViewSetParams { + source: "example.views".into(), + label: Some("current space".into()), + filter: Some(AgentViewFilter::Eq { + field: AgentViewField::Builtin(AgentViewBuiltinField::WorkspaceId), + value: AgentViewValue::Context { + context: AgentViewContext::CurrentWorkspaceId, + }, + }), + sort: Vec::new(), + } +} + fn state_with_remote() -> (ClientShellState, ClientEndpointId) { let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); let profile = remote_profile(); @@ -660,6 +678,415 @@ fn aggregate_agents_use_configured_rows_machine_token_and_status_colors() { } #[test] +fn current_workspace_agent_view_excludes_same_workspace_id_on_other_machine() { + use crate::api::schema::AgentStatus; + use crate::config::AgentSidebarToken; + + let mut config = Config::default(); + config.ui.sidebar.agents.rows = + vec![vec![AgentSidebarToken::Machine, AgentSidebarToken::Agent]]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + let profile = remote_profile(); + let endpoint_id = ClientEndpointId::Ssh(profile.id.clone()); + state.set_endpoint_catalog(&[profile]); + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); + + let mut local = snapshot(); + local.agent_view_label = Some("current space".into()); + local.agent_order = vec!["pane_1".into()]; + local.agents = vec![agent("local agent", AgentStatus::Idle, 1)]; + state.set_snapshot(Box::new(local)); + state.set_pane_surface(surface()); + + let mut remote = snapshot(); + remote.boot_id = "remote-boot".into(); + remote.agent_view_label = Some("current space".into()); + remote.agent_order = vec!["pane_1".into()]; + remote.agents = vec![agent("remote agent", AgentStatus::Idle, 1)]; + state.set_endpoint_snapshot(&endpoint_id, Box::new(remote)); + let view = current_workspace_view(); + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, Some(view.clone())); + state.set_test_endpoint_agent_view(&endpoint_id, Some(view)); + + let frame = state.compose(100, 28).expect("combined endpoint frame"); + let text = frame + .cells + .chunks(frame.width as usize) + .map(|row| { + row.iter() + .map(|cell| cell.symbol.as_str()) + .collect::<String>() + }) + .collect::<Vec<_>>() + .join("\n"); + assert!(text.contains("Local · local agent"), "frame: {text}"); + assert!(!text.contains("Build · remote agent"), "frame: {text}"); + + assert!(state.activate_endpoint_projection(&endpoint_id)); + let mut remote_surface = surface(); + remote_surface.boot_id = "remote-boot".into(); + state.set_pane_surface(remote_surface); + let frame = state.compose(100, 28).expect("remote endpoint frame"); + let text = frame + .cells + .chunks(frame.width as usize) + .map(|row| { + row.iter() + .map(|cell| cell.symbol.as_str()) + .collect::<String>() + }) + .collect::<Vec<_>>() + .join("\n"); + assert!(!text.contains("Local · local agent"), "frame: {text}"); + assert!(text.contains("Build · remote agent"), "frame: {text}"); +} + +#[test] +fn current_workspace_or_blocked_keeps_foreign_attention_only() { + use crate::api::schema::{ + AgentStatus, AgentViewBuiltinField, AgentViewField, AgentViewFilter, AgentViewValue, + }; + use crate::config::AgentSidebarToken; + + let mut config = Config::default(); + config.ui.sidebar.agents.rows = + vec![vec![AgentSidebarToken::Machine, AgentSidebarToken::Agent]]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + let profile = remote_profile(); + let endpoint_id = ClientEndpointId::Ssh(profile.id.clone()); + state.set_endpoint_catalog(&[profile]); + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); + + let mut local = snapshot(); + local.agent_view_label = Some("focus".into()); + local.agents = vec![agent("local agent", AgentStatus::Idle, 1)]; + state.set_snapshot(Box::new(local)); + state.set_pane_surface(surface()); + + let mut remote = snapshot(); + remote.boot_id = "remote-boot".into(); + remote.agents = vec![ + agent("remote idle", AgentStatus::Idle, 1), + ClientShellAgent { + pane_id: "pane_2".into(), + name: Some("remote blocked".into()), + agent_status: AgentStatus::Blocked, + focused: false, + ..agent("remote blocked", AgentStatus::Blocked, 2) + }, + ]; + remote.panes.push(ClientShellPane { + pane_id: "pane_2".into(), + focused: false, + ..remote.panes[0].clone() + }); + state.set_endpoint_snapshot(&endpoint_id, Box::new(remote)); + + let mut view = current_workspace_view(); + view.label = Some("focus".into()); + view.filter = Some(AgentViewFilter::Any { + filters: vec![ + view.filter.take().expect("current workspace filter"), + AgentViewFilter::Eq { + field: AgentViewField::Builtin(AgentViewBuiltinField::Status), + value: AgentViewValue::String("blocked".into()), + }, + ], + }); + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, Some(view)); + + let frame = state.compose(100, 28).expect("combined endpoint frame"); + let text = frame + .cells + .chunks(frame.width as usize) + .map(|row| { + row.iter() + .map(|cell| cell.symbol.as_str()) + .collect::<String>() + }) + .collect::<Vec<_>>() + .join("\n"); + assert!(text.contains("Local · local agent"), "frame: {text}"); + assert!(!text.contains("Build · remote idle"), "frame: {text}"); + assert!(text.contains("Build · remote blocked"), "frame: {text}"); +} + +#[test] +fn selected_default_view_ignores_inactive_endpoint_projection() { + use crate::api::schema::{ + AgentStatus, AgentViewBuiltinField, AgentViewField, AgentViewFilter, AgentViewValue, + }; + use crate::config::AgentSidebarToken; + + let mut config = Config::default(); + config.ui.sidebar.agents.rows = + vec![vec![AgentSidebarToken::Machine, AgentSidebarToken::Agent]]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + let profile = remote_profile(); + let endpoint_id = ClientEndpointId::Ssh(profile.id.clone()); + state.set_endpoint_catalog(&[profile]); + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); + + let mut local = snapshot(); + local.agents = vec![agent("local agent", AgentStatus::Idle, 1)]; + state.set_snapshot(Box::new(local)); + state.set_pane_surface(surface()); + let mut remote = snapshot(); + remote.boot_id = "remote-boot".into(); + remote.agent_view_label = Some("blocked".into()); + remote.agent_order.clear(); + remote.agents = vec![agent("remote agent", AgentStatus::Idle, 1)]; + state.set_endpoint_snapshot(&endpoint_id, Box::new(remote)); + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, None); + state.set_test_endpoint_agent_view( + &endpoint_id, + Some(crate::api::schema::AgentViewSetParams { + source: "remote.views".into(), + label: Some("blocked".into()), + filter: Some(AgentViewFilter::Eq { + field: AgentViewField::Builtin(AgentViewBuiltinField::Status), + value: AgentViewValue::String("blocked".into()), + }), + sort: Vec::new(), + }), + ); + + let frame = state.compose(100, 28).expect("combined endpoint frame"); + let text = frame + .cells + .chunks(frame.width as usize) + .map(|row| { + row.iter() + .map(|cell| cell.symbol.as_str()) + .collect::<String>() + }) + .collect::<Vec<_>>() + .join("\n"); + assert!(text.contains("Local · local agent"), "frame: {text}"); + assert!(text.contains("Build · remote agent"), "frame: {text}"); + assert!(text.contains("grouped"), "frame: {text}"); +} + +#[test] +fn newer_snapshot_does_not_reuse_stale_agent_view_projection() { + let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); + let mut local = snapshot(); + local.agent_view_label = Some("current space".into()); + state.set_snapshot(Box::new(local.clone())); + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, Some(current_workspace_view())); + let endpoint = state + .endpoints + .iter() + .find(|endpoint| endpoint.endpoint_id.is_local()) + .expect("local endpoint"); + assert!(matches!( + ClientShellState::endpoint_agent_view(endpoint), + Some(Ok(Some(_))) + )); + + state.set_test_endpoint_agent_view_projection( + &ClientEndpointId::Local, + "foreign-boot", + 99, + None, + ); + let endpoint = state + .endpoints + .iter() + .find(|endpoint| endpoint.endpoint_id.is_local()) + .expect("local endpoint"); + assert!(matches!( + ClientShellState::endpoint_agent_view(endpoint), + Some(Ok(Some(_))) + )); + + local.revision += 1; + state.set_snapshot(Box::new(local)); + let endpoint = state + .endpoints + .iter() + .find(|endpoint| endpoint.endpoint_id.is_local()) + .expect("local endpoint"); + assert!(ClientShellState::endpoint_agent_view(endpoint).is_none()); +} + +#[test] +fn legacy_custom_views_keep_v1_per_endpoint_projection() { + use crate::api::schema::AgentStatus; + use crate::config::AgentSidebarToken; + + let mut config = Config::default(); + config.ui.sidebar.agents.rows = + vec![vec![AgentSidebarToken::Machine, AgentSidebarToken::Agent]]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + let profile = remote_profile(); + let endpoint_id = ClientEndpointId::Ssh(profile.id.clone()); + state.set_endpoint_catalog(&[profile]); + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); + + let mut local = snapshot(); + local.agent_view_label = Some("current space".into()); + local.agent_order = vec!["pane_1".into()]; + local.agents = vec![agent("local agent", AgentStatus::Idle, 1)]; + state.set_snapshot(Box::new(local)); + state.set_pane_surface(surface()); + let mut remote = snapshot(); + remote.boot_id = "remote-boot".into(); + remote.agent_view_label = Some("current space".into()); + remote.agent_order = vec!["pane_1".into()]; + remote.agents = vec![agent("remote agent", AgentStatus::Idle, 1)]; + state.set_endpoint_snapshot(&endpoint_id, Box::new(remote)); + + let frame = state.compose(100, 28).expect("legacy combined frame"); + let text = frame + .cells + .chunks(frame.width as usize) + .map(|row| { + row.iter() + .map(|cell| cell.symbol.as_str()) + .collect::<String>() + }) + .collect::<Vec<_>>() + .join("\n"); + assert!(text.contains("Local · local agent"), "frame: {text}"); + assert!(text.contains("Build · remote agent"), "frame: {text}"); +} + +#[test] +fn selected_custom_sort_orders_rendering_and_indexed_navigation() { + use crate::api::schema::{ + AgentStatus, AgentViewBuiltinSortField, AgentViewSort, AgentViewSortField, + AgentViewSortOrder, + }; + use crate::config::AgentSidebarToken; + + let mut config = Config::default(); + config.ui.agent_panel_sort = crate::config::AgentPanelSortConfig::Priority; + config.ui.sidebar.agents.rows = + vec![vec![AgentSidebarToken::Machine, AgentSidebarToken::Agent]]; + let mut state = ClientShellState::new(ClientShellConfig::from_config(&config)); + let profile = remote_profile(); + let endpoint_id = ClientEndpointId::Ssh(profile.id.clone()); + state.set_endpoint_catalog(&[profile]); + state.set_endpoint_status(&endpoint_id, ClientEndpointStatus::Online); + + let mut local = snapshot(); + local.agent_view_label = Some("recent".into()); + local.agents = vec![agent("local blocked", AgentStatus::Blocked, 1)]; + state.set_snapshot(Box::new(local)); + state.set_pane_surface(surface()); + let mut remote = snapshot(); + remote.boot_id = "remote-boot".into(); + remote.agents = vec![agent("remote idle", AgentStatus::Idle, 9)]; + state.set_endpoint_snapshot(&endpoint_id, Box::new(remote)); + + let mut view = current_workspace_view(); + view.label = Some("recent".into()); + view.filter = None; + view.sort = vec![AgentViewSort { + field: AgentViewSortField::Builtin(AgentViewBuiltinSortField::StateChangeSeq), + order: AgentViewSortOrder::Desc, + }]; + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, Some(view)); + + let frame = state.compose(100, 28).expect("custom sorted frame"); + let text = frame + .cells + .chunks(frame.width as usize) + .map(|row| { + row.iter() + .map(|cell| cell.symbol.as_str()) + .collect::<String>() + }) + .collect::<Vec<_>>() + .join("\n"); + assert!( + text.find("Build · remote idle").expect("remote row") + < text.find("Local · local blocked").expect("local row"), + "frame: {text}" + ); + + let mut outcome = ClientShellInput::default(); + assert!( + state.handle_endpoint_navigation(crate::input::KeybindAction::FocusAgent(0), &mut outcome,) + ); + assert!(matches!( + outcome.actions.as_slice(), + [ClientShellAction::ActivateEndpoint { + endpoint_id: selected, + target: Some(ClientEndpointFocusTarget::Pane(pane_id)), + }] if selected == &endpoint_id && pane_id == "pane_1" + )); +} + +#[test] +fn selected_position_sort_uses_public_tab_and_pane_numbers() { + use crate::api::schema::{ + AgentStatus, AgentViewBuiltinSortField, AgentViewSort, AgentViewSortField, + AgentViewSortOrder, + }; + + let mut state = ClientShellState::new(ClientShellConfig::from_config(&Config::default())); + let mut selected = snapshot(); + selected.agent_view_label = Some("positions".into()); + + let mut tab_nine = selected.tabs[0].clone(); + tab_nine.tab_id = "ws_1:t9".into(); + tab_nine.number = 9; + let mut tab_two = tab_nine.clone(); + tab_two.tab_id = "ws_1:t2".into(); + tab_two.number = 2; + selected.tabs = vec![tab_nine, tab_two]; + + let mut pane_tab_nine = selected.panes[0].clone(); + pane_tab_nine.tab_id = "ws_1:t9".into(); + pane_tab_nine.pane_id = "ws_1:p1".into(); + let mut pane_nine = pane_tab_nine.clone(); + pane_nine.tab_id = "ws_1:t2".into(); + pane_nine.pane_id = "ws_1:p9".into(); + let mut pane_two = pane_nine.clone(); + pane_two.pane_id = "ws_1:p2".into(); + selected.panes = vec![pane_tab_nine, pane_nine, pane_two]; + + let mut late_tab = agent("tab nine", AgentStatus::Idle, 1); + late_tab.tab_id = "ws_1:t9".into(); + late_tab.pane_id = "ws_1:p1".into(); + let mut late_pane = agent("pane nine", AgentStatus::Idle, 1); + late_pane.tab_id = "ws_1:t2".into(); + late_pane.pane_id = "ws_1:p9".into(); + let mut early_pane = agent("pane two", AgentStatus::Idle, 1); + early_pane.tab_id = "ws_1:t2".into(); + early_pane.pane_id = "ws_1:p2".into(); + selected.agents = vec![late_tab, late_pane, early_pane]; + state.set_snapshot(Box::new(selected)); + + let mut view = current_workspace_view(); + view.label = Some("positions".into()); + view.filter = None; + view.sort = vec![ + AgentViewSort { + field: AgentViewSortField::Builtin(AgentViewBuiltinSortField::TabOrder), + order: AgentViewSortOrder::Asc, + }, + AgentViewSort { + field: AgentViewSortField::Builtin(AgentViewBuiltinSortField::PaneOrder), + order: AgentViewSortOrder::Asc, + }, + ]; + state.set_test_endpoint_agent_view(&ClientEndpointId::Local, Some(view)); + + let names = aggregate_navigation::aggregate_agent_rows( + &state.endpoints, + &state.active_endpoint_id, + crate::config::AgentPanelSortConfig::Priority, + ) + .into_iter() + .map(|row| row.agent.name.as_deref().expect("agent name")) + .collect::<Vec<_>>(); + assert_eq!(names, ["pane two", "pane nine", "tab nine"]); +} + +#[test] fn aggregate_priority_uses_client_observed_recency_across_machines() { use crate::api::schema::AgentStatus; use crate::config::AgentSidebarToken; |
