mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] [1/4] Add recommended plugin endpoint cache (#28399)
Summary - Add authenticated parsing for `/ps/plugins/suggested?scope=GLOBAL`, including remote plugin and connector app identities. - Validate, deduplicate, sort, and cap endpoint candidates before caching them by backend and account identity. - Deduplicate concurrent cache misses and warm recommendations from the existing remote-installed-plugin refresh path used at startup and after account changes. - Keep endpoint results model-invisible in this PR; failures and responses without `enabled: true` resolve to legacy mode. Stack - 1/3. Follow-up: #28400 generalizes plugin suggestion presentation without activating endpoint recommendations. - Final activation: #27704. Validation - `just test -p codex-core-plugins recommended_plugins` - `just fix -p codex-core-plugins` - `just fmt` - `git diff --check`
This commit is contained in:
1 parent
bd2a786326
commit
7e735b59ce
6 files changed
+695
-11
No files matched your search
@@ -11,6 +11,7 @@ use crate::marketplace::MarketplacePluginInstallPolicy;
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::remote::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
||||
use crate::remote::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||
use crate::remote::RecommendedPlugin;
|
||||
use crate::remote::RemoteInstalledPlugin;
|
||||
use crate::startup_sync::curated_plugins_repo_path;
|
||||
use crate::test_support::TEST_CURATED_PLUGIN_CACHE_VERSION;
|
||||
@@ -40,6 +41,7 @@ use codex_utils_absolute_path::test_support::PathBufExt;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
use toml::Value;
|
||||
use wiremock::Mock;
|
||||
@@ -3703,6 +3705,236 @@ plugins = true
|
||||
assert_eq!(featured_plugin_ids, vec!["codex-plugin".to_string()]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_plugin_caches_refresh_warms_recommended_plugins_cache() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/suggested"))
|
||||
.and(query_param("scope", "GLOBAL"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"enabled": true,
|
||||
"plugins": []
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = std::sync::Arc::new(PluginsManager::new(tmp.path().to_path_buf()));
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let cache_key = recommended_plugins_cache_key(&config);
|
||||
|
||||
manager.maybe_start_remote_plugin_caches_refresh(
|
||||
&config,
|
||||
Some(auth.clone()),
|
||||
/*on_effective_plugins_changed*/ None,
|
||||
);
|
||||
|
||||
let mode = tokio::time::timeout(Duration::from_secs(2), async {
|
||||
loop {
|
||||
if let Some(mode) = manager.cached_recommended_plugins_mode(&cache_key) {
|
||||
break mode;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("recommended plugins cache should be warmed");
|
||||
assert_eq!(
|
||||
mode,
|
||||
RecommendedPluginsMode::Endpoint {
|
||||
plugins: Vec::new()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.recommended_plugins_mode_for_config(&config, Some(&auth))
|
||||
.await,
|
||||
mode
|
||||
);
|
||||
manager.clear_recommended_plugins_cache();
|
||||
assert_eq!(manager.cached_recommended_plugins_mode(&cache_key), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recommended_plugins_mode_deduplicates_concurrent_cache_misses() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/suggested"))
|
||||
.and(query_param("scope", "GLOBAL"))
|
||||
.and(header("authorization", "Bearer Access Token"))
|
||||
.and(header("chatgpt-account-id", "account_id"))
|
||||
.and(header("OAI-Product-Sku", "codex"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_json(serde_json::json!({
|
||||
"enabled": true,
|
||||
"plugins": [
|
||||
{
|
||||
"id": "plugin_slack",
|
||||
"name": "slack",
|
||||
"release": {
|
||||
"display_name": "Slack",
|
||||
"app_ids": ["connector_slack"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "plugin_github",
|
||||
"name": "github",
|
||||
"release": {"display_name": "GitHub"}
|
||||
}
|
||||
]
|
||||
}))
|
||||
.set_delay(Duration::from_millis(100)),
|
||||
)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
let expected = RecommendedPluginsMode::Endpoint {
|
||||
plugins: vec![
|
||||
RecommendedPlugin {
|
||||
config_id: "github@openai-curated-remote".to_string(),
|
||||
remote_plugin_id: "plugin_github".to_string(),
|
||||
display_name: "GitHub".to_string(),
|
||||
app_connector_ids: Vec::new(),
|
||||
},
|
||||
RecommendedPlugin {
|
||||
config_id: "slack@openai-curated-remote".to_string(),
|
||||
remote_plugin_id: "plugin_slack".to_string(),
|
||||
display_name: "Slack".to_string(),
|
||||
app_connector_ids: vec!["connector_slack".to_string()],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let (left, right) = tokio::join!(
|
||||
manager.recommended_plugins_mode_for_config(&config, Some(&auth)),
|
||||
manager.recommended_plugins_mode_for_config(&config, Some(&auth)),
|
||||
);
|
||||
assert_eq!((left, right), (expected.clone(), expected.clone()));
|
||||
assert_eq!(
|
||||
manager
|
||||
.recommended_plugins_mode_for_config(&config, Some(&auth))
|
||||
.await,
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recommended_plugins_mode_caches_explicit_false() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/suggested"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"enabled": false,
|
||||
"plugins": []
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
assert_eq!(
|
||||
manager
|
||||
.recommended_plugins_mode_for_config(&config, Some(&auth))
|
||||
.await,
|
||||
RecommendedPluginsMode::Legacy
|
||||
);
|
||||
assert_eq!(
|
||||
manager
|
||||
.recommended_plugins_mode_for_config(&config, Some(&auth))
|
||||
.await,
|
||||
RecommendedPluginsMode::Legacy
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn recommended_plugins_mode_retries_after_fetch_failure() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
write_file(
|
||||
&tmp.path().join(CONFIG_TOML_FILE),
|
||||
r#"[features]
|
||||
plugins = true
|
||||
remote_plugin = true
|
||||
"#,
|
||||
);
|
||||
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/suggested"))
|
||||
.respond_with(ResponseTemplate::new(500).set_body_string("unavailable"))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let mut config = load_config(tmp.path(), tmp.path()).await;
|
||||
config.chatgpt_base_url = server.uri();
|
||||
let manager = PluginsManager::new(tmp.path().to_path_buf());
|
||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||
assert_eq!(
|
||||
manager
|
||||
.recommended_plugins_mode_for_config(&config, Some(&auth))
|
||||
.await,
|
||||
RecommendedPluginsMode::Legacy
|
||||
);
|
||||
|
||||
server.reset().await;
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/ps/plugins/suggested"))
|
||||
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||
"enabled": true,
|
||||
"plugins": []
|
||||
})))
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
manager
|
||||
.recommended_plugins_mode_for_config(&config, Some(&auth))
|
||||
.await,
|
||||
RecommendedPluginsMode::Endpoint {
|
||||
plugins: Vec::new()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_curated_plugin_cache_replaces_existing_local_version_with_short_sha_version() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in new issue
Block a user