use super::*;use acp_thread::{AcpThread, PermissionOptions, StubAgentConnection};use agent::ThreadStore;use agent_ui::{ test_support::{active_session_id, open_thread_with_connection, send_message}, thread_metadata_store::ThreadMetadata,};use chrono::DateTime;use fs::{FakeFs, Fs};use gpui::TestAppContext;use pretty_assertions::assert_eq;use project::AgentId;use settings::SettingsStore;use std::{ path::{Path, PathBuf}, sync::Arc,};use util::path_list::PathList; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); theme_settings::init(theme::LoadThemes::JustBase, cx); editor::init(cx); ThreadStore::init_global(cx); ThreadMetadataStore::init_global(cx); language_model::LanguageModelRegistry::test(cx); prompt_store::init(cx); });} #[track_caller]fn assert_active_thread(sidebar: &Sidebar, session_id: &acp::SessionId, msg: &str) { assert!( sidebar .active_entry .as_ref() .is_some_and(|e| e.is_active_thread(session_id)), "{msg}: expected active_entry to be Thread({session_id:?}), got {:?}", sidebar.active_entry, );} #[track_caller]fn assert_active_draft(sidebar: &Sidebar, workspace: &Entity<Workspace>, msg: &str) { assert!( matches!(&sidebar.active_entry, Some(ActiveEntry::Draft(ws)) if ws == workspace), "{msg}: expected active_entry to be Draft for workspace {:?}, got {:?}", workspace.entity_id(), sidebar.active_entry, );} fn has_thread_entry(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool { sidebar .contents .entries .iter() .any(|entry| matches!(entry, ListEntry::Thread(t) if &t.metadata.session_id == session_id))} #[track_caller]fn assert_remote_project_integration_sidebar_state( sidebar: &mut Sidebar, main_thread_id: &acp::SessionId, remote_thread_id: &acp::SessionId,) { let mut project_headers = sidebar.contents.entries.iter().filter_map(|entry| { if let ListEntry::ProjectHeader { label, .. } = entry { Some(label.as_ref()) } else { None } }); let Some(project_header) = project_headers.next() else { panic!("expected exactly one sidebar project header named `project`, found none"); }; assert_eq!( project_header, "project", "expected the only sidebar project header to be `project`" ); if let Some(unexpected_header) = project_headers.next() { panic!( "expected exactly one sidebar project header named `project`, found extra header `{unexpected_header}`" ); } let mut saw_main_thread = false; let mut saw_remote_thread = false; for entry in &sidebar.contents.entries { match entry { ListEntry::ProjectHeader { label, .. } => { assert_eq!( label.as_ref(), "project", "expected the only sidebar project header to be `project`" ); } ListEntry::Thread(thread) if &thread.metadata.session_id == main_thread_id => { saw_main_thread = true; } ListEntry::Thread(thread) if &thread.metadata.session_id == remote_thread_id => { saw_remote_thread = true; } ListEntry::Thread(thread) => { let title = thread.metadata.title.as_ref(); panic!( "unexpected sidebar thread while simulating remote project integration flicker: title=`{title}`" ); } ListEntry::ViewMore { .. } => { panic!( "unexpected `View More` entry while simulating remote project integration flicker" ); } ListEntry::DraftThread { .. } => {} } } assert!( saw_main_thread, "expected the sidebar to keep showing `Main Thread` under `project`" ); assert!( saw_remote_thread, "expected the sidebar to keep showing `Worktree Thread` under `project`" );} async fn init_test_project( worktree_path: &str, cx: &mut TestAppContext,) -> Entity<project::Project> { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree(worktree_path, serde_json::json!({ "src": {} })) .await; cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx)); project::Project::test(fs, [worktree_path.as_ref()], cx).await} fn setup_sidebar( multi_workspace: &Entity<MultiWorkspace>, cx: &mut gpui::VisualTestContext,) -> Entity<Sidebar> { let sidebar = setup_sidebar_closed(multi_workspace, cx); multi_workspace.update_in(cx, |mw, window, cx| { mw.toggle_sidebar(window, cx); }); cx.run_until_parked(); sidebar} fn setup_sidebar_closed( multi_workspace: &Entity<MultiWorkspace>, cx: &mut gpui::VisualTestContext,) -> Entity<Sidebar> { let multi_workspace = multi_workspace.clone(); let sidebar = cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); multi_workspace.update(cx, |mw, cx| { mw.register_sidebar(sidebar.clone(), cx); }); cx.run_until_parked(); sidebar} async fn save_n_test_threads( count: u32, project: &Entity<project::Project>, cx: &mut gpui::VisualTestContext,) { for i in 0..count { save_thread_metadata( acp::SessionId::new(Arc::from(format!("thread-{}", i))), format!("Thread {}", i + 1).into(), chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, i).unwrap(), None, project, cx, ) } cx.run_until_parked();} async fn save_test_thread_metadata( session_id: &acp::SessionId, project: &Entity<project::Project>, cx: &mut TestAppContext,) { save_thread_metadata( session_id.clone(), "Test".into(), chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(), None, project, cx, )} async fn save_named_thread_metadata( session_id: &str, title: &str, project: &Entity<project::Project>, cx: &mut gpui::VisualTestContext,) { save_thread_metadata( acp::SessionId::new(Arc::from(session_id)), SharedString::from(title.to_string()), chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(), None, project, cx, ); cx.run_until_parked();} fn save_thread_metadata( session_id: acp::SessionId, title: SharedString, updated_at: DateTime<Utc>, created_at: Option<DateTime<Utc>>, project: &Entity<project::Project>, cx: &mut TestAppContext,) { cx.update(|cx| { let (folder_paths, main_worktree_paths) = { let project_ref = project.read(cx); let paths: Vec<Arc<Path>> = project_ref .visible_worktrees(cx) .map(|worktree| worktree.read(cx).abs_path()) .collect(); let folder_paths = PathList::new(&paths); let main_worktree_paths = project_ref.project_group_key(cx).path_list().clone(); (folder_paths, main_worktree_paths) }; let metadata = ThreadMetadata { session_id, agent_id: agent::ZED_AGENT_ID.clone(), title, updated_at, created_at, folder_paths, main_worktree_paths, archived: false, }; ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save_manually(metadata, cx)); }); cx.run_until_parked();} fn save_thread_metadata_with_main_paths( session_id: &str, title: &str, folder_paths: PathList, main_worktree_paths: PathList, cx: &mut TestAppContext,) { let session_id = acp::SessionId::new(Arc::from(session_id)); let title = SharedString::from(title.to_string()); let updated_at = chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(); let metadata = ThreadMetadata { session_id, agent_id: agent::ZED_AGENT_ID.clone(), title, updated_at, created_at: None, folder_paths, main_worktree_paths, archived: false, }; cx.update(|cx| { ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save_manually(metadata, cx)); }); cx.run_until_parked();} fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) { sidebar.update_in(cx, |_, window, cx| { cx.focus_self(window); }); cx.run_until_parked();} fn request_test_tool_authorization( thread: &Entity<AcpThread>, tool_call_id: &str, option_id: &str, cx: &mut gpui::VisualTestContext,) { let tool_call_id = acp::ToolCallId::new(tool_call_id); let label = format!("Tool {tool_call_id}"); let option_id = acp::PermissionOptionId::new(option_id); let _authorization_task = cx.update(|_, cx| { thread.update(cx, |thread, cx| { thread .request_tool_call_authorization( acp::ToolCall::new(tool_call_id, label) .kind(acp::ToolKind::Edit) .into(), PermissionOptions::Flat(vec![acp::PermissionOption::new( option_id, "Allow", acp::PermissionOptionKind::AllowOnce, )]), cx, ) .unwrap() }) }); cx.run_until_parked();} fn format_linked_worktree_chips(worktrees: &[WorktreeInfo]) -> String { let mut seen = Vec::new(); let mut chips = Vec::new(); for wt in worktrees { if wt.kind == ui::WorktreeKind::Main { continue; } if !seen.contains(&wt.name) { seen.push(wt.name.clone()); chips.push(format!("{{{}}}", wt.name)); } } if chips.is_empty() { String::new() } else { format!(" {}", chips.join(", ")) }} fn visible_entries_as_strings( sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext,) -> Vec<String> { sidebar.read_with(cx, |sidebar, _cx| { sidebar .contents .entries .iter() .enumerate() .map(|(ix, entry)| { let selected = if sidebar.selection == Some(ix) { " <== selected" } else { "" }; let is_active = sidebar .active_entry .as_ref() .is_some_and(|active| active.matches_entry(entry)); let active_indicator = if is_active { " (active)" } else { "" }; match entry { ListEntry::ProjectHeader { label, key, highlight_positions: _, .. } => { let icon = if sidebar.collapsed_groups.contains(key) { ">" } else { "v" }; format!("{} [{}]{}", icon, label, selected) } ListEntry::Thread(thread) => { let title = thread.metadata.title.as_ref(); let live = if thread.is_live { " *" } else { "" }; let status_str = match thread.status { AgentThreadStatus::Running => " (running)", AgentThreadStatus::Error => " (error)", AgentThreadStatus::WaitingForConfirmation => " (waiting)", _ => "", }; let notified = if sidebar .contents .is_thread_notified(&thread.metadata.session_id) { " (!)" } else { "" }; let worktree = format_linked_worktree_chips(&thread.worktrees); format!(" {title}{worktree}{live}{status_str}{notified}{active_indicator}{selected}") } ListEntry::ViewMore { is_fully_expanded, .. } => { if *is_fully_expanded { format!(" - Collapse{}", selected) } else { format!(" + View More{}", selected) } } ListEntry::DraftThread { workspace, worktrees, .. } => { let worktree = format_linked_worktree_chips(worktrees); if workspace.is_some() { format!(" [+ New Thread{}]{}", worktree, selected) } else { format!(" [~ Draft{}]{}{}", worktree, active_indicator, selected) } } } }) .collect() })} #[gpui::test]async fn test_serialization_round_trip(cx: &mut TestAppContext) { let project = init_test_project("/my-project", cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let sidebar = setup_sidebar(&multi_workspace, cx); save_n_test_threads(3, &project, cx).await; let project_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx)); // Set a custom width, collapse the group, and expand "View More". sidebar.update_in(cx, |sidebar, window, cx| { sidebar.set_width(Some(px(420.0)), cx); sidebar.toggle_collapse(&project_group_key, window, cx); sidebar.expanded_groups.insert(project_group_key.clone(), 2); }); cx.run_until_parked(); // Capture the serialized state from the first sidebar. let serialized = sidebar.read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)); let serialized = serialized.expect("serialized_state should return Some"); // Create a fresh sidebar and restore into it. let sidebar2 = cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); cx.run_until_parked(); sidebar2.update_in(cx, |sidebar, window, cx| { sidebar.restore_serialized_state(&serialized, window, cx); }); cx.run_until_parked(); // Assert all serialized fields match. let (width1, collapsed1, expanded1) = sidebar.read_with(cx, |s, _| { ( s.width, s.collapsed_groups.clone(), s.expanded_groups.clone(), ) }); let (width2, collapsed2, expanded2) = sidebar2.read_with(cx, |s, _| { ( s.width, s.collapsed_groups.clone(), s.expanded_groups.clone(), ) }); assert_eq!(width1, width2); assert_eq!(collapsed1, collapsed2); assert_eq!(expanded1, expanded2); assert_eq!(width1, px(420.0)); assert!(collapsed1.contains(&project_group_key)); assert_eq!(expanded1.get(&project_group_key), Some(&2));} #[gpui::test]async fn test_restore_serialized_archive_view_does_not_panic(cx: &mut TestAppContext) { // A regression test to ensure that restoring a serialized archive view does not panic. let project = init_test_project_with_agent_panel("/my-project", cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let (sidebar, _panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx); cx.update(|_window, cx| { AgentRegistryStore::init_test_global(cx, vec![]); }); let serialized = serde_json::to_string(&SerializedSidebar { width: Some(400.0), collapsed_groups: Vec::new(), expanded_groups: Vec::new(), active_view: SerializedSidebarView::Archive, }) .expect("serialization should succeed"); multi_workspace.update_in(cx, |multi_workspace, window, cx| { if let Some(sidebar) = multi_workspace.sidebar() { sidebar.restore_serialized_state(&serialized, window, cx); } }); cx.run_until_parked(); // After the deferred `show_archive` runs, the view should be Archive.Lines 1–500 of 8186. Load more
use super::*; use acp_thread::{AcpThread, PermissionOptions, StubAgentConnection}; use agent::ThreadStore; use agent_ui::{ test_support::{active_session_id, open_thread_with_connection, send_message}, thread_metadata_store::ThreadMetadata, }; use chrono::DateTime; use fs::{FakeFs, Fs}; use gpui::TestAppContext; use pretty_assertions::assert_eq; use project::AgentId; use settings::SettingsStore; use std::{ path::{Path, PathBuf}, sync::Arc, }; use util::path_list::PathList; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); cx.set_global(settings_store); theme_settings::init(theme::LoadThemes::JustBase, cx); editor::init(cx); ThreadStore::init_global(cx); ThreadMetadataStore::init_global(cx); language_model::LanguageModelRegistry::test(cx); prompt_store::init(cx); }); } #[track_caller] fn assert_active_thread(sidebar: &Sidebar, session_id: &acp::SessionId, msg: &str) { assert!( sidebar .active_entry .as_ref() .is_some_and(|e| e.is_active_thread(session_id)), "{msg}: expected active_entry to be Thread({session_id:?}), got {:?}", sidebar.active_entry, ); } #[track_caller] fn assert_active_draft(sidebar: &Sidebar, workspace: &Entity<Workspace>, msg: &str) { assert!( matches!(&sidebar.active_entry, Some(ActiveEntry::Draft(ws)) if ws == workspace), "{msg}: expected active_entry to be Draft for workspace {:?}, got {:?}", workspace.entity_id(), sidebar.active_entry, ); } fn has_thread_entry(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool { sidebar .contents .entries .iter() .any(|entry| matches!(entry, ListEntry::Thread(t) if &t.metadata.session_id == session_id)) } #[track_caller] fn assert_remote_project_integration_sidebar_state( sidebar: &mut Sidebar, main_thread_id: &acp::SessionId, remote_thread_id: &acp::SessionId, ) { let mut project_headers = sidebar.contents.entries.iter().filter_map(|entry| { if let ListEntry::ProjectHeader { label, .. } = entry { Some(label.as_ref()) } else { None } }); let Some(project_header) = project_headers.next() else { panic!("expected exactly one sidebar project header named `project`, found none"); }; assert_eq!( project_header, "project", "expected the only sidebar project header to be `project`" ); if let Some(unexpected_header) = project_headers.next() { panic!( "expected exactly one sidebar project header named `project`, found extra header `{unexpected_header}`" ); } let mut saw_main_thread = false; let mut saw_remote_thread = false; for entry in &sidebar.contents.entries { match entry { ListEntry::ProjectHeader { label, .. } => { assert_eq!( label.as_ref(), "project", "expected the only sidebar project header to be `project`" ); } ListEntry::Thread(thread) if &thread.metadata.session_id == main_thread_id => { saw_main_thread = true; } ListEntry::Thread(thread) if &thread.metadata.session_id == remote_thread_id => { saw_remote_thread = true; } ListEntry::Thread(thread) => { let title = thread.metadata.title.as_ref(); panic!( "unexpected sidebar thread while simulating remote project integration flicker: title=`{title}`" ); } ListEntry::ViewMore { .. } => { panic!( "unexpected `View More` entry while simulating remote project integration flicker" ); } ListEntry::DraftThread { .. } => {} } } assert!( saw_main_thread, "expected the sidebar to keep showing `Main Thread` under `project`" ); assert!( saw_remote_thread, "expected the sidebar to keep showing `Worktree Thread` under `project`" ); } async fn init_test_project( worktree_path: &str, cx: &mut TestAppContext, ) -> Entity<project::Project> { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree(worktree_path, serde_json::json!({ "src": {} })) .await; cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx)); project::Project::test(fs, [worktree_path.as_ref()], cx).await } fn setup_sidebar( multi_workspace: &Entity<MultiWorkspace>, cx: &mut gpui::VisualTestContext, ) -> Entity<Sidebar> { let sidebar = setup_sidebar_closed(multi_workspace, cx); multi_workspace.update_in(cx, |mw, window, cx| { mw.toggle_sidebar(window, cx); }); cx.run_until_parked(); sidebar } fn setup_sidebar_closed( multi_workspace: &Entity<MultiWorkspace>, cx: &mut gpui::VisualTestContext, ) -> Entity<Sidebar> { let multi_workspace = multi_workspace.clone(); let sidebar = cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); multi_workspace.update(cx, |mw, cx| { mw.register_sidebar(sidebar.clone(), cx); }); cx.run_until_parked(); sidebar } async fn save_n_test_threads( count: u32, project: &Entity<project::Project>, cx: &mut gpui::VisualTestContext, ) { for i in 0..count { save_thread_metadata( acp::SessionId::new(Arc::from(format!("thread-{}", i))), format!("Thread {}", i + 1).into(), chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, i).unwrap(), None, project, cx, ) } cx.run_until_parked(); } async fn save_test_thread_metadata( session_id: &acp::SessionId, project: &Entity<project::Project>, cx: &mut TestAppContext, ) { save_thread_metadata( session_id.clone(), "Test".into(), chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(), None, project, cx, ) } async fn save_named_thread_metadata( session_id: &str, title: &str, project: &Entity<project::Project>, cx: &mut gpui::VisualTestContext, ) { save_thread_metadata( acp::SessionId::new(Arc::from(session_id)), SharedString::from(title.to_string()), chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(), None, project, cx, ); cx.run_until_parked(); } fn save_thread_metadata( session_id: acp::SessionId, title: SharedString, updated_at: DateTime<Utc>, created_at: Option<DateTime<Utc>>, project: &Entity<project::Project>, cx: &mut TestAppContext, ) { cx.update(|cx| { let (folder_paths, main_worktree_paths) = { let project_ref = project.read(cx); let paths: Vec<Arc<Path>> = project_ref .visible_worktrees(cx) .map(|worktree| worktree.read(cx).abs_path()) .collect(); let folder_paths = PathList::new(&paths); let main_worktree_paths = project_ref.project_group_key(cx).path_list().clone(); (folder_paths, main_worktree_paths) }; let metadata = ThreadMetadata { session_id, agent_id: agent::ZED_AGENT_ID.clone(), title, updated_at, created_at, folder_paths, main_worktree_paths, archived: false, }; ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save_manually(metadata, cx)); }); cx.run_until_parked(); } fn save_thread_metadata_with_main_paths( session_id: &str, title: &str, folder_paths: PathList, main_worktree_paths: PathList, cx: &mut TestAppContext, ) { let session_id = acp::SessionId::new(Arc::from(session_id)); let title = SharedString::from(title.to_string()); let updated_at = chrono::TimeZone::with_ymd_and_hms(&Utc, 2024, 1, 1, 0, 0, 0).unwrap(); let metadata = ThreadMetadata { session_id, agent_id: agent::ZED_AGENT_ID.clone(), title, updated_at, created_at: None, folder_paths, main_worktree_paths, archived: false, }; cx.update(|cx| { ThreadMetadataStore::global(cx).update(cx, |store, cx| store.save_manually(metadata, cx)); }); cx.run_until_parked(); } fn focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) { sidebar.update_in(cx, |_, window, cx| { cx.focus_self(window); }); cx.run_until_parked(); } fn request_test_tool_authorization( thread: &Entity<AcpThread>, tool_call_id: &str, option_id: &str, cx: &mut gpui::VisualTestContext, ) { let tool_call_id = acp::ToolCallId::new(tool_call_id); let label = format!("Tool {tool_call_id}"); let option_id = acp::PermissionOptionId::new(option_id); let _authorization_task = cx.update(|_, cx| { thread.update(cx, |thread, cx| { thread .request_tool_call_authorization( acp::ToolCall::new(tool_call_id, label) .kind(acp::ToolKind::Edit) .into(), PermissionOptions::Flat(vec![acp::PermissionOption::new( option_id, "Allow", acp::PermissionOptionKind::AllowOnce, )]), cx, ) .unwrap() }) }); cx.run_until_parked(); } fn format_linked_worktree_chips(worktrees: &[WorktreeInfo]) -> String { let mut seen = Vec::new(); let mut chips = Vec::new(); for wt in worktrees { if wt.kind == ui::WorktreeKind::Main { continue; } if !seen.contains(&wt.name) { seen.push(wt.name.clone()); chips.push(format!("{{{}}}", wt.name)); } } if chips.is_empty() { String::new() } else { format!(" {}", chips.join(", ")) } } fn visible_entries_as_strings( sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext, ) -> Vec<String> { sidebar.read_with(cx, |sidebar, _cx| { sidebar .contents .entries .iter() .enumerate() .map(|(ix, entry)| { let selected = if sidebar.selection == Some(ix) { " <== selected" } else { "" }; let is_active = sidebar .active_entry .as_ref() .is_some_and(|active| active.matches_entry(entry)); let active_indicator = if is_active { " (active)" } else { "" }; match entry { ListEntry::ProjectHeader { label, key, highlight_positions: _, .. } => { let icon = if sidebar.collapsed_groups.contains(key) { ">" } else { "v" }; format!("{} [{}]{}", icon, label, selected) } ListEntry::Thread(thread) => { let title = thread.metadata.title.as_ref(); let live = if thread.is_live { " *" } else { "" }; let status_str = match thread.status { AgentThreadStatus::Running => " (running)", AgentThreadStatus::Error => " (error)", AgentThreadStatus::WaitingForConfirmation => " (waiting)", _ => "", }; let notified = if sidebar .contents .is_thread_notified(&thread.metadata.session_id) { " (!)" } else { "" }; let worktree = format_linked_worktree_chips(&thread.worktrees); format!(" {title}{worktree}{live}{status_str}{notified}{active_indicator}{selected}") } ListEntry::ViewMore { is_fully_expanded, .. } => { if *is_fully_expanded { format!(" - Collapse{}", selected) } else { format!(" + View More{}", selected) } } ListEntry::DraftThread { workspace, worktrees, .. } => { let worktree = format_linked_worktree_chips(worktrees); if workspace.is_some() { format!(" [+ New Thread{}]{}", worktree, selected) } else { format!(" [~ Draft{}]{}{}", worktree, active_indicator, selected) } } } }) .collect() }) } #[gpui::test] async fn test_serialization_round_trip(cx: &mut TestAppContext) { let project = init_test_project("/my-project", cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let sidebar = setup_sidebar(&multi_workspace, cx); save_n_test_threads(3, &project, cx).await; let project_group_key = project.read_with(cx, |project, cx| project.project_group_key(cx)); // Set a custom width, collapse the group, and expand "View More". sidebar.update_in(cx, |sidebar, window, cx| { sidebar.set_width(Some(px(420.0)), cx); sidebar.toggle_collapse(&project_group_key, window, cx); sidebar.expanded_groups.insert(project_group_key.clone(), 2); }); cx.run_until_parked(); // Capture the serialized state from the first sidebar. let serialized = sidebar.read_with(cx, |sidebar, cx| sidebar.serialized_state(cx)); let serialized = serialized.expect("serialized_state should return Some"); // Create a fresh sidebar and restore into it. let sidebar2 = cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx))); cx.run_until_parked(); sidebar2.update_in(cx, |sidebar, window, cx| { sidebar.restore_serialized_state(&serialized, window, cx); }); cx.run_until_parked(); // Assert all serialized fields match. let (width1, collapsed1, expanded1) = sidebar.read_with(cx, |s, _| { ( s.width, s.collapsed_groups.clone(), s.expanded_groups.clone(), ) }); let (width2, collapsed2, expanded2) = sidebar2.read_with(cx, |s, _| { ( s.width, s.collapsed_groups.clone(), s.expanded_groups.clone(), ) }); assert_eq!(width1, width2); assert_eq!(collapsed1, collapsed2); assert_eq!(expanded1, expanded2); assert_eq!(width1, px(420.0)); assert!(collapsed1.contains(&project_group_key)); assert_eq!(expanded1.get(&project_group_key), Some(&2)); } #[gpui::test] async fn test_restore_serialized_archive_view_does_not_panic(cx: &mut TestAppContext) { // A regression test to ensure that restoring a serialized archive view does not panic. let project = init_test_project_with_agent_panel("/my-project", cx).await; let (multi_workspace, cx) = cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); let (sidebar, _panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx); cx.update(|_window, cx| { AgentRegistryStore::init_test_global(cx, vec![]); }); let serialized = serde_json::to_string(&SerializedSidebar { width: Some(400.0), collapsed_groups: Vec::new(), expanded_groups: Vec::new(), active_view: SerializedSidebarView::Archive, }) .expect("serialization should succeed"); multi_workspace.update_in(cx, |multi_workspace, window, cx| { if let Some(sidebar) = multi_workspace.sidebar() { sidebar.restore_serialized_state(&serialized, window, cx); } }); cx.run_until_parked(); // After the deferred `show_archive` runs, the view should be Archive.