1use anyhow::Result;
2use context_server::test::create_fake_transport;
3use context_server::{ContextServer, ContextServerId};
4use gpui::{AppContext, AsyncApp, Entity, Subscription, Task, TestAppContext, UpdateGlobal as _};
5use http_client::{FakeHttpClient, Response};
6use project::context_server_store::registry::ContextServerDescriptorRegistry;
7use project::context_server_store::*;
8use project::project_settings::ContextServerSettings;
9use project::worktree_store::WorktreeStore;
10use project::{
11 FakeFs, Project, context_server_store::registry::ContextServerDescriptor,
12 project_settings::ProjectSettings,
13};
14use serde_json::json;
15use settings::{ContextServerCommand, Settings, SettingsStore};
16use std::sync::Arc;
17use std::{cell::RefCell, path::PathBuf, rc::Rc};
18use util::path;
19
20#[gpui::test]
21async fn test_context_server_status(cx: &mut TestAppContext) {
22 const SERVER_1_ID: &str = "mcp-1";
23 const SERVER_2_ID: &str = "mcp-2";
24
25 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
26
27 let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
28 let store = cx.new(|cx| {
29 ContextServerStore::test(
30 registry.clone(),
31 project.read(cx).worktree_store(),
32 Some(project.downgrade()),
33 cx,
34 )
35 });
36
37 let server_1_id = ContextServerId(SERVER_1_ID.into());
38 let server_2_id = ContextServerId(SERVER_2_ID.into());
39
40 let server_1 = Arc::new(ContextServer::new(
41 server_1_id.clone(),
42 Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())),
43 ));
44 let server_2 = Arc::new(ContextServer::new(
45 server_2_id.clone(),
46 Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())),
47 ));
48
49 store.update(cx, |store, cx| store.test_start_server(server_1, cx));
50
51 cx.run_until_parked();
52
53 cx.update(|cx| {
54 assert_eq!(
55 store.read(cx).status_for_server(&server_1_id),
56 Some(ContextServerStatus::Running)
57 );
58 assert_eq!(store.read(cx).status_for_server(&server_2_id), None);
59 });
60
61 store.update(cx, |store, cx| {
62 store.test_start_server(server_2.clone(), cx)
63 });
64
65 cx.run_until_parked();
66
67 cx.update(|cx| {
68 assert_eq!(
69 store.read(cx).status_for_server(&server_1_id),
70 Some(ContextServerStatus::Running)
71 );
72 assert_eq!(
73 store.read(cx).status_for_server(&server_2_id),
74 Some(ContextServerStatus::Running)
75 );
76 });
77
78 store
79 .update(cx, |store, cx| store.stop_server(&server_2_id, cx))
80 .unwrap();
81
82 cx.update(|cx| {
83 assert_eq!(
84 store.read(cx).status_for_server(&server_1_id),
85 Some(ContextServerStatus::Running)
86 );
87 assert_eq!(
88 store.read(cx).status_for_server(&server_2_id),
89 Some(ContextServerStatus::Stopped)
90 );
91 });
92}
93
94#[gpui::test]
95async fn test_context_server_status_events(cx: &mut TestAppContext) {
96 const SERVER_1_ID: &str = "mcp-1";
97 const SERVER_2_ID: &str = "mcp-2";
98
99 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
100
101 let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
102 let store = cx.new(|cx| {
103 ContextServerStore::test(
104 registry.clone(),
105 project.read(cx).worktree_store(),
106 Some(project.downgrade()),
107 cx,
108 )
109 });
110
111 let server_1_id = ContextServerId(SERVER_1_ID.into());
112 let server_2_id = ContextServerId(SERVER_2_ID.into());
113
114 let server_1 = Arc::new(ContextServer::new(
115 server_1_id.clone(),
116 Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())),
117 ));
118 let server_2 = Arc::new(ContextServer::new(
119 server_2_id.clone(),
120 Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())),
121 ));
122
123 let _server_events = assert_server_events(
124 &store,
125 vec![
126 (server_1_id.clone(), ContextServerStatus::Starting),
127 (server_1_id, ContextServerStatus::Running),
128 (server_2_id.clone(), ContextServerStatus::Starting),
129 (server_2_id.clone(), ContextServerStatus::Running),
130 (server_2_id.clone(), ContextServerStatus::Stopped),
131 ],
132 cx,
133 );
134
135 store.update(cx, |store, cx| store.test_start_server(server_1, cx));
136
137 cx.run_until_parked();
138
139 store.update(cx, |store, cx| {
140 store.test_start_server(server_2.clone(), cx)
141 });
142
143 cx.run_until_parked();
144
145 store
146 .update(cx, |store, cx| store.stop_server(&server_2_id, cx))
147 .unwrap();
148}
149
150#[gpui::test(iterations = 25)]
151async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) {
152 const SERVER_1_ID: &str = "mcp-1";
153
154 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
155
156 let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
157 let store = cx.new(|cx| {
158 ContextServerStore::test(
159 registry.clone(),
160 project.read(cx).worktree_store(),
161 Some(project.downgrade()),
162 cx,
163 )
164 });
165
166 let server_id = ContextServerId(SERVER_1_ID.into());
167
168 let server_with_same_id_1 = Arc::new(ContextServer::new(
169 server_id.clone(),
170 Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())),
171 ));
172 let server_with_same_id_2 = Arc::new(ContextServer::new(
173 server_id.clone(),
174 Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())),
175 ));
176
177 // If we start another server with the same id, we should report that we stopped the previous one
178 let _server_events = assert_server_events(
179 &store,
180 vec![
181 (server_id.clone(), ContextServerStatus::Starting),
182 (server_id.clone(), ContextServerStatus::Stopped),
183 (server_id.clone(), ContextServerStatus::Starting),
184 (server_id.clone(), ContextServerStatus::Running),
185 ],
186 cx,
187 );
188
189 store.update(cx, |store, cx| {
190 store.test_start_server(server_with_same_id_1.clone(), cx)
191 });
192 store.update(cx, |store, cx| {
193 store.test_start_server(server_with_same_id_2.clone(), cx)
194 });
195
196 cx.run_until_parked();
197
198 cx.update(|cx| {
199 assert_eq!(
200 store.read(cx).status_for_server(&server_id),
201 Some(ContextServerStatus::Running)
202 );
203 });
204}
205
206#[gpui::test]
207async fn test_context_server_maintain_servers_loop(cx: &mut TestAppContext) {
208 const SERVER_1_ID: &str = "mcp-1";
209 const SERVER_2_ID: &str = "mcp-2";
210
211 let server_1_id = ContextServerId(SERVER_1_ID.into());
212 let server_2_id = ContextServerId(SERVER_2_ID.into());
213
214 let fake_descriptor_1 = Arc::new(FakeContextServerDescriptor::new(SERVER_1_ID));
215
216 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
217
218 let executor = cx.executor();
219 let store = project.read_with(cx, |project, _| project.context_server_store());
220 store.update(cx, |store, cx| {
221 store.set_context_server_factory(Box::new(move |id, _| {
222 Arc::new(ContextServer::new(
223 id.clone(),
224 Arc::new(create_fake_transport(id.0.to_string(), executor.clone())),
225 ))
226 }));
227 store.registry().update(cx, |registry, cx| {
228 registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1, cx);
229 });
230 });
231
232 set_context_server_configuration(
233 vec![(
234 server_1_id.0.clone(),
235 settings::ContextServerSettingsContent::Extension {
236 enabled: true,
237 remote: false,
238 settings: json!({
239 "somevalue": true
240 }),
241 },
242 )],
243 cx,
244 );
245
246 // Ensure that mcp-1 starts up
247 {
248 let _server_events = assert_server_events(
249 &store,
250 vec![
251 (server_1_id.clone(), ContextServerStatus::Starting),
252 (server_1_id.clone(), ContextServerStatus::Running),
253 ],
254 cx,
255 );
256 cx.run_until_parked();
257 }
258
259 // Ensure that mcp-1 is restarted when the configuration was changed
260 {
261 let _server_events = assert_server_events(
262 &store,
263 vec![
264 (server_1_id.clone(), ContextServerStatus::Stopped),
265 (server_1_id.clone(), ContextServerStatus::Starting),
266 (server_1_id.clone(), ContextServerStatus::Running),
267 ],
268 cx,
269 );
270 set_context_server_configuration(
271 vec![(
272 server_1_id.0.clone(),
273 settings::ContextServerSettingsContent::Extension {
274 enabled: true,
275 remote: false,
276 settings: json!({
277 "somevalue": false
278 }),
279 },
280 )],
281 cx,
282 );
283
284 cx.run_until_parked();
285 }
286
287 // Ensure that mcp-1 is not restarted when the configuration was not changed
288 {
289 let _server_events = assert_server_events(&store, vec![], cx);
290 set_context_server_configuration(
291 vec![(
292 server_1_id.0.clone(),
293 settings::ContextServerSettingsContent::Extension {
294 enabled: true,
295 remote: false,
296 settings: json!({
297 "somevalue": false
298 }),
299 },
300 )],
301 cx,
302 );
303
304 cx.run_until_parked();
305 }
306
307 // Ensure that mcp-2 is started once it is added to the settings
308 {
309 let _server_events = assert_server_events(
310 &store,
311 vec![
312 (server_2_id.clone(), ContextServerStatus::Starting),
313 (server_2_id.clone(), ContextServerStatus::Running),
314 ],
315 cx,
316 );
317 set_context_server_configuration(
318 vec![
319 (
320 server_1_id.0.clone(),
321 settings::ContextServerSettingsContent::Extension {
322 enabled: true,
323 remote: false,
324 settings: json!({
325 "somevalue": false
326 }),
327 },
328 ),
329 (
330 server_2_id.0.clone(),
331 settings::ContextServerSettingsContent::Stdio {
332 enabled: true,
333 remote: false,
334 command: ContextServerCommand {
335 path: "somebinary".into(),
336 args: vec!["arg".to_string()],
337 env: None,
338 timeout: None,
339 },
340 },
341 ),
342 ],
343 cx,
344 );
345
346 cx.run_until_parked();
347 }
348
349 // Ensure that mcp-2 is restarted once the args have changed
350 {
351 let _server_events = assert_server_events(
352 &store,
353 vec![
354 (server_2_id.clone(), ContextServerStatus::Stopped),
355 (server_2_id.clone(), ContextServerStatus::Starting),
356 (server_2_id.clone(), ContextServerStatus::Running),
357 ],
358 cx,
359 );
360 set_context_server_configuration(
361 vec![
362 (
363 server_1_id.0.clone(),
364 settings::ContextServerSettingsContent::Extension {
365 enabled: true,
366 remote: false,
367 settings: json!({
368 "somevalue": false
369 }),
370 },
371 ),
372 (
373 server_2_id.0.clone(),
374 settings::ContextServerSettingsContent::Stdio {
375 enabled: true,
376 remote: false,
377 command: ContextServerCommand {
378 path: "somebinary".into(),
379 args: vec!["anotherArg".to_string()],
380 env: None,
381 timeout: None,
382 },
383 },
384 ),
385 ],
386 cx,
387 );
388
389 cx.run_until_parked();
390 }
391
392 // Ensure that mcp-2 is removed once it is removed from the settings
393 {
394 let _server_events = assert_server_events(
395 &store,
396 vec![(server_2_id.clone(), ContextServerStatus::Stopped)],
397 cx,
398 );
399 set_context_server_configuration(
400 vec![(
401 server_1_id.0.clone(),
402 settings::ContextServerSettingsContent::Extension {
403 enabled: true,
404 remote: false,
405 settings: json!({
406 "somevalue": false
407 }),
408 },
409 )],
410 cx,
411 );
412
413 cx.run_until_parked();
414
415 cx.update(|cx| {
416 assert_eq!(store.read(cx).status_for_server(&server_2_id), None);
417 });
418 }
419
420 // Ensure that nothing happens if the settings do not change
421 {
422 let _server_events = assert_server_events(&store, vec![], cx);
423 set_context_server_configuration(
424 vec![(
425 server_1_id.0.clone(),
426 settings::ContextServerSettingsContent::Extension {
427 enabled: true,
428 remote: false,
429 settings: json!({
430 "somevalue": false
431 }),
432 },
433 )],
434 cx,
435 );
436
437 cx.run_until_parked();
438
439 cx.update(|cx| {
440 assert_eq!(
441 store.read(cx).status_for_server(&server_1_id),
442 Some(ContextServerStatus::Running)
443 );
444 assert_eq!(store.read(cx).status_for_server(&server_2_id), None);
445 });
446 }
447}
448
449#[gpui::test]
450async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) {
451 const SERVER_1_ID: &str = "mcp-1";
452
453 let server_1_id = ContextServerId(SERVER_1_ID.into());
454
455 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
456
457 let executor = cx.executor();
458 let store = project.read_with(cx, |project, _| project.context_server_store());
459 store.update(cx, |store, _| {
460 store.set_context_server_factory(Box::new(move |id, _| {
461 Arc::new(ContextServer::new(
462 id.clone(),
463 Arc::new(create_fake_transport(id.0.to_string(), executor.clone())),
464 ))
465 }));
466 });
467
468 set_context_server_configuration(
469 vec![(
470 server_1_id.0.clone(),
471 settings::ContextServerSettingsContent::Stdio {
472 enabled: true,
473 remote: false,
474 command: ContextServerCommand {
475 path: "somebinary".into(),
476 args: vec!["arg".to_string()],
477 env: None,
478 timeout: None,
479 },
480 },
481 )],
482 cx,
483 );
484
485 // Ensure that mcp-1 starts up
486 {
487 let _server_events = assert_server_events(
488 &store,
489 vec![
490 (server_1_id.clone(), ContextServerStatus::Starting),
491 (server_1_id.clone(), ContextServerStatus::Running),
492 ],
493 cx,
494 );
495 cx.run_until_parked();
496 }
497
498 // Ensure that mcp-1 is stopped once it is disabled.
499 {
500 let _server_events = assert_server_events(
501 &store,
502 vec![(server_1_id.clone(), ContextServerStatus::Stopped)],
503 cx,
504 );
505 set_context_server_configuration(
506 vec![(
507 server_1_id.0.clone(),
508 settings::ContextServerSettingsContent::Stdio {
509 enabled: false,
510 remote: false,
511 command: ContextServerCommand {
512 path: "somebinary".into(),
513 args: vec!["arg".to_string()],
514 env: None,
515 timeout: None,
516 },
517 },
518 )],
519 cx,
520 );
521
522 cx.run_until_parked();
523 }
524
525 // Ensure that mcp-1 is started once it is enabled again.
526 {
527 let _server_events = assert_server_events(
528 &store,
529 vec![
530 (server_1_id.clone(), ContextServerStatus::Starting),
531 (server_1_id.clone(), ContextServerStatus::Running),
532 ],
533 cx,
534 );
535 set_context_server_configuration(
536 vec![(
537 server_1_id.0.clone(),
538 settings::ContextServerSettingsContent::Stdio {
539 enabled: true,
540 remote: false,
541 command: ContextServerCommand {
542 path: "somebinary".into(),
543 args: vec!["arg".to_string()],
544 timeout: None,
545 env: None,
546 },
547 },
548 )],
549 cx,
550 );
551
552 cx.run_until_parked();
553 }
554}
555
556#[gpui::test]
557async fn test_server_ids_includes_disabled_servers(cx: &mut TestAppContext) {
558 const ENABLED_SERVER_ID: &str = "enabled-server";
559 const DISABLED_SERVER_ID: &str = "disabled-server";
560
561 let enabled_server_id = ContextServerId(ENABLED_SERVER_ID.into());
562 let disabled_server_id = ContextServerId(DISABLED_SERVER_ID.into());
563
564 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
565
566 let executor = cx.executor();
567 let store = project.read_with(cx, |project, _| project.context_server_store());
568 store.update(cx, |store, _| {
569 store.set_context_server_factory(Box::new(move |id, _| {
570 Arc::new(ContextServer::new(
571 id.clone(),
572 Arc::new(create_fake_transport(id.0.to_string(), executor.clone())),
573 ))
574 }));
575 });
576
577 // Configure one enabled and one disabled server
578 set_context_server_configuration(
579 vec![
580 (
581 enabled_server_id.0.clone(),
582 settings::ContextServerSettingsContent::Stdio {
583 enabled: true,
584 remote: false,
585 command: ContextServerCommand {
586 path: "somebinary".into(),
587 args: vec![],
588 env: None,
589 timeout: None,
590 },
591 },
592 ),
593 (
594 disabled_server_id.0.clone(),
595 settings::ContextServerSettingsContent::Stdio {
596 enabled: false,
597 remote: false,
598 command: ContextServerCommand {
599 path: "somebinary".into(),
600 args: vec![],
601 env: None,
602 timeout: None,
603 },
604 },
605 ),
606 ],
607 cx,
608 );
609
610 cx.run_until_parked();
611
612 // Verify that server_ids includes both enabled and disabled servers
613 cx.update(|cx| {
614 let server_ids = store.read(cx).server_ids().to_vec();
615 assert!(
616 server_ids.contains(&enabled_server_id),
617 "server_ids should include enabled server"
618 );
619 assert!(
620 server_ids.contains(&disabled_server_id),
621 "server_ids should include disabled server"
622 );
623 });
624
625 // Verify that the enabled server is running and the disabled server is not
626 cx.read(|cx| {
627 assert_eq!(
628 store.read(cx).status_for_server(&enabled_server_id),
629 Some(ContextServerStatus::Running),
630 "enabled server should be running"
631 );
632 // Disabled server should not be in the servers map (status returns None)
633 // but should still be in server_ids
634 assert_eq!(
635 store.read(cx).status_for_server(&disabled_server_id),
636 None,
637 "disabled server should not have a status (not in servers map)"
638 );
639 });
640}
641
642fn set_context_server_configuration(
643 context_servers: Vec<(Arc<str>, settings::ContextServerSettingsContent)>,
644 cx: &mut TestAppContext,
645) {
646 cx.update(|cx| {
647 SettingsStore::update_global(cx, |store, cx| {
648 store.update_user_settings(cx, |content| {
649 content.project.context_servers.clear();
650 for (id, config) in context_servers {
651 content.project.context_servers.insert(id, config);
652 }
653 });
654 })
655 });
656}
657
658#[gpui::test]
659async fn test_remote_context_server(cx: &mut TestAppContext) {
660 const SERVER_ID: &str = "remote-server";
661 let server_id = ContextServerId(SERVER_ID.into());
662 let server_url = "http://example.com/api";
663
664 let client = FakeHttpClient::create(|_| async move {
665 use http_client::AsyncBody;
666
667 let response = Response::builder()
668 .status(200)
669 .header("Content-Type", "application/json")
670 .body(AsyncBody::from(
671 serde_json::to_string(&json!({
672 "jsonrpc": "2.0",
673 "id": 0,
674 "result": {
675 "protocolVersion": "2024-11-05",
676 "capabilities": {},
677 "serverInfo": {
678 "name": "test-server",
679 "version": "1.0.0"
680 }
681 }
682 }))
683 .unwrap(),
684 ))
685 .unwrap();
686 Ok(response)
687 });
688 cx.update(|cx| cx.set_http_client(client));
689
690 let (_fs, project) = setup_context_server_test(cx, json!({ "code.rs": "" }), vec![]).await;
691
692 let store = project.read_with(cx, |project, _| project.context_server_store());
693
694 set_context_server_configuration(
695 vec![(
696 server_id.0.clone(),
697 settings::ContextServerSettingsContent::Http {
698 enabled: true,
699 url: server_url.to_string(),
700 headers: Default::default(),
701 timeout: None,
702 },
703 )],
704 cx,
705 );
706
707 let _server_events = assert_server_events(
708 &store,
709 vec![
710 (server_id.clone(), ContextServerStatus::Starting),
711 (server_id.clone(), ContextServerStatus::Running),
712 ],
713 cx,
714 );
715 cx.run_until_parked();
716}
717
718struct ServerEvents {
719 received_event_count: Rc<RefCell<usize>>,
720 expected_event_count: usize,
721 _subscription: Subscription,
722}
723
724impl Drop for ServerEvents {
725 fn drop(&mut self) {
726 let actual_event_count = *self.received_event_count.borrow();
727 assert_eq!(
728 actual_event_count, self.expected_event_count,
729 "
730 Expected to receive {} context server store events, but received {} events",
731 self.expected_event_count, actual_event_count
732 );
733 }
734}
735
736#[gpui::test]
737async fn test_context_server_global_timeout(cx: &mut TestAppContext) {
738 cx.update(|cx| {
739 let settings_store = SettingsStore::test(cx);
740 cx.set_global(settings_store);
741 SettingsStore::update_global(cx, |store, cx| {
742 store
743 .set_user_settings(r#"{"context_server_timeout": 90}"#, cx)
744 .expect("Failed to set test user settings");
745 });
746 });
747
748 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
749
750 let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
751 let store = cx.new(|cx| {
752 ContextServerStore::test(
753 registry.clone(),
754 project.read(cx).worktree_store(),
755 Some(project.downgrade()),
756 cx,
757 )
758 });
759
760 let mut async_cx = cx.to_async();
761 let result = ContextServerStore::create_context_server(
762 store.downgrade(),
763 ContextServerId("test-server".into()),
764 Arc::new(ContextServerConfiguration::Http {
765 url: url::Url::parse("http://localhost:8080").expect("Failed to parse test URL"),
766 headers: Default::default(),
767 timeout: None,
768 }),
769 &mut async_cx,
770 )
771 .await;
772
773 assert!(
774 result.is_ok(),
775 "Server should be created successfully with global timeout"
776 );
777}
778
779#[gpui::test]
780async fn test_context_server_per_server_timeout_override(cx: &mut TestAppContext) {
781 const SERVER_ID: &str = "test-server";
782
783 cx.update(|cx| {
784 let settings_store = SettingsStore::test(cx);
785 cx.set_global(settings_store);
786 SettingsStore::update_global(cx, |store, cx| {
787 store
788 .set_user_settings(r#"{"context_server_timeout": 60}"#, cx)
789 .expect("Failed to set test user settings");
790 });
791 });
792
793 let (_fs, project) = setup_context_server_test(
794 cx,
795 json!({"code.rs": ""}),
796 vec![(
797 SERVER_ID.into(),
798 ContextServerSettings::Http {
799 enabled: true,
800 url: "http://localhost:8080".to_string(),
801 headers: Default::default(),
802 timeout: Some(120),
803 },
804 )],
805 )
806 .await;
807
808 let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
809 let store = cx.new(|cx| {
810 ContextServerStore::test(
811 registry.clone(),
812 project.read(cx).worktree_store(),
813 Some(project.downgrade()),
814 cx,
815 )
816 });
817
818 let mut async_cx = cx.to_async();
819 let result = ContextServerStore::create_context_server(
820 store.downgrade(),
821 ContextServerId("test-server".into()),
822 Arc::new(ContextServerConfiguration::Http {
823 url: url::Url::parse("http://localhost:8080").expect("Failed to parse test URL"),
824 headers: Default::default(),
825 timeout: Some(120),
826 }),
827 &mut async_cx,
828 )
829 .await;
830
831 assert!(
832 result.is_ok(),
833 "Server should be created successfully with per-server timeout override"
834 );
835}
836
837#[gpui::test]
838async fn test_context_server_stdio_timeout(cx: &mut TestAppContext) {
839 let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
840
841 let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
842 let store = cx.new(|cx| {
843 ContextServerStore::test(
844 registry.clone(),
845 project.read(cx).worktree_store(),
846 Some(project.downgrade()),
847 cx,
848 )
849 });
850
851 let mut async_cx = cx.to_async();
852 let result = ContextServerStore::create_context_server(
853 store.downgrade(),
854 ContextServerId("stdio-server".into()),
855 Arc::new(ContextServerConfiguration::Custom {
856 command: ContextServerCommand {
857 path: "/usr/bin/node".into(),
858 args: vec!["server.js".into()],
859 env: None,
860 timeout: Some(180000),
861 },
862 remote: false,
863 }),
864 &mut async_cx,
865 )
866 .await;
867
868 assert!(
869 result.is_ok(),
870 "Stdio server should be created successfully with timeout"
871 );
872}
873
874fn assert_server_events(
875 store: &Entity<ContextServerStore>,
876 expected_events: Vec<(ContextServerId, ContextServerStatus)>,
877 cx: &mut TestAppContext,
878) -> ServerEvents {
879 cx.update(|cx| {
880 let mut ix = 0;
881 let received_event_count = Rc::new(RefCell::new(0));
882 let expected_event_count = expected_events.len();
883 let subscription = cx.subscribe(store, {
884 let received_event_count = received_event_count.clone();
885 move |_, event, _| {
886 let ServerStatusChangedEvent {
887 server_id: actual_server_id,
888 status: actual_status,
889 } = event;
890 let (expected_server_id, expected_status) = &expected_events[ix];
891
892 assert_eq!(
893 actual_server_id, expected_server_id,
894 "Expected different server id at index {}",
895 ix
896 );
897 assert_eq!(
898 actual_status, expected_status,
899 "Expected different status at index {}",
900 ix
901 );
902 ix += 1;
903 *received_event_count.borrow_mut() += 1;
904 }
905 });
906 ServerEvents {
907 expected_event_count,
908 received_event_count,
909 _subscription: subscription,
910 }
911 })
912}
913
914async fn setup_context_server_test(
915 cx: &mut TestAppContext,
916 files: serde_json::Value,
917 context_server_configurations: Vec<(Arc<str>, ContextServerSettings)>,
918) -> (Arc<FakeFs>, Entity<Project>) {
919 cx.update(|cx| {
920 let settings_store = SettingsStore::test(cx);
921 cx.set_global(settings_store);
922 let mut settings = ProjectSettings::get_global(cx).clone();
923 for (id, config) in context_server_configurations {
924 settings.context_servers.insert(id, config);
925 }
926 ProjectSettings::override_global(settings, cx);
927 });
928
929 let fs = FakeFs::new(cx.executor());
930 fs.insert_tree(path!("/test"), files).await;
931 let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
932
933 (fs, project)
934}
935
936struct FakeContextServerDescriptor {
937 path: PathBuf,
938}
939
940impl FakeContextServerDescriptor {
941 fn new(path: impl Into<PathBuf>) -> Self {
942 Self { path: path.into() }
943 }
944}
945
946impl ContextServerDescriptor for FakeContextServerDescriptor {
947 fn command(
948 &self,
949 _worktree_store: Entity<WorktreeStore>,
950 _cx: &AsyncApp,
951 ) -> Task<Result<ContextServerCommand>> {
952 Task::ready(Ok(ContextServerCommand {
953 path: self.path.clone(),
954 args: vec!["arg1".to_string(), "arg2".to_string()],
955 env: None,
956 timeout: None,
957 }))
958 }
959
960 fn configuration(
961 &self,
962 _worktree_store: Entity<WorktreeStore>,
963 _cx: &AsyncApp,
964 ) -> Task<Result<Option<::extension::ContextServerConfiguration>>> {
965 Task::ready(Ok(None))
966 }
967}