1use super::*;
2use feature_flags::FeatureFlagAppExt;
3use fs::FakeFs;
4use gpui::TestAppContext;
5use project::{DisableAiSettings, ProjectGroupKey};
6use serde_json::json;
7use settings::SettingsStore;
8
9fn init_test(cx: &mut TestAppContext) {
10 cx.update(|cx| {
11 let settings_store = SettingsStore::test(cx);
12 cx.set_global(settings_store);
13 theme_settings::init(theme::LoadThemes::JustBase, cx);
14 DisableAiSettings::register(cx);
15 cx.update_flags(false, vec!["agent-v2".into()]);
16 });
17}
18
19#[gpui::test]
20async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
21 init_test(cx);
22 let fs = FakeFs::new(cx.executor());
23 let project = Project::test(fs, [], cx).await;
24
25 let (multi_workspace, cx) =
26 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
27
28 multi_workspace.read_with(cx, |mw, cx| {
29 assert!(mw.multi_workspace_enabled(cx));
30 });
31
32 multi_workspace.update_in(cx, |mw, _window, cx| {
33 mw.open_sidebar(cx);
34 assert!(mw.sidebar_open());
35 });
36
37 cx.update(|_window, cx| {
38 DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
39 });
40 cx.run_until_parked();
41
42 multi_workspace.read_with(cx, |mw, cx| {
43 assert!(
44 !mw.sidebar_open(),
45 "Sidebar should be closed when disable_ai is true"
46 );
47 assert!(
48 !mw.multi_workspace_enabled(cx),
49 "Multi-workspace should be disabled when disable_ai is true"
50 );
51 });
52
53 multi_workspace.update_in(cx, |mw, window, cx| {
54 mw.toggle_sidebar(window, cx);
55 });
56 multi_workspace.read_with(cx, |mw, _cx| {
57 assert!(
58 !mw.sidebar_open(),
59 "Sidebar should remain closed when toggled with disable_ai true"
60 );
61 });
62
63 cx.update(|_window, cx| {
64 DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
65 });
66 cx.run_until_parked();
67
68 multi_workspace.read_with(cx, |mw, cx| {
69 assert!(
70 mw.multi_workspace_enabled(cx),
71 "Multi-workspace should be enabled after re-enabling AI"
72 );
73 assert!(
74 !mw.sidebar_open(),
75 "Sidebar should still be closed after re-enabling AI (not auto-opened)"
76 );
77 });
78
79 multi_workspace.update_in(cx, |mw, window, cx| {
80 mw.toggle_sidebar(window, cx);
81 });
82 multi_workspace.read_with(cx, |mw, _cx| {
83 assert!(
84 mw.sidebar_open(),
85 "Sidebar should open when toggled after re-enabling AI"
86 );
87 });
88}
89
90#[gpui::test]
91async fn test_project_group_keys_initial(cx: &mut TestAppContext) {
92 init_test(cx);
93 let fs = FakeFs::new(cx.executor());
94 fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
95 let project = Project::test(fs, ["/root_a".as_ref()], cx).await;
96
97 let expected_key = project.read_with(cx, |project, cx| project.project_group_key(cx));
98
99 let (multi_workspace, cx) =
100 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
101
102 multi_workspace.update(cx, |mw, cx| {
103 mw.open_sidebar(cx);
104 });
105
106 multi_workspace.read_with(cx, |mw, _cx| {
107 let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
108 assert_eq!(keys.len(), 1, "should have exactly one key on creation");
109 assert_eq!(*keys[0], expected_key);
110 });
111}
112
113#[gpui::test]
114async fn test_project_group_keys_add_workspace(cx: &mut TestAppContext) {
115 init_test(cx);
116 let fs = FakeFs::new(cx.executor());
117 fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
118 fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
119 let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
120 let project_b = Project::test(fs.clone(), ["/root_b".as_ref()], cx).await;
121
122 let key_a = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
123 let key_b = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
124 assert_ne!(
125 key_a, key_b,
126 "different roots should produce different keys"
127 );
128
129 let (multi_workspace, cx) =
130 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
131
132 multi_workspace.update(cx, |mw, cx| {
133 mw.open_sidebar(cx);
134 });
135
136 multi_workspace.read_with(cx, |mw, _cx| {
137 assert_eq!(mw.project_group_keys().count(), 1);
138 });
139
140 // Adding a workspace with a different project root adds a new key.
141 multi_workspace.update_in(cx, |mw, window, cx| {
142 mw.test_add_workspace(project_b, window, cx);
143 });
144
145 multi_workspace.read_with(cx, |mw, _cx| {
146 let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
147 assert_eq!(
148 keys.len(),
149 2,
150 "should have two keys after adding a second workspace"
151 );
152 assert_eq!(*keys[0], key_a);
153 assert_eq!(*keys[1], key_b);
154 });
155}
156
157#[gpui::test]
158async fn test_project_group_keys_duplicate_not_added(cx: &mut TestAppContext) {
159 init_test(cx);
160 let fs = FakeFs::new(cx.executor());
161 fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
162 let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
163 // A second project entity pointing at the same path produces the same key.
164 let project_a2 = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
165
166 let key_a = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
167 let key_a2 = project_a2.read_with(cx, |p, cx| p.project_group_key(cx));
168 assert_eq!(key_a, key_a2, "same root path should produce the same key");
169
170 let (multi_workspace, cx) =
171 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a, window, cx));
172
173 multi_workspace.update(cx, |mw, cx| {
174 mw.open_sidebar(cx);
175 });
176
177 multi_workspace.update_in(cx, |mw, window, cx| {
178 mw.test_add_workspace(project_a2, window, cx);
179 });
180
181 multi_workspace.read_with(cx, |mw, _cx| {
182 let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
183 assert_eq!(
184 keys.len(),
185 1,
186 "duplicate key should not be added when a workspace with the same root is inserted"
187 );
188 });
189}
190
191#[gpui::test]
192async fn test_project_group_keys_on_worktree_added(cx: &mut TestAppContext) {
193 init_test(cx);
194 let fs = FakeFs::new(cx.executor());
195 fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
196 fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
197 let project = Project::test(fs, ["/root_a".as_ref()], cx).await;
198
199 let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
200
201 let (multi_workspace, cx) =
202 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
203
204 multi_workspace.update(cx, |mw, cx| {
205 mw.open_sidebar(cx);
206 });
207
208 // Add a second worktree to the same project.
209 let (worktree, _) = project
210 .update(cx, |project, cx| {
211 project.find_or_create_worktree("/root_b", true, cx)
212 })
213 .await
214 .unwrap();
215 worktree
216 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
217 .await;
218 cx.run_until_parked();
219
220 let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
221 assert_ne!(
222 initial_key, updated_key,
223 "key should change after adding a worktree"
224 );
225
226 multi_workspace.read_with(cx, |mw, _cx| {
227 let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
228 assert_eq!(
229 keys.len(),
230 2,
231 "should have both the original and updated key"
232 );
233 assert_eq!(*keys[0], initial_key);
234 assert_eq!(*keys[1], updated_key);
235 });
236}
237
238#[gpui::test]
239async fn test_project_group_keys_on_worktree_removed(cx: &mut TestAppContext) {
240 init_test(cx);
241 let fs = FakeFs::new(cx.executor());
242 fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
243 fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
244 let project = Project::test(fs, ["/root_a".as_ref(), "/root_b".as_ref()], cx).await;
245
246 let initial_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
247
248 let (multi_workspace, cx) =
249 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
250
251 multi_workspace.update(cx, |mw, cx| {
252 mw.open_sidebar(cx);
253 });
254
255 // Remove one worktree.
256 let worktree_b_id = project.read_with(cx, |project, cx| {
257 project
258 .worktrees(cx)
259 .find(|wt| wt.read(cx).root_name().as_unix_str() == "root_b")
260 .unwrap()
261 .read(cx)
262 .id()
263 });
264 project.update(cx, |project, cx| {
265 project.remove_worktree(worktree_b_id, cx);
266 });
267 cx.run_until_parked();
268
269 let updated_key = project.read_with(cx, |p, cx| p.project_group_key(cx));
270 assert_ne!(
271 initial_key, updated_key,
272 "key should change after removing a worktree"
273 );
274
275 multi_workspace.read_with(cx, |mw, _cx| {
276 let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
277 assert_eq!(
278 keys.len(),
279 2,
280 "should accumulate both the original and post-removal key"
281 );
282 assert_eq!(*keys[0], initial_key);
283 assert_eq!(*keys[1], updated_key);
284 });
285}
286
287#[gpui::test]
288async fn test_project_group_keys_across_multiple_workspaces_and_worktree_changes(
289 cx: &mut TestAppContext,
290) {
291 init_test(cx);
292 let fs = FakeFs::new(cx.executor());
293 fs.insert_tree("/root_a", json!({ "file.txt": "" })).await;
294 fs.insert_tree("/root_b", json!({ "file.txt": "" })).await;
295 fs.insert_tree("/root_c", json!({ "file.txt": "" })).await;
296 let project_a = Project::test(fs.clone(), ["/root_a".as_ref()], cx).await;
297 let project_b = Project::test(fs.clone(), ["/root_b".as_ref()], cx).await;
298
299 let key_a = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
300 let key_b = project_b.read_with(cx, |p, cx| p.project_group_key(cx));
301
302 let (multi_workspace, cx) =
303 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
304
305 multi_workspace.update(cx, |mw, cx| {
306 mw.open_sidebar(cx);
307 });
308
309 multi_workspace.update_in(cx, |mw, window, cx| {
310 mw.test_add_workspace(project_b, window, cx);
311 });
312
313 multi_workspace.read_with(cx, |mw, _cx| {
314 assert_eq!(mw.project_group_keys().count(), 2);
315 });
316
317 // Now add a worktree to project_a. This should produce a third key.
318 let (worktree, _) = project_a
319 .update(cx, |project, cx| {
320 project.find_or_create_worktree("/root_c", true, cx)
321 })
322 .await
323 .unwrap();
324 worktree
325 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
326 .await;
327 cx.run_until_parked();
328
329 let key_a_updated = project_a.read_with(cx, |p, cx| p.project_group_key(cx));
330 assert_ne!(key_a, key_a_updated);
331
332 multi_workspace.read_with(cx, |mw, _cx| {
333 let keys: Vec<&ProjectGroupKey> = mw.project_group_keys().collect();
334 assert_eq!(
335 keys.len(),
336 3,
337 "should have key_a, key_b, and the updated key_a with root_c"
338 );
339 assert_eq!(*keys[0], key_a);
340 assert_eq!(*keys[1], key_b);
341 assert_eq!(*keys[2], key_a_updated);
342 });
343}