mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Preserve plugin app manifest order (#25491)
## Summary - Preserve app declaration order when loading plugin .app.json files. - Keep plugin connector summaries in plugin app order after connector metadata is merged and filtered. - Add regression coverage for .app.json order and connector summary order. ## Validation - just fmt - just test -p codex-chatgpt connectors_for_plugin_apps_returns_only_requested_plugin_apps - just test -p codex-core-plugins effective_apps_preserves_app_config_order - just fix -p codex-core-plugins (passes with existing clippy large_enum_variant warning in core-plugins/src/manifest.rs) - just fix -p codex-chatgpt - just bazel-lock-update - just bazel-lock-check
This commit is contained in:
committed by
GitHub
Unverified
parent
6ddb747e76
commit
9756316d89
Generated
+1
@@ -2669,6 +2669,7 @@ dependencies = [
|
||||
"codex-utils-plugins",
|
||||
"dirs",
|
||||
"flate2",
|
||||
"indexmap 2.13.0",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"reqwest 0.12.28",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -154,20 +155,21 @@ pub fn connectors_for_plugin_apps(
|
||||
connectors: Vec<AppInfo>,
|
||||
plugin_apps: &[AppConnectorId],
|
||||
) -> Vec<AppInfo> {
|
||||
let plugin_app_ids = plugin_apps
|
||||
.iter()
|
||||
.map(|connector_id| connector_id.0.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let connectors = merge_plugin_connectors(
|
||||
connectors,
|
||||
plugin_apps
|
||||
.iter()
|
||||
.map(|connector_id| connector_id.0.clone()),
|
||||
);
|
||||
filter_disallowed_connectors(connectors, originator().value.as_str())
|
||||
.into_iter()
|
||||
.filter(|connector| plugin_app_ids.contains(connector.id.as_str()))
|
||||
let mut connectors_by_id =
|
||||
filter_disallowed_connectors(connectors, originator().value.as_str())
|
||||
.into_iter()
|
||||
.map(|connector| (connector.id.clone(), connector))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
plugin_apps
|
||||
.iter()
|
||||
.filter_map(|connector_id| connectors_by_id.remove(connector_id.0.as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -266,13 +268,14 @@ mod tests {
|
||||
let connectors = connectors_for_plugin_apps(
|
||||
vec![app("alpha"), app("beta")],
|
||||
&[
|
||||
AppConnectorId("gmail".to_string()),
|
||||
AppConnectorId("alpha".to_string()),
|
||||
AppConnectorId("gmail".to_string()),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
connectors,
|
||||
vec![app("alpha"), merged_app("gmail", /*is_accessible*/ false)]
|
||||
vec![merged_app("gmail", /*is_accessible*/ false), app("alpha")]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ codex-utils-plugins = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
indexmap = { workspace = true, features = ["serde"] }
|
||||
reqwest = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -32,6 +32,7 @@ use codex_protocol::protocol::Product;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_plugins::find_plugin_manifest_path;
|
||||
use indexmap::IndexMap;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Map as JsonMap;
|
||||
use serde_json::Value as JsonValue;
|
||||
@@ -99,7 +100,7 @@ impl PluginMcpFile {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PluginAppFile {
|
||||
#[serde(default)]
|
||||
apps: HashMap<String, PluginAppConfig>,
|
||||
apps: IndexMap<String, PluginAppConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
@@ -883,10 +884,7 @@ async fn load_apps_from_paths(
|
||||
}
|
||||
};
|
||||
|
||||
let mut apps: Vec<PluginAppConfig> = parsed.apps.into_values().collect();
|
||||
apps.sort_unstable_by(|left, right| left.id.cmp(&right.id));
|
||||
|
||||
connector_ids.extend(apps.into_iter().filter_map(|app| {
|
||||
connector_ids.extend(parsed.apps.into_values().filter_map(|app| {
|
||||
if app.id.trim().is_empty() {
|
||||
warn!(
|
||||
plugin = %plugin_root.display(),
|
||||
@@ -898,7 +896,8 @@ async fn load_apps_from_paths(
|
||||
}
|
||||
}));
|
||||
}
|
||||
connector_ids.dedup();
|
||||
let mut seen_connector_ids = HashSet::new();
|
||||
connector_ids.retain(|connector_id| seen_connector_ids.insert(connector_id.0.clone()));
|
||||
connector_ids
|
||||
}
|
||||
|
||||
|
||||
@@ -985,6 +985,50 @@ async fn effective_apps_dedupes_connector_ids_across_plugins() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn effective_apps_preserves_app_config_order() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
let plugin_root = codex_home
|
||||
.path()
|
||||
.join("plugins/cache")
|
||||
.join("test/sample/local");
|
||||
|
||||
write_file(
|
||||
&plugin_root.join(".codex-plugin/plugin.json"),
|
||||
r#"{"name":"sample"}"#,
|
||||
);
|
||||
write_file(
|
||||
&plugin_root.join(".app.json"),
|
||||
r#"{
|
||||
"apps": {
|
||||
"slack": {
|
||||
"id": "connector_slack"
|
||||
},
|
||||
"github": {
|
||||
"id": "connector_github"
|
||||
},
|
||||
"slack-copy": {
|
||||
"id": "connector_slack"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
);
|
||||
|
||||
let outcome = load_plugins_from_config(
|
||||
&plugin_config_toml(/*enabled*/ true, /*plugins_feature_enabled*/ true),
|
||||
codex_home.path(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
outcome.effective_apps(),
|
||||
vec![
|
||||
AppConnectorId("connector_slack".to_string()),
|
||||
AppConnectorId("connector_github".to_string()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_index_filters_inactive_and_zero_capability_plugins() {
|
||||
let codex_home = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user