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:
xl-openai
2026-04-16 19:43:19 -07:00
committed by GitHub
parent a803790a10
commit 37161bc76e
13 changed files with 581 additions and 222 deletions

View File

@@ -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()?;

View File

@@ -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(())
}