aboutsummaryrefslogtreecommitdiff
path: root/src/client/shell/tests/endpoints.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/shell/tests/endpoints.rs')
-rw-r--r--src/client/shell/tests/endpoints.rs427
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;