mirror of
https://github.com/openai/codex.git
synced 2026-04-29 17:06:51 +00:00
feat: Handle alternate plugin manifest paths (#18182)
Load plugin manifests through a shared discoverable-path helper so manifest reads, installs, and skill names all see the same alternate manifest location.
This commit is contained in:
@@ -34,6 +34,8 @@ use wiremock::matchers::query_param;
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const TEST_CURATED_PLUGIN_SHA: &str = "0123456789abcdef0123456789abcdef01234567";
|
||||
const STARTUP_REMOTE_PLUGIN_SYNC_MARKER_FILE: &str = ".tmp/app-server-remote-plugin-sync-v1";
|
||||
const ALTERNATE_MARKETPLACE_RELATIVE_PATH: &str = ".claude-plugin/marketplace.json";
|
||||
const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
|
||||
|
||||
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
|
||||
std::fs::write(
|
||||
@@ -246,6 +248,138 @@ async fn plugin_list_keeps_valid_marketplaces_when_another_marketplace_fails_to_
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_uses_alternate_discoverable_manifest_and_keeps_undiscoverable_plugins()
|
||||
-> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo_root = TempDir::new()?;
|
||||
let valid_plugin_root = repo_root.path().join("plugins/valid-plugin");
|
||||
std::fs::create_dir_all(repo_root.path().join(".git"))?;
|
||||
std::fs::create_dir_all(
|
||||
repo_root
|
||||
.path()
|
||||
.join(ALTERNATE_MARKETPLACE_RELATIVE_PATH)
|
||||
.parent()
|
||||
.unwrap(),
|
||||
)?;
|
||||
std::fs::create_dir_all(
|
||||
valid_plugin_root
|
||||
.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH)
|
||||
.parent()
|
||||
.unwrap(),
|
||||
)?;
|
||||
write_plugins_enabled_config(codex_home.path())?;
|
||||
|
||||
let marketplace_path =
|
||||
AbsolutePathBuf::try_from(repo_root.path().join(ALTERNATE_MARKETPLACE_RELATIVE_PATH))?;
|
||||
let valid_plugin_path = AbsolutePathBuf::try_from(valid_plugin_root.clone())?;
|
||||
|
||||
std::fs::write(
|
||||
marketplace_path.as_path(),
|
||||
r#"{
|
||||
"name": "alternate-marketplace",
|
||||
"plugins": [
|
||||
{
|
||||
"name": "valid-plugin",
|
||||
"source": "./plugins/valid-plugin"
|
||||
},
|
||||
{
|
||||
"name": "missing-plugin",
|
||||
"source": "./plugins/missing-plugin"
|
||||
}
|
||||
]
|
||||
}"#,
|
||||
)?;
|
||||
std::fs::write(
|
||||
valid_plugin_root.join(ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH),
|
||||
r#"{
|
||||
"name": "valid-plugin",
|
||||
"interface": {
|
||||
"displayName": "Valid Plugin"
|
||||
}
|
||||
}"#,
|
||||
)?;
|
||||
|
||||
let home = codex_home.path().to_string_lossy().into_owned();
|
||||
let mut mcp = McpProcess::new_with_env(
|
||||
codex_home.path(),
|
||||
&[
|
||||
("HOME", Some(home.as_str())),
|
||||
("USERPROFILE", Some(home.as_str())),
|
||||
],
|
||||
)
|
||||
.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())?]),
|
||||
force_remote_sync: false,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let response: PluginListResponse = to_response(response)?;
|
||||
|
||||
assert_eq!(
|
||||
response.marketplaces,
|
||||
vec![PluginMarketplaceEntry {
|
||||
name: "alternate-marketplace".to_string(),
|
||||
path: marketplace_path,
|
||||
interface: None,
|
||||
plugins: vec![
|
||||
PluginSummary {
|
||||
id: "valid-plugin@alternate-marketplace".to_string(),
|
||||
name: "valid-plugin".to_string(),
|
||||
source: PluginSource::Local {
|
||||
path: valid_plugin_path,
|
||||
},
|
||||
installed: false,
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
interface: Some(codex_app_server_protocol::PluginInterface {
|
||||
display_name: Some("Valid Plugin".to_string()),
|
||||
short_description: None,
|
||||
long_description: None,
|
||||
developer_name: None,
|
||||
category: None,
|
||||
capabilities: Vec::new(),
|
||||
website_url: None,
|
||||
privacy_policy_url: None,
|
||||
terms_of_service_url: None,
|
||||
default_prompt: None,
|
||||
brand_color: None,
|
||||
composer_icon: None,
|
||||
logo: None,
|
||||
screenshots: Vec::new(),
|
||||
}),
|
||||
},
|
||||
PluginSummary {
|
||||
id: "missing-plugin@alternate-marketplace".to_string(),
|
||||
name: "missing-plugin".to_string(),
|
||||
source: PluginSource::Local {
|
||||
path: AbsolutePathBuf::try_from(
|
||||
repo_root.path().join("plugins/missing-plugin"),
|
||||
)?,
|
||||
},
|
||||
installed: false,
|
||||
enabled: false,
|
||||
install_policy: PluginInstallPolicy::Available,
|
||||
auth_policy: PluginAuthPolicy::OnInstall,
|
||||
interface: None,
|
||||
},
|
||||
],
|
||||
}]
|
||||
);
|
||||
assert!(response.marketplace_load_errors.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_list_accepts_omitted_cwds() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
@@ -512,11 +512,7 @@ async fn plugin_read_returns_invalid_request_when_plugin_manifest_is_missing() -
|
||||
.await??;
|
||||
|
||||
assert_eq!(err.error.code, -32600);
|
||||
assert!(
|
||||
err.error
|
||||
.message
|
||||
.contains("missing or invalid .codex-plugin/plugin.json")
|
||||
);
|
||||
assert!(err.error.message.contains("missing or invalid plugin.json"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user