context_server_store.rs

  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}