Compare commits

...

1 Commits

Author SHA1 Message Date
xli-oai
5d4d7e51e8 Read cached metadata for installed Git plugins 2026-05-01 21:13:34 -07:00
3 changed files with 248 additions and 4 deletions

View File

@@ -1051,6 +1051,123 @@ async fn plugin_list_accepts_legacy_string_default_prompt() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn plugin_list_returns_installed_git_source_interface_from_cache() -> Result<()> {
let codex_home = TempDir::new()?;
let repo_root = TempDir::new()?;
let missing_remote_repo = repo_root.path().join("missing-remote-plugin-repo");
let missing_remote_repo_url = url::Url::from_directory_path(&missing_remote_repo)
.unwrap()
.to_string();
std::fs::create_dir_all(repo_root.path().join(".git"))?;
std::fs::create_dir_all(repo_root.path().join(".agents/plugins"))?;
std::fs::write(
repo_root.path().join(".agents/plugins/marketplace.json"),
format!(
r#"{{
"name": "debug",
"plugins": [
{{
"name": "toolkit",
"source": {{
"source": "git-subdir",
"url": "{missing_remote_repo_url}",
"path": "plugins/toolkit"
}},
"category": "Developer Tools"
}}
]
}}"#
),
)?;
let cached_plugin_root = codex_home.path().join("plugins/cache/debug/toolkit/local");
std::fs::create_dir_all(cached_plugin_root.join(".codex-plugin"))?;
std::fs::write(
cached_plugin_root.join(".codex-plugin/plugin.json"),
r##"{
"name": "toolkit",
"interface": {
"displayName": "Toolkit",
"shortDescription": "Search cached data",
"category": "Cached Category",
"brandColor": "#3B82F6",
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png"
}
}"##,
)?;
std::fs::write(
codex_home.path().join("config.toml"),
r#"[features]
plugins = true
[plugins."toolkit@debug"]
enabled = true
"#,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_plugin_list_request(PluginListParams {
cwds: Some(vec![AbsolutePathBuf::try_from(repo_root.path())?]),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let response: PluginListResponse = to_response(response)?;
let plugin = response
.marketplaces
.iter()
.flat_map(|marketplace| marketplace.plugins.iter())
.find(|plugin| plugin.name == "toolkit")
.expect("expected toolkit entry");
assert_eq!(plugin.id, "toolkit@debug");
assert_eq!(plugin.installed, true);
assert_eq!(plugin.enabled, true);
assert_eq!(
plugin.source,
PluginSource::Git {
url: missing_remote_repo_url,
path: Some("plugins/toolkit".to_string()),
ref_name: None,
sha: None,
}
);
let interface = plugin
.interface
.as_ref()
.expect("expected cached plugin interface");
assert_eq!(interface.display_name.as_deref(), Some("Toolkit"));
assert_eq!(
interface.short_description.as_deref(),
Some("Search cached data")
);
assert_eq!(interface.category.as_deref(), Some("Developer Tools"));
assert_eq!(interface.brand_color.as_deref(), Some("#3B82F6"));
let canonical_cached_plugin_root = std::fs::canonicalize(&cached_plugin_root)?;
assert_eq!(
interface.composer_icon,
Some(AbsolutePathBuf::try_from(
canonical_cached_plugin_root.join("assets/icon.png")
)?)
);
assert_eq!(
interface.logo,
Some(AbsolutePathBuf::try_from(
canonical_cached_plugin_root.join("assets/logo.png")
)?)
);
Ok(())
}
#[tokio::test]
async fn app_server_startup_remote_plugin_sync_runs_once() -> Result<()> {
let codex_home = TempDir::new()?;

View File

@@ -1183,18 +1183,36 @@ impl PluginsManager {
if !self.restriction_product_matches(plugin.policy.products.as_deref()) {
return None;
}
let installed = installed_plugins.contains(&plugin_key);
let enabled = enabled_plugins.contains(&plugin_key);
let mut interface = plugin.interface;
if installed
&& matches!(&plugin.source, MarketplacePluginSource::Git { .. })
&& let Ok(plugin_id) =
PluginId::new(plugin.name.clone(), marketplace_name.clone())
&& let Some(plugin_root) = self.store.active_plugin_root(&plugin_id)
&& let Some(manifest) = load_plugin_manifest(plugin_root.as_path())
{
let marketplace_category = interface
.as_ref()
.and_then(|interface| interface.category.clone());
interface = plugin_interface_with_marketplace_category(
manifest.interface,
marketplace_category,
);
}
Some(ConfiguredMarketplacePlugin {
// Enabled state is keyed by `<plugin>@<marketplace>`, so duplicate
// plugin entries from duplicate marketplace files intentionally
// resolve to the first discovered source.
id: plugin_key.clone(),
installed: installed_plugins.contains(&plugin_key),
enabled: enabled_plugins.contains(&plugin_key),
id: plugin_key,
installed,
enabled,
name: plugin.name,
source: plugin.source,
policy: plugin.policy,
interface: plugin.interface,
interface,
})
})
.collect::<Vec<_>>();

View File

@@ -1983,6 +1983,115 @@ enabled = true
);
}
#[tokio::test]
async fn list_marketplaces_installed_git_source_reads_metadata_from_cache_without_cloning() {
let tmp = tempfile::tempdir().unwrap();
let repo_root = tmp.path().join("repo");
let missing_remote_repo = tmp.path().join("missing-remote-plugin-repo");
let missing_remote_repo_url = url::Url::from_directory_path(&missing_remote_repo)
.unwrap()
.to_string();
fs::create_dir_all(repo_root.join(".git")).unwrap();
write_file(
&repo_root.join(".agents/plugins/marketplace.json"),
&format!(
r#"{{
"name": "debug",
"plugins": [
{{
"name": "toolkit",
"source": {{
"source": "git-subdir",
"url": "{missing_remote_repo_url}",
"path": "plugins/toolkit"
}},
"category": "Developer Tools"
}}
]
}}"#
),
);
let cached_plugin_root = tmp.path().join("plugins/cache/debug/toolkit/local");
write_file(
&cached_plugin_root.join(".codex-plugin/plugin.json"),
r##"{
"name": "toolkit",
"interface": {
"displayName": "Toolkit",
"shortDescription": "Search cached data",
"category": "Cached Category",
"brandColor": "#3B82F6",
"composerIcon": "./assets/icon.png",
"logo": "./assets/logo.png",
"screenshots": ["./assets/screenshot.png"]
}
}"##,
);
write_file(
&tmp.path().join(CONFIG_TOML_FILE),
r#"[features]
plugins = true
[plugins."toolkit@debug"]
enabled = true
"#,
);
let config = load_config(tmp.path(), &repo_root).await;
let marketplaces = PluginsManager::new(tmp.path().to_path_buf())
.list_marketplaces_for_config(&config, &[AbsolutePathBuf::try_from(repo_root).unwrap()])
.unwrap()
.marketplaces;
let marketplace = marketplaces
.into_iter()
.find(|marketplace| marketplace.name == "debug")
.expect("debug marketplace should be listed");
assert_eq!(
marketplace.plugins,
vec![ConfiguredMarketplacePlugin {
id: "toolkit@debug".to_string(),
name: "toolkit".to_string(),
source: MarketplacePluginSource::Git {
url: missing_remote_repo_url,
path: Some("plugins/toolkit".to_string()),
ref_name: None,
sha: None,
},
policy: MarketplacePluginPolicy {
installation: MarketplacePluginInstallPolicy::Available,
authentication: MarketplacePluginAuthPolicy::OnInstall,
products: None,
},
interface: Some(PluginManifestInterface {
display_name: Some("Toolkit".to_string()),
short_description: Some("Search cached data".to_string()),
category: Some("Developer Tools".to_string()),
brand_color: Some("#3B82F6".to_string()),
composer_icon: Some(
AbsolutePathBuf::try_from(cached_plugin_root.join("assets/icon.png")).unwrap(),
),
logo: Some(
AbsolutePathBuf::try_from(cached_plugin_root.join("assets/logo.png")).unwrap(),
),
screenshots: vec![
AbsolutePathBuf::try_from(cached_plugin_root.join("assets/screenshot.png"))
.unwrap(),
],
..Default::default()
}),
installed: true,
enabled: true,
}]
);
assert!(
!tmp.path()
.join("plugins/.marketplace-plugin-source-staging")
.exists()
);
}
#[tokio::test]
async fn sync_plugins_from_remote_returns_default_when_feature_disabled() {
let tmp = tempfile::tempdir().unwrap();