Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:47:42.166Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

extension_store_test.rs

1244 lines · 42.3 KB · rust
1use crate::{
2    Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
3    ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry,
4    RELOAD_DEBOUNCE_DURATION, SchemaVersion,
5};
6use async_compression::futures::bufread::GzipEncoder;
7use collections::{BTreeMap, HashSet};
8use extension::ExtensionHostProxy;
9use fs::{FakeFs, Fs, RealFs};
10use futures::{AsyncReadExt, FutureExt, StreamExt, io::BufReader};
11use gpui::{AppContext as _, BackgroundExecutor, TaskExt, TestAppContext};
12use http_client::{FakeHttpClient, Response};
13use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry};
14use language_extension::LspAccess;
15use lsp::LanguageServerName;
16use node_runtime::NodeRuntime;
17use parking_lot::Mutex;
18use project::{DEFAULT_COMPLETION_CONTEXT, Project};
19use release_channel::AppVersion;
20use reqwest_client::ReqwestClient;
21use serde_json::json;
22use settings::SettingsStore;
23use std::{
24    ffi::OsString,
25    path::{Path, PathBuf},
26    sync::Arc,
27};
28use theme::ThemeRegistry;
29use util::{rel_path::rel_path_buf, test::TempTree};
30
31#[cfg(test)]
32#[ctor::ctor(unsafe)]
33fn init_logger() {
34    zlog::init_test();
35}
36
37fn remote_sync_entry(id: &str, manifest_body: &str) -> ExtensionIndexEntry {
38    let manifest = format!(
39        r#"
40        id = "{id}"
41        name = "{id}"
42        version = "1.0.0"
43        schema_version = 0
44
45        {manifest_body}
46        "#
47    );
48
49    ExtensionIndexEntry {
50        manifest: Arc::new(toml::from_str(&manifest).unwrap()),
51        dev: false,
52    }
53}
54
55fn remote_sync_language_entry(extension: &str, path: &str) -> ExtensionIndexLanguageEntry {
56    ExtensionIndexLanguageEntry {
57        extension: extension.into(),
58        path: path.into(),
59        matcher: LanguageMatcher::default().into(),
60        hidden: false,
61        grammar: None,
62    }
63}
64
65fn remote_sync_extension_ids(index: &ExtensionIndex) -> Vec<String> {
66    let mut extensions = index
67        .extensions_to_sync_to_remote()
68        .into_entries()
69        .map(|(id, _)| id.to_string())
70        .collect::<Vec<_>>();
71
72    extensions.sort();
73
74    extensions
75}
76
77#[test]
78fn remote_sync_includes_language_dependencies() {
79    let index = ExtensionIndex {
80        extensions: [
81            (
82                "bar-language".into(),
83                remote_sync_entry("bar-language", r#"languages = ["languages/bar"]"#),
84            ),
85            (
86                "foo-lsp".into(),
87                remote_sync_entry(
88                    "foo-lsp",
89                    r#"
90                    [language_servers.foo]
91                    language = "Foo"
92                    "#,
93                ),
94            ),
95            (
96                "foo-language".into(),
97                remote_sync_entry("foo-language", r#"languages = ["languages/foo"]"#),
98            ),
99        ]
100        .into_iter()
101        .collect(),
102        languages: [
103            (
104                "Bar".into(),
105                remote_sync_language_entry("bar-language", "languages/bar"),
106            ),
107            (
108                "Foo".into(),
109                remote_sync_language_entry("foo-language", "languages/foo"),
110            ),
111        ]
112        .into_iter()
113        .collect(),
114        themes: BTreeMap::default(),
115        icon_themes: BTreeMap::default(),
116    };
117
118    assert_eq!(
119        remote_sync_extension_ids(&index),
120        ["foo-language", "foo-lsp"]
121    );
122}
123
124#[test]
125fn remote_sync_keeps_shared_language_dependency_once() {
126    let index = ExtensionIndex {
127        extensions: [
128            (
129                "aaa-lsp".into(),
130                remote_sync_entry(
131                    "aaa-lsp",
132                    r#"
133                    [language_servers.aaa]
134                    language = "Foo"
135                    "#,
136                ),
137            ),
138            (
139                "bbb-lsp".into(),
140                remote_sync_entry(
141                    "bbb-lsp",
142                    r#"
143                    [language_servers.bbb]
144                    language = "Foo"
145                    "#,
146                ),
147            ),
148            (
149                "zzz-language".into(),
150                remote_sync_entry("zzz-language", r#"languages = ["languages/foo"]"#),
151            ),
152        ]
153        .into_iter()
154        .collect(),
155        languages: [(
156            "Foo".into(),
157            remote_sync_language_entry("zzz-language", "languages/foo"),
158        )]
159        .into_iter()
160        .collect(),
161        themes: BTreeMap::default(),
162        icon_themes: BTreeMap::default(),
163    };
164
165    assert_eq!(
166        remote_sync_extension_ids(&index),
167        ["aaa-lsp", "bbb-lsp", "zzz-language"]
168    );
169}
170
171#[test]
172fn remote_sync_keeps_remote_loadable_extensions_without_language_dependency() {
173    let index = ExtensionIndex {
174        extensions: [(
175            "foo".into(),
176            remote_sync_entry(
177                "foo",
178                r#"
179                [language_servers.foo]
180                language = "Foo"
181                "#,
182            ),
183        )]
184        .into_iter()
185        .collect(),
186        languages: BTreeMap::default(),
187        themes: BTreeMap::default(),
188        icon_themes: BTreeMap::default(),
189    };
190
191    assert_eq!(remote_sync_extension_ids(&index), ["foo"]);
192}
193
194#[test]
195fn remote_sync_keeps_debug_adapters() {
196    let index = ExtensionIndex {
197        extensions: [(
198            "foo".into(),
199            remote_sync_entry(
200                "foo",
201                r#"
202                [debug_adapters.foo]
203                "#,
204            ),
205        )]
206        .into_iter()
207        .collect(),
208        languages: BTreeMap::default(),
209        themes: BTreeMap::default(),
210        icon_themes: BTreeMap::default(),
211    };
212
213    assert_eq!(remote_sync_extension_ids(&index), ["foo"]);
214}
215
216#[gpui::test]
217async fn test_extension_store(cx: &mut TestAppContext) {
218    init_test(cx);
219
220    let fs = FakeFs::new(cx.executor());
221    let http_client = FakeHttpClient::with_200_response();
222
223    fs.insert_tree(
224        "/the-extension-dir",
225        json!({
226            "installed": {
227                "zed-monokai": {
228                    "extension.json": r#"{
229                        "id": "zed-monokai",
230                        "name": "Zed Monokai",
231                        "version": "2.0.0",
232                        "themes": {
233                            "Monokai Dark": "themes/monokai.json",
234                            "Monokai Light": "themes/monokai.json",
235                            "Monokai Pro Dark": "themes/monokai-pro.json",
236                            "Monokai Pro Light": "themes/monokai-pro.json"
237                        }
238                    }"#,
239                    "themes": {
240                        "monokai.json": r#"{
241                            "name": "Monokai",
242                            "author": "Someone",
243                            "themes": [
244                                {
245                                    "name": "Monokai Dark",
246                                    "appearance": "dark",
247                                    "style": {}
248                                },
249                                {
250                                    "name": "Monokai Light",
251                                    "appearance": "light",
252                                    "style": {}
253                                }
254                            ]
255                        }"#,
256                        "monokai-pro.json": r#"{
257                            "name": "Monokai Pro",
258                            "author": "Someone",
259                            "themes": [
260                                {
261                                    "name": "Monokai Pro Dark",
262                                    "appearance": "dark",
263                                    "style": {}
264                                },
265                                {
266                                    "name": "Monokai Pro Light",
267                                    "appearance": "light",
268                                    "style": {}
269                                }
270                            ]
271                        }"#,
272                    }
273                },
274                "zed-ruby": {
275                    "extension.json": r#"{
276                        "id": "zed-ruby",
277                        "name": "Zed Ruby",
278                        "version": "1.0.0",
279                        "grammars": {
280                            "ruby": "grammars/ruby.wasm",
281                            "embedded_template": "grammars/embedded_template.wasm"
282                        },
283                        "languages": {
284                            "ruby": "languages/ruby",
285                            "erb": "languages/erb"
286                        }
287                    }"#,
288                    "grammars": {
289                        "ruby.wasm": "",
290                        "embedded_template.wasm": "",
291                    },
292                    "languages": {
293                        "ruby": {
294                            "config.toml": r#"
295                                name = "Ruby"
296                                grammar = "ruby"
297                                path_suffixes = ["rb"]
298                            "#,
299                            "highlights.scm": "",
300                        },
301                        "erb": {
302                            "config.toml": r#"
303                                name = "ERB"
304                                grammar = "embedded_template"
305                                path_suffixes = ["erb"]
306                            "#,
307                            "highlights.scm": "",
308                        }
309                    },
310                }
311            }
312        }),
313    )
314    .await;
315
316    let mut expected_index = ExtensionIndex {
317        extensions: [
318            (
319                "zed-ruby".into(),
320                ExtensionIndexEntry {
321                    manifest: Arc::new(ExtensionManifest {
322                        id: "zed-ruby".into(),
323                        name: "Zed Ruby".into(),
324                        version: "1.0.0".into(),
325                        schema_version: SchemaVersion::ZERO,
326                        description: None,
327                        authors: Vec::new(),
328                        repository: None,
329                        themes: Default::default(),
330                        icon_themes: Vec::new(),
331                        lib: Default::default(),
332                        languages: vec![
333                            rel_path_buf("languages/erb"),
334                            rel_path_buf("languages/ruby"),
335                        ],
336                        grammars: [
337                            ("embedded_template".into(), GrammarManifestEntry::default()),
338                            ("ruby".into(), GrammarManifestEntry::default()),
339                        ]
340                        .into_iter()
341                        .collect(),
342                        language_servers: BTreeMap::default(),
343                        context_servers: BTreeMap::default(),
344                        slash_commands: BTreeMap::default(),
345                        snippets: None,
346                        capabilities: Vec::new(),
347                        debug_adapters: Default::default(),
348                        debug_locators: Default::default(),
349                        language_model_providers: BTreeMap::default(),
350                    }),
351                    dev: false,
352                },
353            ),
354            (
355                "zed-monokai".into(),
356                ExtensionIndexEntry {
357                    manifest: Arc::new(ExtensionManifest {
358                        id: "zed-monokai".into(),
359                        name: "Zed Monokai".into(),
360                        version: "2.0.0".into(),
361                        schema_version: SchemaVersion::ZERO,
362                        description: None,
363                        authors: vec![],
364                        repository: None,
365                        themes: vec![
366                            rel_path_buf("themes/monokai-pro.json"),
367                            rel_path_buf("themes/monokai.json"),
368                        ],
369                        icon_themes: Vec::new(),
370                        lib: Default::default(),
371                        languages: Default::default(),
372                        grammars: BTreeMap::default(),
373                        language_servers: BTreeMap::default(),
374                        context_servers: BTreeMap::default(),
375                        slash_commands: BTreeMap::default(),
376                        snippets: None,
377                        capabilities: Vec::new(),
378                        debug_adapters: Default::default(),
379                        debug_locators: Default::default(),
380                        language_model_providers: BTreeMap::default(),
381                    }),
382                    dev: false,
383                },
384            ),
385        ]
386        .into_iter()
387        .collect(),
388        languages: [
389            (
390                "ERB".into(),
391                ExtensionIndexLanguageEntry {
392                    extension: "zed-ruby".into(),
393                    path: "languages/erb".into(),
394                    grammar: Some("embedded_template".into()),
395                    hidden: false,
396                    matcher: (LanguageMatcher {
397                        path_suffixes: vec!["erb".into()],
398                        first_line_pattern: None,
399                        ..LanguageMatcher::default()
400                    })
401                    .into(),
402                },
403            ),
404            (
405                "Ruby".into(),
406                ExtensionIndexLanguageEntry {
407                    extension: "zed-ruby".into(),
408                    path: "languages/ruby".into(),
409                    grammar: Some("ruby".into()),
410                    hidden: false,
411                    matcher: (LanguageMatcher {
412                        path_suffixes: vec!["rb".into()],
413                        first_line_pattern: None,
414                        ..LanguageMatcher::default()
415                    })
416                    .into(),
417                },
418            ),
419        ]
420        .into_iter()
421        .collect(),
422        themes: [
423            (
424                "Monokai Dark".into(),
425                ExtensionIndexThemeEntry {
426                    extension: "zed-monokai".into(),
427                    path: "themes/monokai.json".into(),
428                },
429            ),
430            (
431                "Monokai Light".into(),
432                ExtensionIndexThemeEntry {
433                    extension: "zed-monokai".into(),
434                    path: "themes/monokai.json".into(),
435                },
436            ),
437            (
438                "Monokai Pro Dark".into(),
439                ExtensionIndexThemeEntry {
440                    extension: "zed-monokai".into(),
441                    path: "themes/monokai-pro.json".into(),
442                },
443            ),
444            (
445                "Monokai Pro Light".into(),
446                ExtensionIndexThemeEntry {
447                    extension: "zed-monokai".into(),
448                    path: "themes/monokai-pro.json".into(),
449                },
450            ),
451        ]
452        .into_iter()
453        .collect(),
454        icon_themes: BTreeMap::default(),
455    };
456
457    let proxy = Arc::new(ExtensionHostProxy::new());
458    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
459    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
460    let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
461    language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone());
462    let node_runtime = NodeRuntime::unavailable();
463
464    let store = cx.new(|cx| {
465        ExtensionStore::new(
466            PathBuf::from("/the-extension-dir"),
467            None,
468            proxy.clone(),
469            fs.clone(),
470            http_client.clone(),
471            http_client.clone(),
472            None,
473            node_runtime.clone(),
474            cx,
475        )
476    });
477
478    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
479    store.read_with(cx, |store, _| {
480        let index = &store.extension_index;
481        assert_eq!(index.extensions, expected_index.extensions);
482
483        for ((actual_key, actual_language), (expected_key, expected_language)) in
484            index.languages.iter().zip(expected_index.languages.iter())
485        {
486            assert_eq!(actual_key, expected_key);
487            assert_eq!(actual_language.grammar, expected_language.grammar);
488            assert_eq!(actual_language.matcher, expected_language.matcher);
489            assert_eq!(actual_language.hidden, expected_language.hidden);
490        }
491        assert_eq!(index.themes, expected_index.themes);
492
493        assert_eq!(
494            language_registry.language_names(),
495            [
496                LanguageName::new_static("ERB"),
497                LanguageName::new_static("Plain Text"),
498                LanguageName::new_static("Ruby"),
499            ]
500        );
501        assert_eq!(
502            theme_registry.list_names(),
503            [
504                "Monokai Dark",
505                "Monokai Light",
506                "Monokai Pro Dark",
507                "Monokai Pro Light",
508                "One Dark",
509            ]
510        );
511    });
512
513    fs.insert_tree(
514        "/the-extension-dir/installed/zed-gruvbox",
515        json!({
516            "extension.json": r#"{
517                "id": "zed-gruvbox",
518                "name": "Zed Gruvbox",
519                "version": "1.0.0",
520                "themes": {
521                    "Gruvbox": "themes/gruvbox.json"
522                }
523            }"#,
524            "themes": {
525                "gruvbox.json": r#"{
526                    "name": "Gruvbox",
527                    "author": "Someone Else",
528                    "themes": [
529                        {
530                            "name": "Gruvbox",
531                            "appearance": "dark",
532                            "style": {}
533                        }
534                    ]
535                }"#,
536            }
537        }),
538    )
539    .await;
540
541    expected_index.extensions.insert(
542        "zed-gruvbox".into(),
543        ExtensionIndexEntry {
544            manifest: Arc::new(ExtensionManifest {
545                id: "zed-gruvbox".into(),
546                name: "Zed Gruvbox".into(),
547                version: "1.0.0".into(),
548                schema_version: SchemaVersion::ZERO,
549                description: None,
550                authors: vec![],
551                repository: None,
552                themes: vec![rel_path_buf("themes/gruvbox.json")],
553                icon_themes: Vec::new(),
554                lib: Default::default(),
555                languages: Default::default(),
556                grammars: BTreeMap::default(),
557                language_servers: BTreeMap::default(),
558                context_servers: BTreeMap::default(),
559                slash_commands: BTreeMap::default(),
560                snippets: None,
561                capabilities: Vec::new(),
562                debug_adapters: Default::default(),
563                debug_locators: Default::default(),
564                language_model_providers: BTreeMap::default(),
565            }),
566            dev: false,
567        },
568    );
569    expected_index.themes.insert(
570        "Gruvbox".into(),
571        ExtensionIndexThemeEntry {
572            extension: "zed-gruvbox".into(),
573            path: "themes/gruvbox.json".into(),
574        },
575    );
576
577    #[allow(clippy::let_underscore_future)]
578    let _ = store.update(cx, |store, cx| store.reload(None, cx));
579
580    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
581    store.read_with(cx, |store, _| {
582        let index = &store.extension_index;
583
584        for ((actual_key, actual_language), (expected_key, expected_language)) in
585            index.languages.iter().zip(expected_index.languages.iter())
586        {
587            assert_eq!(actual_key, expected_key);
588            assert_eq!(actual_language.grammar, expected_language.grammar);
589            assert_eq!(actual_language.matcher, expected_language.matcher);
590            assert_eq!(actual_language.hidden, expected_language.hidden);
591        }
592
593        assert_eq!(index.extensions, expected_index.extensions);
594        assert_eq!(index.themes, expected_index.themes);
595
596        assert_eq!(
597            theme_registry.list_names(),
598            [
599                "Gruvbox",
600                "Monokai Dark",
601                "Monokai Light",
602                "Monokai Pro Dark",
603                "Monokai Pro Light",
604                "One Dark",
605            ]
606        );
607    });
608
609    let prev_fs_metadata_call_count = fs.metadata_call_count();
610    let prev_fs_read_dir_call_count = fs.read_dir_call_count();
611
612    // Create new extension store, as if Zed were restarting.
613    drop(store);
614    let store = cx.new(|cx| {
615        ExtensionStore::new(
616            PathBuf::from("/the-extension-dir"),
617            None,
618            proxy,
619            fs.clone(),
620            http_client.clone(),
621            http_client.clone(),
622            None,
623            node_runtime.clone(),
624            cx,
625        )
626    });
627
628    cx.executor().run_until_parked();
629    store.read_with(cx, |store, _| {
630        assert_eq!(store.extension_index.extensions, expected_index.extensions);
631        assert_eq!(store.extension_index.themes, expected_index.themes);
632        assert_eq!(
633            store.extension_index.icon_themes,
634            expected_index.icon_themes
635        );
636
637        for ((actual_key, actual_language), (expected_key, expected_language)) in store
638            .extension_index
639            .languages
640            .iter()
641            .zip(expected_index.languages.iter())
642        {
643            assert_eq!(actual_key, expected_key);
644            assert_eq!(actual_language.grammar, expected_language.grammar);
645            assert_eq!(actual_language.matcher, expected_language.matcher);
646            assert_eq!(actual_language.hidden, expected_language.hidden);
647        }
648
649        assert_eq!(
650            language_registry.language_names(),
651            [
652                LanguageName::new_static("ERB"),
653                LanguageName::new_static("Plain Text"),
654                LanguageName::new_static("Ruby"),
655            ]
656        );
657        assert_eq!(
658            language_registry.grammar_names(),
659            ["embedded_template".into(), "ruby".into()]
660        );
661        assert_eq!(
662            theme_registry.list_names(),
663            [
664                "Gruvbox",
665                "Monokai Dark",
666                "Monokai Light",
667                "Monokai Pro Dark",
668                "Monokai Pro Light",
669                "One Dark",
670            ]
671        );
672
673        // The on-disk manifest limits the number of FS calls that need to be made
674        // on startup.
675        assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count);
676        assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2);
677    });
678
679    store.update(cx, |store, cx| {
680        store
681            .uninstall_extension("zed-ruby".into(), cx)
682            .detach_and_log_err(cx);
683    });
684
685    cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
686    expected_index.extensions.remove("zed-ruby");
687    expected_index.languages.remove("Ruby");
688    expected_index.languages.remove("ERB");
689
690    store.read_with(cx, |store, _| {
691        assert_eq!(store.extension_index.extensions, expected_index.extensions);
692        assert_eq!(store.extension_index.themes, expected_index.themes);
693        assert_eq!(
694            store.extension_index.icon_themes,
695            expected_index.icon_themes
696        );
697
698        for ((actual_key, actual_language), (expected_key, expected_language)) in store
699            .extension_index
700            .languages
701            .iter()
702            .zip(expected_index.languages.iter())
703        {
704            assert_eq!(actual_key, expected_key);
705            assert_eq!(actual_language.grammar, expected_language.grammar);
706            assert_eq!(actual_language.matcher, expected_language.matcher);
707            assert_eq!(actual_language.hidden, expected_language.hidden);
708        }
709
710        assert_eq!(
711            language_registry.language_names(),
712            [LanguageName::new_static("Plain Text")]
713        );
714        assert_eq!(language_registry.grammar_names(), []);
715    });
716}
717
718#[gpui::test]
719async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
720    init_test(cx);
721    cx.executor().allow_parking();
722
723    let executor = cx.executor();
724    async fn await_or_timeout<T>(
725        executor: &BackgroundExecutor,
726        what: &'static str,
727        seconds: u64,
728        future: impl std::future::Future<Output = T>,
729    ) -> T {
730        let timeout = executor.timer(std::time::Duration::from_secs(seconds));
731
732        futures::select! {
733            output = future.fuse() => output,
734            _ = futures::FutureExt::fuse(timeout) => panic!(
735            "[test_extension_store_with_test_extension] timed out after {seconds}s while {what}"
736        )
737        }
738    }
739
740    let root_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
741        .parent()
742        .unwrap()
743        .parent()
744        .unwrap();
745    let cache_dir = root_dir.join("target");
746    let test_extension_id = "test-extension";
747    let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
748
749    let fs = Arc::new(RealFs::new(None, cx.executor()));
750    let extensions_tree = TempTree::new(json!({
751        "installed": {},
752        "work": {}
753    }));
754    let project_dir = TempTree::new(json!({
755        "test.gleam": ""
756    }));
757
758    let extensions_dir = extensions_tree.path().canonicalize().unwrap();
759    let project_dir = project_dir.path().canonicalize().unwrap();
760
761    let project = await_or_timeout(
762        &executor,
763        "awaiting Project::test",
764        5,
765        Project::test(fs.clone(), [project_dir.as_path()], cx),
766    )
767    .await;
768
769    let proxy = Arc::new(ExtensionHostProxy::new());
770    let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
771    theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
772    let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
773    language_extension::init(
774        LspAccess::ViaLspStore(
775            project
776                .update(cx, |project, _| project.lsp_store())
777                .downgrade(),
778        ),
779        proxy.clone(),
780        language_registry.clone(),
781    );
782    let node_runtime = NodeRuntime::unavailable();
783
784    let mut status_updates = language_registry.language_server_binary_statuses();
785
786    struct FakeLanguageServerVersion {
787        version: String,
788        binary_contents: String,
789        http_request_count: usize,
790    }
791
792    let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion {
793        version: "v1.2.3".into(),
794        binary_contents: "the-binary-contents".into(),
795        http_request_count: 0,
796    }));
797
798    let extension_client = FakeHttpClient::create({
799        let language_server_version = language_server_version.clone();
800        move |request| {
801            let language_server_version = language_server_version.clone();
802            async move {
803                let version = language_server_version.lock().version.clone();
804                let binary_contents = language_server_version.lock().binary_contents.clone();
805
806                let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases";
807                let asset_download_uri =
808                    format!("https://fake-download.example.com/gleam-{version}");
809
810                let uri = request.uri().to_string();
811                if uri == github_releases_uri {
812                    language_server_version.lock().http_request_count += 1;
813                    Ok(Response::new(
814                        json!([
815                            {
816                                "tag_name": version,
817                                "prerelease": false,
818                                "tarball_url": "",
819                                "zipball_url": "",
820                                "assets": [
821                                    {
822                                        "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"),
823                                        "browser_download_url": asset_download_uri
824                                    },
825                                    {
826                                        "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"),
827                                        "browser_download_url": asset_download_uri
828                                    },
829                                    {
830                                        "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"),
831                                        "browser_download_url": asset_download_uri
832                                    },
833                                    {
834                                        "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"),
835                                        "browser_download_url": asset_download_uri
836                                    }
837                                ]
838                            }
839                        ])
840                        .to_string()
841                        .into(),
842                    ))
843                } else if uri == asset_download_uri {
844                    language_server_version.lock().http_request_count += 1;
845                    let mut bytes = Vec::<u8>::new();
846                    let mut archive = async_tar::Builder::new(&mut bytes);
847                    let mut header = async_tar::Header::new_gnu();
848                    header.set_size(binary_contents.len() as u64);
849                    archive
850                        .append_data(&mut header, "gleam", binary_contents.as_bytes())
851                        .await
852                        .unwrap();
853                    archive.into_inner().await.unwrap();
854                    let mut gzipped_bytes = Vec::new();
855                    let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice()));
856                    encoder.read_to_end(&mut gzipped_bytes).await.unwrap();
857                    Ok(Response::new(gzipped_bytes.into()))
858                } else {
859                    Ok(Response::builder().status(404).body("not found".into())?)
860                }
861            }
862        }
863    });
864    let user_agent = cx.update(|cx| {
865        format!(
866            "Zed/{} ({}; {})",
867            AppVersion::global(cx),
868            std::env::consts::OS,
869            std::env::consts::ARCH
870        )
871    });
872    let builder_client =
873        Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client"));
874
875    let extension_store = cx.new(|cx| {
876        ExtensionStore::new(
877            extensions_dir.clone(),
878            Some(cache_dir),
879            proxy,
880            fs.clone(),
881            extension_client.clone(),
882            builder_client,
883            None,
884            node_runtime,
885            cx,
886        )
887    });
888
889    // Ensure that debounces fire.
890    let mut events = cx.events(&extension_store);
891    let executor = cx.executor();
892    let _task = cx.executor().spawn(async move {
893        while let Some(event) = events.next().await {
894            if let Event::StartedReloading = event {
895                executor.advance_clock(RELOAD_DEBOUNCE_DURATION);
896            }
897        }
898    });
899
900    extension_store.update(cx, |_, cx| {
901        cx.subscribe(&extension_store, |_, _, event, _| {
902            if matches!(event, Event::ExtensionFailedToLoad(_)) {
903                panic!("extension failed to load");
904            }
905        })
906        .detach();
907    });
908
909    let mut extension_events = cx.events(&cx.update(|cx| {
910        extension::ExtensionEvents::try_global(cx)
911            .expect("ExtensionEvents should be initialized in tests")
912    }));
913
914    let executor = cx.executor();
915    await_or_timeout(
916        &executor,
917        "awaiting install_dev_extension",
918        60,
919        extension_store.update(cx, |store, cx| {
920            store.install_dev_extension(test_extension_dir.clone(), cx)
921        }),
922    )
923    .await
924    .unwrap();
925
926    await_or_timeout(
927        &executor,
928        "awaiting ExtensionsInstalledChanged",
929        10,
930        async {
931            while let Some(event) = extension_events.next().await {
932                if matches!(event, extension::Event::ExtensionsInstalledChanged) {
933                    return;
934                }
935            }
936
937            panic!(
938                "[test_extension_store_with_test_extension] extension event stream ended before ExtensionsInstalledChanged"
939            );
940        },
941    )
942    .await;
943
944    let mut fake_servers = language_registry.register_fake_lsp_server(
945        LanguageServerName("gleam".into()),
946        lsp::ServerCapabilities {
947            completion_provider: Some(Default::default()),
948            ..Default::default()
949        },
950        None,
951    );
952    cx.executor().run_until_parked();
953
954    let mut project_events = cx.events(&project);
955    let buffer_path = project_dir.join("test.gleam");
956    let (buffer, _handle) = await_or_timeout(
957        &executor,
958        "awaiting open_local_buffer_with_lsp",
959        5,
960        project.update(cx, |project, cx| {
961            project.open_local_buffer_with_lsp(buffer_path.clone(), cx)
962        }),
963    )
964    .await
965    .unwrap();
966    cx.executor().run_until_parked();
967
968    let buffer_remote_id = buffer.read_with(cx, |buffer, _cx| buffer.remote_id());
969
970    let fake_server = await_or_timeout(
971        &executor,
972        "awaiting first fake server spawn",
973        10,
974        fake_servers.next(),
975    )
976    .await
977    .unwrap();
978
979    let work_dir = extensions_dir.join(format!("work/{test_extension_id}"));
980    let expected_server_path = work_dir.join("gleam-v1.2.3/gleam");
981    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
982
983    // check that IO operations in extension work correctly
984    assert!(work_dir.join("dir-created-with-rel-path").exists());
985    assert!(work_dir.join("dir-created-with-abs-path").exists());
986    assert!(work_dir.join("file-created-with-abs-path").exists());
987    assert!(work_dir.join("file-created-with-rel-path").exists());
988
989    assert_eq!(fake_server.binary.path, expected_server_path);
990    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
991    assert_eq!(
992        await_or_timeout(
993            &executor,
994            "awaiting fs.load(expected_server_path)",
995            5,
996            fs.load(&expected_server_path)
997        )
998        .await
999        .unwrap(),
1000        expected_binary_contents
1001    );
1002    assert_eq!(language_server_version.lock().http_request_count, 2);
1003    assert_eq!(
1004        [
1005            await_or_timeout(
1006                &executor,
1007                "awaiting status_updates #1",
1008                5,
1009                status_updates.next()
1010            )
1011            .await
1012            .unwrap(),
1013            await_or_timeout(
1014                &executor,
1015                "awaiting status_updates #2",
1016                5,
1017                status_updates.next()
1018            )
1019            .await
1020            .unwrap(),
1021            await_or_timeout(
1022                &executor,
1023                "awaiting status_updates #3",
1024                5,
1025                status_updates.next()
1026            )
1027            .await
1028            .unwrap(),
1029            await_or_timeout(
1030                &executor,
1031                "awaiting status_updates #4",
1032                5,
1033                status_updates.next()
1034            )
1035            .await
1036            .unwrap(),
1037        ],
1038        [
1039            (
1040                LanguageServerName::new_static("gleam"),
1041                BinaryStatus::Starting
1042            ),
1043            (
1044                LanguageServerName::new_static("gleam"),
1045                BinaryStatus::CheckingForUpdate
1046            ),
1047            (
1048                LanguageServerName::new_static("gleam"),
1049                BinaryStatus::Downloading
1050            ),
1051            (LanguageServerName::new_static("gleam"), BinaryStatus::None)
1052        ]
1053    );
1054
1055    // The extension creates custom labels for completion items.
1056    fake_server.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
1057        Ok(Some(lsp::CompletionResponse::Array(vec![
1058            lsp::CompletionItem {
1059                label: "foo".into(),
1060                kind: Some(lsp::CompletionItemKind::FUNCTION),
1061                detail: Some("fn() -> Result(Nil, Error)".into()),
1062                ..Default::default()
1063            },
1064            lsp::CompletionItem {
1065                label: "bar.baz".into(),
1066                kind: Some(lsp::CompletionItemKind::FUNCTION),
1067                detail: Some("fn(List(a)) -> a".into()),
1068                ..Default::default()
1069            },
1070            lsp::CompletionItem {
1071                label: "Quux".into(),
1072                kind: Some(lsp::CompletionItemKind::CONSTRUCTOR),
1073                detail: Some("fn(String) -> T".into()),
1074                ..Default::default()
1075            },
1076            lsp::CompletionItem {
1077                label: "my_string".into(),
1078                kind: Some(lsp::CompletionItemKind::CONSTANT),
1079                detail: Some("String".into()),
1080                ..Default::default()
1081            },
1082        ])))
1083    });
1084
1085    // `register_fake_lsp_server` can yield a server instance before the client has fully registered
1086    // the buffer with the project LSP plumbing. Wait for the project to observe that registration
1087    // before issuing requests like completion.
1088    await_or_timeout(
1089        &executor,
1090        "awaiting LanguageServerBufferRegistered",
1091        5,
1092        async {
1093            while let Some(event) = project_events.next().await {
1094                if let project::Event::LanguageServerBufferRegistered { buffer_id, .. } = event {
1095                    if buffer_id == buffer_remote_id {
1096                        return;
1097                    }
1098                }
1099            }
1100
1101            panic!(
1102                "[test_extension_store_with_test_extension] project event stream ended before buffer registration for {}",
1103                buffer_path.display()
1104            );
1105        },
1106    )
1107    .await;
1108
1109    let completion_labels = await_or_timeout(
1110        &executor,
1111        "awaiting completions",
1112        5,
1113        project.update(cx, |project, cx| {
1114            project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx)
1115        }),
1116    )
1117    .await
1118    .unwrap()
1119    .into_iter()
1120    .flat_map(|response| response.completions)
1121    .map(|c| c.label.text)
1122    .collect::<Vec<_>>();
1123    assert_eq!(
1124        completion_labels,
1125        [
1126            "foo: fn() -> Result(Nil, Error)".to_string(),
1127            "bar.baz: fn(List(a)) -> a".to_string(),
1128            "Quux: fn(String) -> T".to_string(),
1129            "my_string: String".to_string(),
1130        ]
1131    );
1132
1133    // Simulate a new version of the language server being released
1134    language_server_version.lock().version = "v2.0.0".into();
1135    language_server_version.lock().binary_contents = "the-new-binary-contents".into();
1136    language_server_version.lock().http_request_count = 0;
1137
1138    // Start a new instance of the language server.
1139    project.update(cx, |project, cx| {
1140        project.restart_language_servers_for_buffers(
1141            vec![buffer.clone()],
1142            HashSet::default(),
1143            true,
1144            cx,
1145        )
1146    });
1147    cx.executor().run_until_parked();
1148
1149    // The extension has cached the binary path, and does not attempt
1150    // to reinstall it.
1151    let fake_server = await_or_timeout(
1152        &executor,
1153        "awaiting second fake server spawn",
1154        5,
1155        fake_servers.next(),
1156    )
1157    .await
1158    .unwrap();
1159    assert_eq!(fake_server.binary.path, expected_server_path);
1160    assert_eq!(
1161        await_or_timeout(
1162            &executor,
1163            "awaiting fs.load(expected_server_path) after restart",
1164            5,
1165            fs.load(&expected_server_path)
1166        )
1167        .await
1168        .unwrap(),
1169        expected_binary_contents
1170    );
1171    assert_eq!(language_server_version.lock().http_request_count, 0);
1172
1173    // Reload the extension, clearing its cache.
1174    // Start a new instance of the language server.
1175    await_or_timeout(
1176        &executor,
1177        "awaiting extension_store.reload(test-extension)",
1178        5,
1179        extension_store.update(cx, |store, cx| {
1180            store.reload(Some("test-extension".into()), cx)
1181        }),
1182    )
1183    .await;
1184    cx.executor().run_until_parked();
1185    project.update(cx, |project, cx| {
1186        project.restart_language_servers_for_buffers(
1187            vec![buffer.clone()],
1188            HashSet::default(),
1189            true,
1190            cx,
1191        )
1192    });
1193
1194    // The extension re-fetches the latest version of the language server.
1195    let fake_server = await_or_timeout(
1196        &executor,
1197        "awaiting third fake server spawn",
1198        5,
1199        fake_servers.next(),
1200    )
1201    .await
1202    .unwrap();
1203    let new_expected_server_path =
1204        extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam"));
1205    let expected_binary_contents = language_server_version.lock().binary_contents.clone();
1206    assert_eq!(fake_server.binary.path, new_expected_server_path);
1207    assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]);
1208    assert_eq!(
1209        await_or_timeout(
1210            &executor,
1211            "awaiting fs.load(new_expected_server_path)",
1212            5,
1213            fs.load(&new_expected_server_path)
1214        )
1215        .await
1216        .unwrap(),
1217        expected_binary_contents
1218    );
1219
1220    // The old language server directory has been cleaned up.
1221    assert!(
1222        await_or_timeout(
1223            &executor,
1224            "awaiting fs.metadata(expected_server_path)",
1225            5,
1226            fs.metadata(&expected_server_path)
1227        )
1228        .await
1229        .unwrap()
1230        .is_none()
1231    );
1232}
1233
1234fn init_test(cx: &mut TestAppContext) {
1235    cx.update(|cx| {
1236        let store = SettingsStore::test(cx);
1237        cx.set_global(store);
1238        release_channel::init(semver::Version::new(0, 0, 0), cx);
1239        extension::init(cx);
1240        theme_settings::init(theme::LoadThemes::JustBase, cx);
1241        gpui_tokio::init(cx);
1242    });
1243}
1244
Served at tenant.openagents/omega Member data and write actions are omitted.