debugger: Fix running JS tests when worktree root and package root do not coincide (#32644)

Cole Miller , Anthony , and Conrad Irwin created

- construct the correct path to the test library based on the location
of package.json
- run scripts from the package root where they were defined
- run tests in the directory of the defining file

Release Notes:

- Debugger Beta: fixed running JS tests when the worktree root is above
the location of package.json.

---------

Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

crates/languages/src/typescript.rs           | 382 +++++++++++++++------
crates/project/src/debugger/locators/node.rs |  19 
crates/project/src/toolchain_store.rs        |   2 
3 files changed, 290 insertions(+), 113 deletions(-)

Detailed changes

crates/languages/src/typescript.rs 🔗

@@ -8,8 +8,7 @@ use futures::future::join_all;
 use gpui::{App, AppContext, AsyncApp, Task};
 use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
 use language::{
-    ContextLocation, ContextProvider, File, LanguageToolchainStore, LocalFile, LspAdapter,
-    LspAdapterDelegate,
+    ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
 };
 use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
 use node_runtime::NodeRuntime;
@@ -29,6 +28,7 @@ use util::archive::extract_zip;
 use util::merge_json_value_into;
 use util::{ResultExt, fs::remove_matching, maybe};
 
+#[derive(Debug)]
 pub(crate) struct TypeScriptContextProvider {
     last_package_json: PackageJsonContents,
 }
@@ -42,47 +42,81 @@ const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
 const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
 
-#[derive(Clone, Default)]
+const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH"));
+
+const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH"));
+
+const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH"));
+
+const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
+    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
+
+#[derive(Clone, Debug, Default)]
 struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
 
+#[derive(Clone, Debug)]
 struct PackageJson {
     mtime: DateTime<Local>,
     data: PackageJsonData,
 }
 
-#[derive(Clone, Default)]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
 struct PackageJsonData {
-    jest: bool,
-    mocha: bool,
-    vitest: bool,
-    jasmine: bool,
-    scripts: BTreeSet<String>,
+    jest_package_path: Option<Arc<Path>>,
+    mocha_package_path: Option<Arc<Path>>,
+    vitest_package_path: Option<Arc<Path>>,
+    jasmine_package_path: Option<Arc<Path>>,
+    scripts: BTreeSet<(Arc<Path>, String)>,
     package_manager: Option<&'static str>,
 }
 
 impl PackageJsonData {
-    fn new(package_json: HashMap<String, Value>) -> Self {
+    fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
         let mut scripts = BTreeSet::new();
         if let Some(serde_json::Value::Object(package_json_scripts)) = package_json.get("scripts") {
-            scripts.extend(package_json_scripts.keys().cloned());
+            scripts.extend(
+                package_json_scripts
+                    .keys()
+                    .cloned()
+                    .map(|name| (path.clone(), name)),
+            );
         }
 
-        let mut jest = false;
-        let mut mocha = false;
-        let mut vitest = false;
-        let mut jasmine = false;
+        let mut jest_package_path = None;
+        let mut mocha_package_path = None;
+        let mut vitest_package_path = None;
+        let mut jasmine_package_path = None;
         if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
-            jest |= dependencies.contains_key("jest");
-            mocha |= dependencies.contains_key("mocha");
-            vitest |= dependencies.contains_key("vitest");
-            jasmine |= dependencies.contains_key("jasmine");
+            if dependencies.contains_key("jest") {
+                jest_package_path.get_or_insert_with(|| path.clone());
+            }
+            if dependencies.contains_key("mocha") {
+                mocha_package_path.get_or_insert_with(|| path.clone());
+            }
+            if dependencies.contains_key("vitest") {
+                vitest_package_path.get_or_insert_with(|| path.clone());
+            }
+            if dependencies.contains_key("jasmine") {
+                jasmine_package_path.get_or_insert_with(|| path.clone());
+            }
         }
         if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
         {
-            jest |= dev_dependencies.contains_key("jest");
-            mocha |= dev_dependencies.contains_key("mocha");
-            vitest |= dev_dependencies.contains_key("vitest");
-            jasmine |= dev_dependencies.contains_key("jasmine");
+            if dev_dependencies.contains_key("jest") {
+                jest_package_path.get_or_insert_with(|| path.clone());
+            }
+            if dev_dependencies.contains_key("mocha") {
+                mocha_package_path.get_or_insert_with(|| path.clone());
+            }
+            if dev_dependencies.contains_key("vitest") {
+                vitest_package_path.get_or_insert_with(|| path.clone());
+            }
+            if dev_dependencies.contains_key("jasmine") {
+                jasmine_package_path.get_or_insert_with(|| path.clone());
+            }
         }
 
         let package_manager = package_json
@@ -101,33 +135,37 @@ impl PackageJsonData {
             });
 
         Self {
-            jest,
-            mocha,
-            vitest,
-            jasmine,
+            jest_package_path,
+            mocha_package_path,
+            vitest_package_path,
+            jasmine_package_path,
             scripts,
             package_manager,
         }
     }
 
     fn merge(&mut self, other: Self) {
-        self.jest |= other.jest;
-        self.mocha |= other.mocha;
-        self.vitest |= other.vitest;
-        self.jasmine |= other.jasmine;
+        self.jest_package_path = self.jest_package_path.take().or(other.jest_package_path);
+        self.mocha_package_path = self.mocha_package_path.take().or(other.mocha_package_path);
+        self.vitest_package_path = self
+            .vitest_package_path
+            .take()
+            .or(other.vitest_package_path);
+        self.jasmine_package_path = self
+            .jasmine_package_path
+            .take()
+            .or(other.jasmine_package_path);
         self.scripts.extend(other.scripts);
+        self.package_manager = self.package_manager.or(other.package_manager);
     }
 
     fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
-        if self.jest {
+        if self.jest_package_path.is_some() {
             task_templates.0.push(TaskTemplate {
                 label: "jest file test".to_owned(),
                 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
-                args: vec![
-                    "jest".to_owned(),
-                    VariableName::RelativeFile.template_value(),
-                ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                args: vec!["jest".to_owned(), VariableName::Filename.template_value()],
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
             task_templates.0.push(TaskTemplate {
@@ -140,28 +178,28 @@ impl PackageJsonData {
                         "\"{}\"",
                         TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
                     ),
-                    VariableName::RelativeFile.template_value(),
+                    VariableName::Filename.template_value(),
                 ],
                 tags: vec![
                     "ts-test".to_owned(),
                     "js-test".to_owned(),
                     "tsx-test".to_owned(),
                 ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
         }
 
-        if self.vitest {
+        if self.vitest_package_path.is_some() {
             task_templates.0.push(TaskTemplate {
                 label: format!("{} file test", "vitest".to_owned()),
                 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
                 args: vec![
                     "vitest".to_owned(),
                     "run".to_owned(),
-                    VariableName::RelativeFile.template_value(),
+                    VariableName::Filename.template_value(),
                 ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
             task_templates.0.push(TaskTemplate {
@@ -179,27 +217,24 @@ impl PackageJsonData {
                         "\"{}\"",
                         TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
                     ),
-                    VariableName::RelativeFile.template_value(),
+                    VariableName::Filename.template_value(),
                 ],
                 tags: vec![
                     "ts-test".to_owned(),
                     "js-test".to_owned(),
                     "tsx-test".to_owned(),
                 ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
         }
 
-        if self.mocha {
+        if self.mocha_package_path.is_some() {
             task_templates.0.push(TaskTemplate {
                 label: format!("{} file test", "mocha".to_owned()),
                 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
-                args: vec![
-                    "mocha".to_owned(),
-                    VariableName::RelativeFile.template_value(),
-                ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                args: vec!["mocha".to_owned(), VariableName::Filename.template_value()],
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
             task_templates.0.push(TaskTemplate {
@@ -213,27 +248,27 @@ impl PackageJsonData {
                     "mocha".to_owned(),
                     "--grep".to_owned(),
                     format!("\"{}\"", VariableName::Symbol.template_value()),
-                    VariableName::RelativeFile.template_value(),
+                    VariableName::Filename.template_value(),
                 ],
                 tags: vec![
                     "ts-test".to_owned(),
                     "js-test".to_owned(),
                     "tsx-test".to_owned(),
                 ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
         }
 
-        if self.jasmine {
+        if self.jasmine_package_path.is_some() {
             task_templates.0.push(TaskTemplate {
                 label: format!("{} file test", "jasmine".to_owned()),
                 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
                 args: vec![
                     "jasmine".to_owned(),
-                    VariableName::RelativeFile.template_value(),
+                    VariableName::Filename.template_value(),
                 ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
             task_templates.0.push(TaskTemplate {
@@ -246,30 +281,31 @@ impl PackageJsonData {
                 args: vec![
                     "jasmine".to_owned(),
                     format!("--filter={}", VariableName::Symbol.template_value()),
-                    VariableName::RelativeFile.template_value(),
+                    VariableName::Filename.template_value(),
                 ],
                 tags: vec![
                     "ts-test".to_owned(),
                     "js-test".to_owned(),
                     "tsx-test".to_owned(),
+                    "jasmine-test".to_owned(),
                 ],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(VariableName::Dirname.template_value()),
                 ..TaskTemplate::default()
             });
         }
 
-        for script in &self.scripts {
+        for (path, script) in &self.scripts {
             task_templates.0.push(TaskTemplate {
                 label: format!("package.json > {script}",),
                 command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
-                args: vec![
-                    "--prefix".to_owned(),
-                    VariableName::WorktreeRoot.template_value(),
-                    "run".to_owned(),
-                    script.to_owned(),
-                ],
+                args: vec!["run".to_owned(), script.to_owned()],
                 tags: vec!["package-script".into()],
-                cwd: Some(VariableName::WorktreeRoot.template_value()),
+                cwd: Some(
+                    path.parent()
+                        .unwrap_or(Path::new(""))
+                        .to_string_lossy()
+                        .to_string(),
+                ),
                 ..TaskTemplate::default()
             });
         }
@@ -287,13 +323,9 @@ impl TypeScriptContextProvider {
         &self,
         fs: Arc<dyn Fs>,
         worktree_root: &Path,
-        file_abs_path: &Path,
+        file_relative_path: &Path,
         cx: &App,
     ) -> Task<anyhow::Result<PackageJsonData>> {
-        let Some(file_relative_path) = file_abs_path.strip_prefix(&worktree_root).ok() else {
-            log::debug!("No package json data for off-worktree files");
-            return Task::ready(Ok(PackageJsonData::default()));
-        };
         let new_json_data = file_relative_path
             .ancestors()
             .map(|path| worktree_root.join(path))
@@ -345,7 +377,8 @@ impl TypeScriptContextProvider {
                         serde_json::from_str(&package_json_string).with_context(|| {
                             format!("parsing package.json from {package_json_path:?}")
                         })?;
-                    let new_data = PackageJsonData::new(package_json);
+                    let new_data =
+                        PackageJsonData::new(package_json_path.as_path().into(), package_json);
                     {
                         let mut contents = existing_package_json.0.write().await;
                         contents.insert(
@@ -361,31 +394,25 @@ impl TypeScriptContextProvider {
             }
         })
     }
+}
 
-    fn detect_package_manager(
-        &self,
-        worktree_root: PathBuf,
-        fs: Arc<dyn Fs>,
-        cx: &App,
-    ) -> Task<&'static str> {
-        let last_package_json = self.last_package_json.clone();
-        let package_json_data =
-            self.package_json_data(&worktree_root, last_package_json, fs.clone(), cx);
-        cx.background_spawn(async move {
-            if let Ok(package_json_data) = package_json_data.await {
-                if let Some(package_manager) = package_json_data.package_manager {
-                    return package_manager;
-                }
-            }
-            if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
-                return "pnpm";
-            }
-            if fs.is_file(&worktree_root.join("yarn.lock")).await {
-                return "yarn";
-            }
-            "npm"
-        })
+async fn detect_package_manager(
+    worktree_root: PathBuf,
+    fs: Arc<dyn Fs>,
+    package_json_data: Option<PackageJsonData>,
+) -> &'static str {
+    if let Some(package_json_data) = package_json_data {
+        if let Some(package_manager) = package_json_data.package_manager {
+            return package_manager;
+        }
+    }
+    if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
+        return "pnpm";
     }
+    if fs.is_file(&worktree_root.join("yarn.lock")).await {
+        return "yarn";
+    }
+    "npm"
 }
 
 impl ContextProvider for TypeScriptContextProvider {
@@ -401,9 +428,9 @@ impl ContextProvider for TypeScriptContextProvider {
         let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
             return Task::ready(None);
         };
-        let file_abs_path = file.abs_path(cx);
+        let file_relative_path = file.path().clone();
         let package_json_data =
-            self.combined_package_json_data(fs.clone(), &worktree_root, &file_abs_path, cx);
+            self.combined_package_json_data(fs.clone(), &worktree_root, &file_relative_path, cx);
 
         cx.background_spawn(async move {
             let mut task_templates = TaskTemplates(Vec::new());
@@ -426,7 +453,7 @@ impl ContextProvider for TypeScriptContextProvider {
                 }
                 Err(e) => {
                     log::error!(
-                        "Failed to read package.json for worktree {file_abs_path:?}: {e:#}"
+                        "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
                     );
                 }
             }
@@ -455,14 +482,73 @@ impl ContextProvider for TypeScriptContextProvider {
                 replace_test_name_parameters(symbol),
             );
         }
-
-        let task = location
-            .worktree_root
-            .zip(location.fs)
-            .map(|(worktree_root, fs)| self.detect_package_manager(worktree_root, fs, cx));
+        let file_path = location
+            .file_location
+            .buffer
+            .read(cx)
+            .file()
+            .map(|file| file.path());
+
+        let args = location.worktree_root.zip(location.fs).zip(file_path).map(
+            |((worktree_root, fs), file_path)| {
+                (
+                    self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
+                    worktree_root,
+                    fs,
+                )
+            },
+        );
         cx.background_spawn(async move {
-            if let Some(task) = task {
-                vars.insert(TYPESCRIPT_RUNNER_VARIABLE, task.await.to_owned());
+            if let Some((task, worktree_root, fs)) = args {
+                let package_json_data = task.await.log_err();
+                vars.insert(
+                    TYPESCRIPT_RUNNER_VARIABLE,
+                    detect_package_manager(worktree_root, fs, package_json_data.clone())
+                        .await
+                        .to_owned(),
+                );
+
+                if let Some(package_json_data) = package_json_data {
+                    if let Some(path) = package_json_data.jest_package_path {
+                        vars.insert(
+                            TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
+                            path.parent()
+                                .unwrap_or(Path::new(""))
+                                .to_string_lossy()
+                                .to_string(),
+                        );
+                    }
+
+                    if let Some(path) = package_json_data.mocha_package_path {
+                        vars.insert(
+                            TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
+                            path.parent()
+                                .unwrap_or(Path::new(""))
+                                .to_string_lossy()
+                                .to_string(),
+                        );
+                    }
+
+                    if let Some(path) = package_json_data.vitest_package_path {
+                        vars.insert(
+                            TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
+                            path.parent()
+                                .unwrap_or(Path::new(""))
+                                .to_string_lossy()
+                                .to_string(),
+                        );
+                    }
+
+                    if let Some(path) = package_json_data.jasmine_package_path {
+                        vars.insert(
+                            TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
+                            path.parent()
+                                .unwrap_or(Path::new(""))
+                                .to_string_lossy()
+                                .to_string(),
+                        );
+                    }
+                }
             }
             Ok(vars)
         })
@@ -991,8 +1077,16 @@ async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
 
 #[cfg(test)]
 mod tests {
-    use gpui::{AppContext as _, TestAppContext};
+    use std::path::Path;
+
+    use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
+    use language::language_settings;
+    use project::{FakeFs, Project};
+    use serde_json::json;
     use unindent::Unindent;
+    use util::path;
+
+    use crate::typescript::{PackageJsonData, TypeScriptContextProvider};
 
     #[gpui::test]
     async fn test_outline(cx: &mut TestAppContext) {
@@ -1033,4 +1127,82 @@ mod tests {
             ]
         );
     }
+
+    #[gpui::test]
+    async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            settings::init(cx);
+            Project::init_settings(cx);
+            language_settings::init(cx);
+        });
+
+        let package_json_1 = json!({
+            "dependencies": {
+                "mocha": "1.0.0",
+                "vitest": "1.0.0"
+            },
+            "scripts": {
+                "test": ""
+            }
+        })
+        .to_string();
+
+        let package_json_2 = json!({
+            "devDependencies": {
+                "vitest": "2.0.0"
+            },
+            "scripts": {
+                "test": ""
+            }
+        })
+        .to_string();
+
+        let fs = FakeFs::new(executor);
+        fs.insert_tree(
+            path!("/root"),
+            json!({
+                "package.json": package_json_1,
+                "sub": {
+                    "package.json": package_json_2,
+                    "file.js": "",
+                }
+            }),
+        )
+        .await;
+
+        let provider = TypeScriptContextProvider::new();
+        let package_json_data = cx
+            .update(|cx| {
+                provider.combined_package_json_data(
+                    fs.clone(),
+                    path!("/root").as_ref(),
+                    "sub/file1.js".as_ref(),
+                    cx,
+                )
+            })
+            .await
+            .unwrap();
+        pretty_assertions::assert_eq!(
+            package_json_data,
+            PackageJsonData {
+                jest_package_path: None,
+                mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
+                vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
+                jasmine_package_path: None,
+                scripts: [
+                    (
+                        Path::new(path!("/root/package.json")).into(),
+                        "test".to_owned()
+                    ),
+                    (
+                        Path::new(path!("/root/sub/package.json")).into(),
+                        "test".to_owned()
+                    )
+                ]
+                .into_iter()
+                .collect(),
+                package_manager: None,
+            }
+        );
+    }
 }

crates/project/src/debugger/locators/node.rs 🔗

@@ -1,4 +1,4 @@
-use std::{borrow::Cow, path::Path};
+use std::{borrow::Cow, path::PathBuf};
 
 use anyhow::{Result, bail};
 use async_trait::async_trait;
@@ -11,8 +11,6 @@ pub(crate) struct NodeLocator;
 
 const TYPESCRIPT_RUNNER_VARIABLE: VariableName =
     VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER"));
-const TYPESCRIPT_JEST_TASK_VARIABLE: VariableName =
-    VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST"));
 
 #[async_trait]
 impl DapLocator for NodeLocator {
@@ -34,14 +32,21 @@ impl DapLocator for NodeLocator {
             return None;
         }
         let test_library = build_config.args.first()?;
-        let program_path = Path::new("$ZED_WORKTREE_ROOT")
+        let program_path_base: PathBuf = match test_library.as_str() {
+            "jest" => "${ZED_CUSTOM_TYPESCRIPT_JEST_PACKAGE_PATH}".to_owned(),
+            "mocha" => "${ZED_CUSTOM_TYPESCRIPT_MOCHA_PACKAGE_PATH}".to_owned(),
+            "vitest" => "${ZED_CUSTOM_TYPESCRIPT_VITEST_PACKAGE_PATH}".to_owned(),
+            "jasmine" => "${ZED_CUSTOM_TYPESCRIPT_JASMINE_PACKAGE_PATH}".to_owned(),
+            _ => VariableName::WorktreeRoot.template_value(),
+        }
+        .into();
+
+        let program_path = program_path_base
             .join("node_modules")
             .join(".bin")
             .join(test_library);
 
-        let mut args = if test_library == "jest"
-            || test_library == &TYPESCRIPT_JEST_TASK_VARIABLE.template_value()
-        {
+        let mut args = if test_library == "jest" {
             vec!["--runInBand".to_owned()]
         } else {
             vec![]

crates/project/src/toolchain_store.rs 🔗

@@ -278,7 +278,7 @@ impl language::LanguageToolchainStore for RemoteStore {
     }
 }
 
-pub(crate) struct EmptyToolchainStore;
+pub struct EmptyToolchainStore;
 #[async_trait(?Send)]
 impl language::LanguageToolchainStore for EmptyToolchainStore {
     async fn active_toolchain(