diff --git a/codex-rs/core-skills/src/loader.rs b/codex-rs/core-skills/src/loader.rs index 1953eb6053..2a3a3189be 100644 --- a/codex-rs/core-skills/src/loader.rs +++ b/codex-rs/core-skills/src/loader.rs @@ -13,34 +13,18 @@ use codex_config::default_project_root_markers; use codex_config::merge_toml_values; use codex_config::project_root_markers_from_config; use codex_exec_server::ExecutorFileSystem; -use codex_exec_server::ExecutorRpcBatchCall; -use codex_exec_server::ExecutorRpcBatchResult; -use codex_exec_server::FS_CANONICALIZE_METHOD; -use codex_exec_server::FS_GET_METADATA_METHOD; -use codex_exec_server::FS_READ_DIRECTORY_METHOD; -use codex_exec_server::FS_READ_FILE_METHOD; -use codex_exec_server::FileMetadata; -use codex_exec_server::FsCanonicalizeParams; -use codex_exec_server::FsCanonicalizeResponse; -use codex_exec_server::FsGetMetadataParams; -use codex_exec_server::FsGetMetadataResponse; -use codex_exec_server::FsReadDirectoryParams; -use codex_exec_server::FsReadDirectoryResponse; -use codex_exec_server::FsReadFileParams; -use codex_exec_server::FsReadFileResponse; use codex_exec_server::LOCAL_FS; use codex_protocol::protocol::Product; use codex_protocol::protocol::SkillScope; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::AbsolutePathBufGuard; use codex_utils_path_uri::PathUri; -use codex_utils_plugins::PluginNamespaceResolver; use codex_utils_plugins::PluginSkillRoot; +use codex_utils_plugins::plugin_namespace_for_skill_path; use dirs::home_dir; use serde::Deserialize; -use serde::Serialize; -use serde::de::DeserializeOwned; use std::collections::HashSet; +use std::collections::VecDeque; use std::error::Error; use std::fmt; use std::io; @@ -84,12 +68,6 @@ struct LoadedSkillMetadata { policy: Option, } -struct PreloadedSkillFile { - contents: io::Result, - metadata_contents: io::Result, - resolved_path: AbsolutePathBuf, -} - #[derive(Debug, Default, Deserialize)] struct Interface { display_name: Option, @@ -516,7 +494,7 @@ async fn discover_skills_under_root( } fn enqueue_dir( - queue: &mut Vec<(AbsolutePathBuf, usize)>, + queue: &mut VecDeque<(AbsolutePathBuf, usize)>, visited_dirs: &mut HashSet, truncated_by_dir_limit: &mut bool, path: AbsolutePathBuf, @@ -530,7 +508,7 @@ async fn discover_skills_under_root( return; } if visited_dirs.insert(path.clone()) { - queue.push((path, depth)); + queue.push_back((path, depth)); } } @@ -543,196 +521,101 @@ async fn discover_skills_under_root( let mut visited_dirs: HashSet = HashSet::new(); visited_dirs.insert(root.clone()); - let mut queue = vec![(root.clone(), 0)]; + let mut queue: VecDeque<(AbsolutePathBuf, usize)> = VecDeque::from([(root.clone(), 0)]); let mut truncated_by_dir_limit = false; - let mut skill_paths = Vec::new(); - while !queue.is_empty() { - let dirs = std::mem::take(&mut queue); - let directory_calls = match dirs - .iter() - .map(|(dir, _)| { - rpc_batch_call( - FS_READ_DIRECTORY_METHOD, - FsReadDirectoryParams { - path: PathUri::from_abs_path(dir), - sandbox: None, - }, - ) - }) - .collect::>>() - { - Ok(calls) => calls, - Err(err) => { - error!("failed to build batched skills directory reads: {err:#}"); - break; - } - }; - let directory_results = match fs.execute_rpc_batch(directory_calls).await { - Ok(results) => results, - Err(err) => { - error!("failed to batch-read skills directories: {err:#}"); - break; + while let Some((dir, depth)) = queue.pop_front() { + let dir_uri = PathUri::from_abs_path(&dir); + let entries = match fs.read_directory(&dir_uri, /*sandbox*/ None).await { + Ok(entries) => entries, + Err(e) => { + error!("failed to read skills dir {}: {e:#}", dir.display()); + continue; } }; - let mut candidates = Vec::new(); - for ((dir, depth), result) in dirs.into_iter().zip(directory_results) { - let response: FsReadDirectoryResponse = - match decode_rpc_batch_result(Some(result), "read skills dir") { - Ok(response) => response, - Err(err) => { - error!("failed to read skills dir {}: {err:#}", dir.display()); - continue; - } - }; - candidates.extend(response.entries.into_iter().filter_map(|entry| { - let file_name = entry.file_name; - if file_name.starts_with('.') { - return None; + for entry in entries { + let file_name = entry.file_name; + if file_name.starts_with('.') { + continue; + } + + let path = dir.join(&file_name); + let path_uri = PathUri::from_abs_path(&path); + let metadata = match fs.get_metadata(&path_uri, /*sandbox*/ None).await { + Ok(metadata) => metadata, + Err(e) => { + error!("failed to stat skills path {}: {e:#}", path.display()); + continue; } - Some((dir.join(&file_name), depth, file_name)) - })); - } - - let metadata_calls = match candidates - .iter() - .map(|(path, _, _)| { - rpc_batch_call( - FS_GET_METADATA_METHOD, - FsGetMetadataParams { - path: PathUri::from_abs_path(path), - sandbox: None, - }, - ) - }) - .collect::>>() - { - Ok(calls) => calls, - Err(err) => { - error!("failed to build batched skills path stats: {err:#}"); - break; - } - }; - let metadata_results = match fs.execute_rpc_batch(metadata_calls).await { - Ok(results) => results, - Err(err) => { - error!("failed to batch-stat skills paths: {err:#}"); - break; - } - }; - - let mut directories = Vec::new(); - for ((path, depth, file_name), result) in candidates.into_iter().zip(metadata_results) { - let response: FsGetMetadataResponse = - match decode_rpc_batch_result(Some(result), "stat skills path") { - Ok(response) => response, - Err(err) => { - error!("failed to stat skills path {}: {err:#}", path.display()); - continue; - } - }; - let metadata = FileMetadata { - is_directory: response.is_directory, - is_file: response.is_file, - is_symlink: response.is_symlink, - size: response.size, - created_at_ms: response.created_at_ms, - modified_at_ms: response.modified_at_ms, }; + if metadata.is_symlink { - if follow_symlinks && metadata.is_directory { - directories.push((path, depth + 1)); + if !follow_symlinks { + continue; } - } else if metadata.is_directory { - directories.push((path, depth + 1)); - } else if metadata.is_file && file_name == SKILLS_FILENAME { - skill_paths.push(path); + match fs.read_directory(&path_uri, /*sandbox*/ None).await { + Ok(_) => { + let resolved_dir = canonicalize_for_skill_identity(fs, &path).await; + enqueue_dir( + &mut queue, + &mut visited_dirs, + &mut truncated_by_dir_limit, + resolved_dir, + depth + 1, + ); + } + Err(err) + if matches!( + err.kind(), + io::ErrorKind::NotADirectory | io::ErrorKind::NotFound + ) => {} + Err(err) => { + error!( + "failed to read skills symlink dir {}: {err:#}", + path.display() + ); + } + } + continue; } - } - let canonical_calls = match directories - .iter() - .map(|(path, _)| { - rpc_batch_call( - FS_CANONICALIZE_METHOD, - FsCanonicalizeParams { - path: PathUri::from_abs_path(path), - sandbox: None, - }, + if metadata.is_directory { + let resolved_dir = canonicalize_for_skill_identity(fs, &path).await; + enqueue_dir( + &mut queue, + &mut visited_dirs, + &mut truncated_by_dir_limit, + resolved_dir, + depth + 1, + ); + continue; + } + + if metadata.is_file && file_name == SKILLS_FILENAME { + match parse_skill_file( + fs, + &path, + scope, + plugin_id, + plugin_namespace, + plugin_root.as_ref(), ) - }) - .collect::>>() - { - Ok(calls) => calls, - Err(err) => { - error!("failed to build batched skills directory canonicalization: {err:#}"); - break; - } - }; - let canonical_results = match fs.execute_rpc_batch(canonical_calls).await { - Ok(results) => results, - Err(err) => { - error!("failed to batch-canonicalize skills directories: {err:#}"); - break; - } - }; - for ((path, depth), result) in directories.into_iter().zip(canonical_results) { - let resolved_dir = match decode_rpc_batch_result::( - Some(result), - "canonicalize skills dir", - ) { - Ok(response) => response.path.to_abs_path().unwrap_or_else(|_| path.clone()), - Err(err) => { - error!( - "failed to canonicalize skills dir {}: {err:#}", - path.display() - ); - path + .await + { + Ok(skill) => { + outcome.skills.push(skill); + } + Err(err) => { + if scope != SkillScope::System { + outcome.errors.push(SkillError { + path: path.clone(), + message: err.to_string(), + }); + } + } } - }; - enqueue_dir( - &mut queue, - &mut visited_dirs, - &mut truncated_by_dir_limit, - resolved_dir, - depth, - ); - } - } - - let plugin_namespace_resolver = if plugin_namespace.is_none() { - Some(PluginNamespaceResolver::load(fs, &skill_paths).await) - } else { - None - }; - let preloaded_skills = match preload_skill_files(fs, &skill_paths).await { - Ok(preloaded_skills) => preloaded_skills, - Err(err) => { - error!("failed to batch-read skill files: {err:#}"); - Vec::new() - } - }; - for (path, preloaded) in skill_paths.into_iter().zip(preloaded_skills) { - let plugin_namespace = plugin_namespace.or_else(|| { - plugin_namespace_resolver - .as_ref() - .and_then(|resolver| resolver.resolve(&path)) - }); - match parse_skill_file( - &path, - scope, - plugin_id, - plugin_namespace, - plugin_root.as_ref(), - preloaded, - ) { - Ok(skill) => outcome.skills.push(skill), - Err(err) if scope != SkillScope::System => outcome.errors.push(SkillError { - path, - message: err.to_string(), - }), - Err(_) => {} + } } } @@ -745,116 +628,19 @@ async fn discover_skills_under_root( } } -async fn preload_skill_files( +async fn parse_skill_file( fs: &dyn ExecutorFileSystem, - skill_paths: &[AbsolutePathBuf], -) -> io::Result> { - let calls = skill_paths - .iter() - .flat_map(|path| { - let metadata_path = path - .parent() - .map(|parent| { - parent - .join(SKILLS_METADATA_DIR) - .join(SKILLS_METADATA_FILENAME) - }) - .unwrap_or_else(|| path.clone()); - [ - rpc_batch_call( - FS_READ_FILE_METHOD, - FsReadFileParams { - path: PathUri::from_abs_path(path), - sandbox: None, - }, - ), - rpc_batch_call( - FS_READ_FILE_METHOD, - FsReadFileParams { - path: PathUri::from_abs_path(&metadata_path), - sandbox: None, - }, - ), - rpc_batch_call( - FS_CANONICALIZE_METHOD, - FsCanonicalizeParams { - path: PathUri::from_abs_path(path), - sandbox: None, - }, - ), - ] - }) - .collect::>>()?; - let mut results = fs.execute_rpc_batch(calls).await?.into_iter(); - - Ok(skill_paths - .iter() - .map(|path| { - let contents = decode_text_rpc_result(results.next(), "read skill file"); - let metadata_contents = decode_text_rpc_result(results.next(), "read skill metadata"); - let resolved_path = match decode_rpc_batch_result::( - results.next(), - "canonicalize skill path", - ) { - Ok(response) => response.path.to_abs_path().unwrap_or_else(|_| path.clone()), - Err(_) => path.clone(), - }; - PreloadedSkillFile { - contents, - metadata_contents, - resolved_path, - } - }) - .collect()) -} - -fn rpc_batch_call(method: &str, params: P) -> io::Result { - Ok(ExecutorRpcBatchCall { - method: method.to_string(), - params: serde_json::to_value(params).map_err(io::Error::other)?, - }) -} - -fn decode_rpc_batch_result( - result: Option, - operation: &str, -) -> io::Result { - match result { - Some(Ok(value)) => serde_json::from_value(value) - .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)), - Some(Err(error)) => Err(error), - None => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("filesystem returned no result for {operation}"), - )), - } -} - -fn decode_text_rpc_result( - result: Option, - operation: &str, -) -> io::Result { - let response: FsReadFileResponse = decode_rpc_batch_result(result, operation)?; - let contents = response - .into_bytes() - .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?; - String::from_utf8(contents).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)) -} - -fn parse_skill_file( path: &AbsolutePathBuf, scope: SkillScope, plugin_id: Option<&str>, plugin_namespace: Option<&str>, plugin_root: Option<&AbsolutePathBuf>, - preloaded: PreloadedSkillFile, ) -> Result { - let PreloadedSkillFile { - contents, - metadata_contents, - resolved_path, - } = preloaded; - let contents = contents.map_err(SkillParseError::Read)?; + let path_uri = PathUri::from_abs_path(path); + let contents = fs + .read_file_text(&path_uri, /*sandbox*/ None) + .await + .map_err(SkillParseError::Read)?; let frontmatter = extract_frontmatter(&contents).ok_or(SkillParseError::MissingFrontmatter)?; @@ -878,9 +664,7 @@ fn parse_skill_file( .map(sanitize_single_line) .filter(|value| !value.is_empty()) .unwrap_or_else(|| default_skill_name(path)); - let name = plugin_namespace - .map(|namespace| format!("{namespace}:{base_name}")) - .unwrap_or_else(|| base_name.clone()); + let name = namespaced_skill_name(fs, path, &base_name, plugin_namespace).await; let description = parsed .description .as_deref() @@ -896,7 +680,7 @@ fn parse_skill_file( interface, dependencies, policy, - } = load_skill_metadata(path, plugin_root, metadata_contents); + } = load_skill_metadata(fs, path, plugin_root).await; validate_len(&base_name, MAX_NAME_LEN, "name")?; validate_len(&name, MAX_QUALIFIED_NAME_LEN, "qualified name")?; @@ -909,6 +693,8 @@ fn parse_skill_file( )?; } + let resolved_path = canonicalize_for_skill_identity(fs, path).await; + Ok(SkillMetadata { name, description, @@ -934,10 +720,25 @@ fn default_skill_name(path: &AbsolutePathBuf) -> String { .unwrap_or_else(|| "skill".to_string()) } -fn load_skill_metadata( +async fn namespaced_skill_name( + fs: &dyn ExecutorFileSystem, + path: &AbsolutePathBuf, + base_name: &str, + plugin_namespace: Option<&str>, +) -> String { + if let Some(plugin_namespace) = plugin_namespace { + return format!("{plugin_namespace}:{base_name}"); + } + plugin_namespace_for_skill_path(fs, path) + .await + .map(|namespace| format!("{namespace}:{base_name}")) + .unwrap_or_else(|| base_name.to_string()) +} + +async fn load_skill_metadata( + fs: &dyn ExecutorFileSystem, skill_path: &AbsolutePathBuf, plugin_root: Option<&AbsolutePathBuf>, - contents: io::Result, ) -> LoadedSkillMetadata { // Fail open: optional metadata should not block loading SKILL.md. let Some(skill_dir) = skill_path.parent() else { @@ -946,11 +747,28 @@ fn load_skill_metadata( let metadata_path = skill_dir .join(SKILLS_METADATA_DIR) .join(SKILLS_METADATA_FILENAME); - let contents = match contents { - Ok(contents) => contents, + let metadata_path_uri = PathUri::from_abs_path(&metadata_path); + match fs.get_metadata(&metadata_path_uri, /*sandbox*/ None).await { + Ok(metadata) if metadata.is_file => {} + Ok(_) => return LoadedSkillMetadata::default(), Err(error) if error.kind() == io::ErrorKind::NotFound => { return LoadedSkillMetadata::default(); } + Err(error) => { + tracing::warn!( + "ignoring {path}: failed to stat {label}: {error}", + path = metadata_path.display(), + label = SKILLS_METADATA_FILENAME + ); + return LoadedSkillMetadata::default(); + } + } + + let contents = match fs + .read_file_text(&metadata_path_uri, /*sandbox*/ None) + .await + { + Ok(contents) => contents, Err(error) => { tracing::warn!( "ignoring {path}: failed to read {label}: {error}", diff --git a/codex-rs/utils/plugins/src/lib.rs b/codex-rs/utils/plugins/src/lib.rs index 238d11f7cb..203b1f22e4 100644 --- a/codex-rs/utils/plugins/src/lib.rs +++ b/codex-rs/utils/plugins/src/lib.rs @@ -8,7 +8,6 @@ pub mod mention_syntax; pub mod plugin_namespace; pub use plugin_namespace::DISCOVERABLE_PLUGIN_MANIFEST_PATHS; -pub use plugin_namespace::PluginNamespaceResolver; pub use plugin_namespace::find_plugin_manifest_path; pub use plugin_namespace::plugin_namespace_for_skill_path; diff --git a/codex-rs/utils/plugins/src/plugin_namespace.rs b/codex-rs/utils/plugins/src/plugin_namespace.rs index 4d3573e13d..b29ff5c5d6 100644 --- a/codex-rs/utils/plugins/src/plugin_namespace.rs +++ b/codex-rs/utils/plugins/src/plugin_namespace.rs @@ -1,19 +1,8 @@ //! Resolve plugin namespace from skill file paths by walking ancestors for `plugin.json`. use codex_exec_server::ExecutorFileSystem; -use codex_exec_server::ExecutorRpcBatchCall; -use codex_exec_server::ExecutorRpcBatchResult; -use codex_exec_server::FS_GET_METADATA_METHOD; -use codex_exec_server::FS_READ_FILE_METHOD; -use codex_exec_server::FsGetMetadataParams; -use codex_exec_server::FsGetMetadataResponse; -use codex_exec_server::FsReadFileParams; -use codex_exec_server::FsReadFileResponse; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; -use std::collections::HashMap; -use std::collections::HashSet; -use std::io; use std::path::Path; use std::path::PathBuf; @@ -35,8 +24,29 @@ struct RawPluginManifestName { name: String, } -fn plugin_manifest_name(plugin_root: &AbsolutePathBuf, contents: &[u8]) -> Option { - let RawPluginManifestName { name: raw_name } = serde_json::from_slice(contents).ok()?; +async fn plugin_manifest_name( + fs: &dyn ExecutorFileSystem, + plugin_root: &AbsolutePathBuf, +) -> Option { + let mut manifest_path = None; + for relative_path in DISCOVERABLE_PLUGIN_MANIFEST_PATHS { + let candidate = plugin_root.join(relative_path); + let candidate_uri = PathUri::from_abs_path(&candidate); + match fs.get_metadata(&candidate_uri, /*sandbox*/ None).await { + Ok(metadata) if metadata.is_file => { + manifest_path = Some(candidate); + break; + } + Ok(_) | Err(_) => {} + } + } + let manifest_path = manifest_path?; + let manifest_path_uri = PathUri::from_abs_path(&manifest_path); + let contents = fs + .read_file_text(&manifest_path_uri, /*sandbox*/ None) + .await + .ok()?; + let RawPluginManifestName { name: raw_name } = serde_json::from_str(&contents).ok()?; Some( plugin_root .file_name() @@ -47,124 +57,18 @@ fn plugin_manifest_name(plugin_root: &AbsolutePathBuf, contents: &[u8]) -> Optio ) } -/// Resolves plugin namespaces while caching and batching ancestor manifest probes. -pub struct PluginNamespaceResolver { - manifests_by_root: HashMap, -} - -impl PluginNamespaceResolver { - /// Loads plugin manifest names for every ancestor of `paths`. - pub async fn load(fs: &dyn ExecutorFileSystem, paths: &[AbsolutePathBuf]) -> Self { - let mut seen = HashSet::new(); - let roots = paths - .iter() - .flat_map(AbsolutePathBuf::ancestors) - .filter(|ancestor| seen.insert(ancestor.clone())) - .collect::>(); - let calls = roots - .iter() - .flat_map(|root| { - DISCOVERABLE_PLUGIN_MANIFEST_PATHS - .iter() - .flat_map(|relative_path| { - let path = PathUri::from_abs_path(&root.join(relative_path)); - [ - rpc_batch_call( - FS_GET_METADATA_METHOD, - FsGetMetadataParams { - path: path.clone(), - sandbox: None, - }, - ), - rpc_batch_call( - FS_READ_FILE_METHOD, - FsReadFileParams { - path, - sandbox: None, - }, - ), - ] - }) - }) - .collect::>>(); - let mut results = fs - .execute_rpc_batch(calls.unwrap_or_default()) - .await - .unwrap_or_default() - .into_iter(); - let manifests_by_root = roots - .into_iter() - .filter_map(|root| { - let mut name = None; - let mut selected = false; - for _ in DISCOVERABLE_PLUGIN_MANIFEST_PATHS { - let metadata_result = results.next(); - let contents_result = results.next(); - let is_file = decode_rpc_batch_result::( - metadata_result, - "stat plugin manifest", - ) - .is_ok_and(|metadata| metadata.is_file); - if !selected && is_file { - selected = true; - if let Ok(response) = decode_rpc_batch_result::( - contents_result, - "read plugin manifest", - ) && let Ok(contents) = response.into_bytes() - { - name = plugin_manifest_name(&root, &contents); - } - } - } - name.map(|name| (root, name)) - }) - .collect(); - - Self { manifests_by_root } - } - - /// Returns the nearest preloaded plugin namespace for `path`. - pub fn resolve(&self, path: &AbsolutePathBuf) -> Option<&str> { - path.ancestors() - .find_map(|ancestor| self.manifests_by_root.get(&ancestor).map(String::as_str)) - } -} - -fn rpc_batch_call( - method: &str, - params: P, -) -> io::Result { - Ok(ExecutorRpcBatchCall { - method: method.to_string(), - params: serde_json::to_value(params).map_err(io::Error::other)?, - }) -} - -fn decode_rpc_batch_result( - result: Option, - operation: &str, -) -> io::Result { - match result { - Some(Ok(value)) => serde_json::from_value(value) - .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error)), - Some(Err(error)) => Err(error), - None => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("filesystem returned no result for {operation}"), - )), - } -} - /// Returns the plugin manifest `name` for the nearest ancestor of `path` that contains a valid /// plugin manifest (same `name` rules as full manifest loading in codex-core). pub async fn plugin_namespace_for_skill_path( fs: &dyn ExecutorFileSystem, path: &AbsolutePathBuf, ) -> Option { - PluginNamespaceResolver::load(fs, std::slice::from_ref(path)) - .await - .resolve(path) - .map(str::to_string) + for ancestor in path.ancestors() { + if let Some(name) = plugin_manifest_name(fs, &ancestor).await { + return Some(name); + } + } + None } #[cfg(test)] @@ -176,7 +80,6 @@ mod tests { use std::fs; use tempfile::tempdir; - const PRIMARY_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".codex-plugin/plugin.json"; const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json"; #[tokio::test] @@ -188,7 +91,7 @@ mod tests { fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir"); fs::create_dir_all(plugin_root.join(".codex-plugin")).expect("mkdir manifest"); fs::write( - plugin_root.join(PRIMARY_PLUGIN_MANIFEST_RELATIVE_PATH), + plugin_root.join(".codex-plugin/plugin.json"), r#"{"name":"sample"}"#, ) .expect("write manifest"); @@ -219,32 +122,4 @@ mod tests { ); assert_eq!(find_plugin_manifest_path(&plugin_root), Some(manifest_path)); } - - #[tokio::test] - async fn invalid_primary_manifest_does_not_fall_back_to_alternate() { - let tmp = tempdir().expect("tempdir"); - let plugin_root = tmp.path().join("plugins/sample"); - let skill_path = plugin_root.join("skills/search/SKILL.md"); - let primary_manifest_path = plugin_root.join(PRIMARY_PLUGIN_MANIFEST_RELATIVE_PATH); - let alternate_manifest_path = plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH); - - fs::create_dir_all(skill_path.parent().expect("parent")).expect("mkdir"); - fs::create_dir_all(primary_manifest_path.parent().expect("manifest parent")) - .expect("mkdir primary manifest"); - fs::create_dir_all(alternate_manifest_path.parent().expect("manifest parent")) - .expect("mkdir alternate manifest"); - fs::write(&primary_manifest_path, "invalid json").expect("write primary manifest"); - fs::write(&alternate_manifest_path, r#"{"name":"sample"}"#) - .expect("write alternate manifest"); - fs::write(&skill_path, "---\ndescription: search\n---\n").expect("write skill"); - - assert_eq!( - plugin_namespace_for_skill_path(LOCAL_FS.as_ref(), &skill_path.abs()).await, - None - ); - assert_eq!( - find_plugin_manifest_path(&plugin_root), - Some(primary_manifest_path) - ); - } }