mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
Fetch remote installed plugins across all scopes (#37210)
## What changed - Fetch the paginated installed-plugin snapshot without a `scope` query instead of issuing separate requests for global, user, and workspace plugins. - Use the combined snapshot for installed-plugin caching and bundle synchronization while continuing to reconcile each scope's marketplace independently. ## Testing - Cover pagination across mixed scopes, request query parameters, marketplace reconciliation, stale cache cleanup, and discoverable-plugin filtering. GitOrigin-RevId: 7428a9db81d7ec04e65cc4b236b652278b200956
This commit is contained in:
1 parent
0a0ebb8535
commit
1ae82ce6a5
8 files changed
+562
-529
No files matched your search
@@ -1,3 +1,6 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::sync::OnceLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -55,6 +58,9 @@ const TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS: &str =
|
|||||||
"CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS";
|
"CODEX_TEST_ALLOW_HTTP_REMOTE_PLUGIN_BUNDLE_DOWNLOADS";
|
||||||
const ALTERNATE_MARKETPLACE_RELATIVE_PATH: &str = ".claude-plugin/marketplace.json";
|
const ALTERNATE_MARKETPLACE_RELATIVE_PATH: &str = ".claude-plugin/marketplace.json";
|
||||||
const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
|
const ALTERNATE_PLUGIN_MANIFEST_RELATIVE_PATH: &str = ".claude-plugin/plugin.json";
|
||||||
|
type RemoteInstalledPluginFixtures = BTreeMap<String, BTreeMap<String, Vec<serde_json::Value>>>;
|
||||||
|
static REMOTE_INSTALLED_PLUGIN_FIXTURES: OnceLock<Mutex<RemoteInstalledPluginFixtures>> =
|
||||||
|
OnceLock::new();
|
||||||
|
|
||||||
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
|
fn write_plugins_enabled_config(codex_home: &std::path::Path) -> std::io::Result<()> {
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
@@ -3379,8 +3385,7 @@ plugin_sharing = true
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
wait_for_remote_installed_snapshot_request(&server).await?;
|
||||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3466,8 +3471,7 @@ plugin_sharing = false
|
|||||||
true
|
true
|
||||||
)]
|
)]
|
||||||
);
|
);
|
||||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
wait_for_remote_installed_snapshot_request(&server).await?;
|
||||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3553,7 +3557,7 @@ plugin_sharing = false
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
wait_for_remote_installed_scope_request(&server, "USER").await?;
|
wait_for_remote_installed_snapshot_request(&server).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4271,8 +4275,7 @@ async fn plugin_list_fetches_shared_with_me_kind() -> Result<()> {
|
|||||||
share_context.discoverability,
|
share_context.discoverability,
|
||||||
Some(PluginShareDiscoverability::Unlisted)
|
Some(PluginShareDiscoverability::Unlisted)
|
||||||
);
|
);
|
||||||
wait_for_remote_installed_scope_request(&server, "WORKSPACE").await?;
|
wait_for_remote_installed_snapshot_request(&server).await?;
|
||||||
wait_for_remote_installed_scope_request(&server, "GLOBAL").await?;
|
|
||||||
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
wait_for_remote_plugin_request_count(&server, "/ps/plugins/list", /*expected_count*/ 0).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -4751,7 +4754,7 @@ async fn wait_for_remote_plugin_list_scope_request_count(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &str) -> Result<()> {
|
async fn wait_for_remote_installed_snapshot_request(server: &MockServer) -> Result<()> {
|
||||||
timeout(DEFAULT_TIMEOUT, async {
|
timeout(DEFAULT_TIMEOUT, async {
|
||||||
loop {
|
loop {
|
||||||
let Some(requests) = server.received_requests().await else {
|
let Some(requests) = server.received_requests().await else {
|
||||||
@@ -4760,10 +4763,7 @@ async fn wait_for_remote_installed_scope_request(server: &MockServer, scope: &st
|
|||||||
if requests.iter().any(|request| {
|
if requests.iter().any(|request| {
|
||||||
request.method == "GET"
|
request.method == "GET"
|
||||||
&& request.url.path().ends_with("/ps/plugins/installed")
|
&& request.url.path().ends_with("/ps/plugins/installed")
|
||||||
&& request
|
&& request.url.query_pairs().all(|(name, _)| name != "scope")
|
||||||
.url
|
|
||||||
.query_pairs()
|
|
||||||
.any(|(name, value)| name == "scope" && value == scope)
|
|
||||||
}) {
|
}) {
|
||||||
return Ok::<(), anyhow::Error>(());
|
return Ok::<(), anyhow::Error>(());
|
||||||
}
|
}
|
||||||
@@ -4998,6 +4998,19 @@ async fn mount_shared_workspace_plugins(server: &MockServer, body: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body: &str) {
|
async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body: &str) {
|
||||||
|
let plugins = serde_json::from_str::<serde_json::Value>(body)
|
||||||
|
.expect("installed plugin fixture should be valid JSON")["plugins"]
|
||||||
|
.as_array()
|
||||||
|
.expect("installed plugin fixture should contain plugins")
|
||||||
|
.clone();
|
||||||
|
REMOTE_INSTALLED_PLUGIN_FIXTURES
|
||||||
|
.get_or_init(Default::default)
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.entry(server.uri())
|
||||||
|
.or_default()
|
||||||
|
.insert(scope.to_string(), plugins);
|
||||||
|
|
||||||
Mock::given(method("GET"))
|
Mock::given(method("GET"))
|
||||||
.and(path("/backend-api/ps/plugins/installed"))
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
.and(query_param("scope", scope))
|
.and(query_param("scope", scope))
|
||||||
@@ -5006,6 +5019,34 @@ async fn mount_remote_installed_plugins(server: &MockServer, scope: &str, body:
|
|||||||
.respond_with(ResponseTemplate::new(200).set_body_string(body))
|
.respond_with(ResponseTemplate::new(200).set_body_string(body))
|
||||||
.mount(server)
|
.mount(server)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
let server_uri = server.uri();
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(header("authorization", "Bearer chatgpt-token"))
|
||||||
|
.and(header("chatgpt-account-id", "account-123"))
|
||||||
|
.respond_with(move |_request: &wiremock::Request| {
|
||||||
|
let fixtures = REMOTE_INSTALLED_PLUGIN_FIXTURES
|
||||||
|
.get()
|
||||||
|
.expect("installed plugin fixtures should exist")
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
let scoped_plugins = fixtures
|
||||||
|
.get(&server_uri)
|
||||||
|
.expect("installed plugin fixtures should exist for this server");
|
||||||
|
let plugins = ["GLOBAL", "WORKSPACE", "USER"]
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|scope| scoped_plugins.get(scope).into_iter().flatten())
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||||
|
"plugins": plugins,
|
||||||
|
"pagination": {"limit": 50, "next_page_token": null},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.mount(server)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn mount_empty_user_installed_plugins(server: &MockServer) {
|
async fn mount_empty_user_installed_plugins(server: &MockServer) {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ use wiremock::matchers::header;
|
|||||||
use wiremock::matchers::method;
|
use wiremock::matchers::method;
|
||||||
use wiremock::matchers::path;
|
use wiremock::matchers::path;
|
||||||
use wiremock::matchers::query_param;
|
use wiremock::matchers::query_param;
|
||||||
|
use wiremock::matchers::query_param_is_missing;
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
const WATCHER_TIMEOUT: Duration = Duration::from_secs(20);
|
const WATCHER_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
@@ -579,6 +580,14 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result
|
|||||||
.mount(&server)
|
.mount(&server)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(header("authorization", "Bearer chatgpt-token"))
|
||||||
|
.and(header("chatgpt-account-id", "account-123"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_string(global_installed_body))
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
|
||||||
let plugin_list_request_id = mcp
|
let plugin_list_request_id = mcp
|
||||||
.send_plugin_list_request(PluginListParams {
|
.send_plugin_list_request(PluginListParams {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ use wiremock::ResponseTemplate;
|
|||||||
use wiremock::matchers::method;
|
use wiremock::matchers::method;
|
||||||
use wiremock::matchers::path;
|
use wiremock::matchers::path;
|
||||||
use wiremock::matchers::query_param;
|
use wiremock::matchers::query_param;
|
||||||
|
use wiremock::matchers::query_param_is_missing;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn returns_fallback_plugins_when_remote_disabled_for_codex_auth() {
|
async fn returns_fallback_plugins_when_remote_disabled_for_codex_auth() {
|
||||||
@@ -786,7 +787,7 @@ source = "/tmp/{sales_marketplace_name}"
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn expands_cached_remote_plugins_by_loaded_apps() {
|
async fn cached_remote_discovery_requires_installed_cache_and_filters_candidates() {
|
||||||
let codex_home = tempdir().expect("tempdir should succeed");
|
let codex_home = tempdir().expect("tempdir should succeed");
|
||||||
write_file(
|
write_file(
|
||||||
&codex_home.path().join(CONFIG_TOML_FILE),
|
&codex_home.path().join(CONFIG_TOML_FILE),
|
||||||
@@ -801,6 +802,25 @@ plugins = true
|
|||||||
.and(query_param("scope", "GLOBAL"))
|
.and(query_param("scope", "GLOBAL"))
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_github",
|
||||||
|
"name": "github",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "AVAILABLE",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote GitHub",
|
||||||
|
"description": "Remote GitHub long",
|
||||||
|
"app_ids": ["github"],
|
||||||
|
"interface": {"short_description": "Remote GitHub short"},
|
||||||
|
"skills": [{
|
||||||
|
"name": "github",
|
||||||
|
"description": "Use GitHub",
|
||||||
|
"interface": null
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "plugins~Plugin_remote_unlisted",
|
"id": "plugins~Plugin_remote_unlisted",
|
||||||
"name": "remote-unlisted",
|
"name": "remote-unlisted",
|
||||||
@@ -835,6 +855,32 @@ plugins = true
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_slack_not_available",
|
||||||
|
"name": "slack",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "NOT_AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "AVAILABLE",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote Slack",
|
||||||
|
"description": "Remote Slack long",
|
||||||
|
"interface": {"short_description": "Remote Slack short"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "plugins~Plugin_remote_figma_admin_disabled",
|
||||||
|
"name": "figma",
|
||||||
|
"scope": "GLOBAL",
|
||||||
|
"installation_policy": "AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "DISABLED_BY_ADMIN",
|
||||||
|
"release": {
|
||||||
|
"display_name": "Remote Figma",
|
||||||
|
"description": "Remote Figma long",
|
||||||
|
"interface": {"short_description": "Remote Figma short"}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"pagination": {
|
"pagination": {
|
||||||
@@ -860,20 +906,29 @@ plugins = true
|
|||||||
.await
|
.await
|
||||||
.expect("remote plugin catalog cache should write");
|
.expect("remote plugin catalog cache should write");
|
||||||
|
|
||||||
for scope in ["GLOBAL", "USER", "WORKSPACE"] {
|
assert_eq!(
|
||||||
Mock::given(method("GET"))
|
list_discoverable_plugins(
|
||||||
.and(path("/backend-api/ps/plugins/installed"))
|
&plugins_manager,
|
||||||
.and(query_param("scope", scope))
|
discovery_input(plugins.clone(), &[], &[], &["remote-unlisted-app"]),
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
Some(&auth),
|
||||||
"plugins": [],
|
)
|
||||||
"pagination": {
|
.await,
|
||||||
"next_page_token": null
|
Vec::new()
|
||||||
}
|
);
|
||||||
})))
|
|
||||||
.expect(1)
|
Mock::given(method("GET"))
|
||||||
.mount(&server)
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
.await;
|
.and(query_param_is_missing("scope"))
|
||||||
}
|
.and(query_param_is_missing("includeDownloadUrls"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"plugins": [],
|
||||||
|
"pagination": {
|
||||||
|
"next_page_token": null
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
plugins_manager
|
plugins_manager
|
||||||
.build_and_cache_remote_installed_plugin_marketplaces(
|
.build_and_cache_remote_installed_plugin_marketplaces(
|
||||||
&plugins,
|
&plugins,
|
||||||
@@ -884,24 +939,55 @@ plugins = true
|
|||||||
.await
|
.await
|
||||||
.expect("remote installed plugin cache should write");
|
.expect("remote installed plugin cache should write");
|
||||||
|
|
||||||
|
let expected_github = ToolSuggestDiscoverablePlugin {
|
||||||
|
id: "github@openai-curated-remote".to_string(),
|
||||||
|
remote_plugin_id: Some("plugins~Plugin_remote_github".to_string()),
|
||||||
|
name: "Remote GitHub".to_string(),
|
||||||
|
description: Some("Remote GitHub short".to_string()),
|
||||||
|
has_skills: true,
|
||||||
|
mcp_server_names: Vec::new(),
|
||||||
|
app_connector_ids: vec!["github".to_string()],
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
list_discoverable_plugins(
|
||||||
|
&plugins_manager,
|
||||||
|
discovery_input(plugins.clone(), &[], &[], &[]),
|
||||||
|
Some(&auth),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
vec![expected_github.clone()]
|
||||||
|
);
|
||||||
|
|
||||||
let discoverable_plugins = list_discoverable_plugins(
|
let discoverable_plugins = list_discoverable_plugins(
|
||||||
&plugins_manager,
|
&plugins_manager,
|
||||||
discovery_input(plugins, &[], &[], &["remote-unlisted-app"]),
|
discovery_input(plugins.clone(), &[], &[], &["remote-unlisted-app"]),
|
||||||
Some(&auth),
|
Some(&auth),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
discoverable_plugins,
|
discoverable_plugins,
|
||||||
vec![ToolSuggestDiscoverablePlugin {
|
vec![
|
||||||
id: "remote-unlisted@openai-curated-remote".to_string(),
|
expected_github,
|
||||||
remote_plugin_id: Some("plugins~Plugin_remote_unlisted".to_string()),
|
ToolSuggestDiscoverablePlugin {
|
||||||
name: "Remote Unlisted".to_string(),
|
id: "remote-unlisted@openai-curated-remote".to_string(),
|
||||||
description: Some("Remote Unlisted short".to_string()),
|
remote_plugin_id: Some("plugins~Plugin_remote_unlisted".to_string()),
|
||||||
has_skills: true,
|
name: "Remote Unlisted".to_string(),
|
||||||
mcp_server_names: Vec::new(),
|
description: Some("Remote Unlisted short".to_string()),
|
||||||
app_connector_ids: vec!["remote-unlisted-app".to_string()],
|
has_skills: true,
|
||||||
}]
|
mcp_server_names: Vec::new(),
|
||||||
|
app_connector_ids: vec!["remote-unlisted-app".to_string()],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
list_discoverable_plugins(
|
||||||
|
&plugins_manager,
|
||||||
|
discovery_input(plugins, &[], &["github@openai-curated-remote"], &[]),
|
||||||
|
Some(&auth),
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
Vec::new()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -468,6 +468,12 @@ pub enum RemotePluginScope {
|
|||||||
Workspace,
|
Workspace,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum RemoteInstalledPluginScope {
|
||||||
|
All,
|
||||||
|
Single(RemotePluginScope),
|
||||||
|
}
|
||||||
|
|
||||||
impl RemotePluginScope {
|
impl RemotePluginScope {
|
||||||
const CATALOG_CACHE_SCOPES: [Self; 3] = [Self::Global, Self::User, Self::Workspace];
|
const CATALOG_CACHE_SCOPES: [Self; 3] = [Self::Global, Self::User, Self::Workspace];
|
||||||
|
|
||||||
@@ -1162,28 +1168,16 @@ pub(crate) async fn fetch_remote_installed_plugins(
|
|||||||
auth: Option<&CodexAuth>,
|
auth: Option<&CodexAuth>,
|
||||||
) -> Result<Vec<RemoteInstalledPlugin>, RemotePluginCatalogError> {
|
) -> Result<Vec<RemoteInstalledPlugin>, RemotePluginCatalogError> {
|
||||||
let auth = ensure_chatgpt_auth(auth)?;
|
let auth = ensure_chatgpt_auth(auth)?;
|
||||||
let global = async {
|
let mut installed_plugins = fetch_installed_plugins(
|
||||||
let scope = RemotePluginScope::Global;
|
config,
|
||||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
auth,
|
||||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
RemoteInstalledPluginScope::All,
|
||||||
};
|
/*include_download_urls*/ false,
|
||||||
let workspace = async {
|
)
|
||||||
let scope = RemotePluginScope::Workspace;
|
.await?
|
||||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
.into_iter()
|
||||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
.map(|plugin| remote_installed_plugin_to_cache_entry(&plugin))
|
||||||
};
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let user = async {
|
|
||||||
let scope = RemotePluginScope::User;
|
|
||||||
let installed_plugins = fetch_installed_plugins_for_scope(config, auth, scope).await?;
|
|
||||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
|
||||||
};
|
|
||||||
|
|
||||||
let (global, workspace, user) = tokio::try_join!(global, workspace, user)?;
|
|
||||||
let mut installed_plugins = [global, workspace, user]
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|(_scope, plugins)| plugins)
|
|
||||||
.map(|plugin| remote_installed_plugin_to_cache_entry(&plugin))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
installed_plugins.sort_by(|left, right| {
|
installed_plugins.sort_by(|left, right| {
|
||||||
left.marketplace_name
|
left.marketplace_name
|
||||||
.cmp(&right.marketplace_name)
|
.cmp(&right.marketplace_name)
|
||||||
@@ -1989,16 +1983,19 @@ async fn fetch_installed_plugins_for_scope(
|
|||||||
auth: &CodexAuth,
|
auth: &CodexAuth,
|
||||||
scope: RemotePluginScope,
|
scope: RemotePluginScope,
|
||||||
) -> Result<Vec<RemotePluginInstalledItem>, RemotePluginCatalogError> {
|
) -> Result<Vec<RemotePluginInstalledItem>, RemotePluginCatalogError> {
|
||||||
fetch_installed_plugins_for_scope_with_download_url(
|
fetch_installed_plugins(
|
||||||
config, auth, scope, /*include_download_urls*/ false,
|
config,
|
||||||
|
auth,
|
||||||
|
RemoteInstalledPluginScope::Single(scope),
|
||||||
|
/*include_download_urls*/ false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fetch_installed_plugins_for_scope_with_download_url(
|
async fn fetch_installed_plugins(
|
||||||
config: &RemotePluginServiceConfig,
|
config: &RemotePluginServiceConfig,
|
||||||
auth: &CodexAuth,
|
auth: &CodexAuth,
|
||||||
scope: RemotePluginScope,
|
scope: RemoteInstalledPluginScope,
|
||||||
include_download_urls: bool,
|
include_download_urls: bool,
|
||||||
) -> Result<Vec<RemotePluginInstalledItem>, RemotePluginCatalogError> {
|
) -> Result<Vec<RemotePluginInstalledItem>, RemotePluginCatalogError> {
|
||||||
let mut plugins = Vec::new();
|
let mut plugins = Vec::new();
|
||||||
@@ -2066,15 +2063,23 @@ async fn get_remote_shared_workspace_plugins_page(
|
|||||||
async fn get_remote_plugin_installed_page(
|
async fn get_remote_plugin_installed_page(
|
||||||
config: &RemotePluginServiceConfig,
|
config: &RemotePluginServiceConfig,
|
||||||
auth: &CodexAuth,
|
auth: &CodexAuth,
|
||||||
scope: RemotePluginScope,
|
scope: RemoteInstalledPluginScope,
|
||||||
page_token: Option<&str>,
|
page_token: Option<&str>,
|
||||||
include_download_urls: bool,
|
include_download_urls: bool,
|
||||||
) -> Result<RemotePluginInstalledResponse, RemotePluginCatalogError> {
|
) -> Result<RemotePluginInstalledResponse, RemotePluginCatalogError> {
|
||||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||||
let mut url = Url::parse(&format!("{base_url}/ps/plugins/installed"))
|
let mut url = Url::parse(&format!("{base_url}/ps/plugins/installed"))
|
||||||
.map_err(RemotePluginCatalogError::InvalidBaseUrl)?;
|
.map_err(RemotePluginCatalogError::InvalidBaseUrl)?;
|
||||||
url.query_pairs_mut()
|
match scope {
|
||||||
.append_pair("scope", scope.api_value());
|
RemoteInstalledPluginScope::All => {
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("limit", &REMOTE_PLUGIN_LIST_PAGE_LIMIT.to_string());
|
||||||
|
}
|
||||||
|
RemoteInstalledPluginScope::Single(scope) => {
|
||||||
|
url.query_pairs_mut()
|
||||||
|
.append_pair("scope", scope.api_value());
|
||||||
|
}
|
||||||
|
}
|
||||||
if include_download_urls {
|
if include_download_urls {
|
||||||
url.query_pairs_mut()
|
url.query_pairs_mut()
|
||||||
.append_pair("includeDownloadUrls", "true");
|
.append_pair("includeDownloadUrls", "true");
|
||||||
|
|||||||
@@ -4,12 +4,13 @@ use super::REMOTE_WORKSPACE_MARKETPLACE_NAME;
|
|||||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME;
|
||||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
|
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME;
|
||||||
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
|
use super::REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME;
|
||||||
|
use super::RemoteInstalledPluginScope;
|
||||||
use super::RemotePluginCatalogError;
|
use super::RemotePluginCatalogError;
|
||||||
use super::RemotePluginScope;
|
use super::RemotePluginScope;
|
||||||
use super::RemotePluginServiceConfig;
|
use super::RemotePluginServiceConfig;
|
||||||
use super::RemotePluginShareDiscoverability;
|
use super::RemotePluginShareDiscoverability;
|
||||||
use super::ensure_chatgpt_auth;
|
use super::ensure_chatgpt_auth;
|
||||||
use super::fetch_installed_plugins_for_scope_with_download_url;
|
use super::fetch_installed_plugins;
|
||||||
use super::remote_plugin_canonical_marketplace_name;
|
use super::remote_plugin_canonical_marketplace_name;
|
||||||
use crate::store::PLUGINS_CACHE_DIR;
|
use crate::store::PLUGINS_CACHE_DIR;
|
||||||
use crate::store::PluginStore;
|
use crate::store::PluginStore;
|
||||||
@@ -136,32 +137,13 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
|||||||
) -> Result<RemoteInstalledPluginBundleSyncOutcome, RemoteInstalledPluginBundleSyncError> {
|
) -> Result<RemoteInstalledPluginBundleSyncOutcome, RemoteInstalledPluginBundleSyncError> {
|
||||||
let auth = ensure_chatgpt_auth(auth)?;
|
let auth = ensure_chatgpt_auth(auth)?;
|
||||||
let authenticated_account_id = auth.get_account_id();
|
let authenticated_account_id = auth.get_account_id();
|
||||||
let global = async {
|
let installed_plugins = fetch_installed_plugins(
|
||||||
let scope = RemotePluginScope::Global;
|
config,
|
||||||
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
|
auth,
|
||||||
config, auth, scope, /*include_download_urls*/ true,
|
RemoteInstalledPluginScope::All,
|
||||||
)
|
/*include_download_urls*/ true,
|
||||||
.await?;
|
)
|
||||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
.await?;
|
||||||
};
|
|
||||||
let workspace = async {
|
|
||||||
let scope = RemotePluginScope::Workspace;
|
|
||||||
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
|
|
||||||
config, auth, scope, /*include_download_urls*/ true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
|
||||||
};
|
|
||||||
let user = async {
|
|
||||||
let scope = RemotePluginScope::User;
|
|
||||||
let installed_plugins = fetch_installed_plugins_for_scope_with_download_url(
|
|
||||||
config, auth, scope, /*include_download_urls*/ true,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok::<_, RemotePluginCatalogError>((scope, installed_plugins))
|
|
||||||
};
|
|
||||||
|
|
||||||
let (global, workspace, user) = tokio::try_join!(global, workspace, user)?;
|
|
||||||
let store = PluginStore::try_new(codex_home.clone())?;
|
let store = PluginStore::try_new(codex_home.clone())?;
|
||||||
let mut installed_plugin_names_by_marketplace =
|
let mut installed_plugin_names_by_marketplace =
|
||||||
BTreeMap::<String, BTreeSet<String>>::from_iter([
|
BTreeMap::<String, BTreeSet<String>>::from_iter([
|
||||||
@@ -190,101 +172,99 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
|||||||
let mut materialized_remote_plugins = BTreeMap::new();
|
let mut materialized_remote_plugins = BTreeMap::new();
|
||||||
let mut failed_remote_plugin_ids = BTreeSet::new();
|
let mut failed_remote_plugin_ids = BTreeSet::new();
|
||||||
|
|
||||||
for (_scope, installed_plugins) in [global, workspace, user] {
|
for installed_plugin in installed_plugins {
|
||||||
for installed_plugin in installed_plugins {
|
let plugin = installed_plugin.plugin;
|
||||||
let plugin = installed_plugin.plugin;
|
let scope = plugin.scope;
|
||||||
let scope = plugin.scope;
|
let discoverability = plugin.discoverability;
|
||||||
let discoverability = plugin.discoverability;
|
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
||||||
let marketplace_name = remote_plugin_canonical_marketplace_name(&plugin)?.to_string();
|
installed_plugin_names_by_marketplace
|
||||||
installed_plugin_names_by_marketplace
|
.entry(marketplace_name.clone())
|
||||||
.entry(marketplace_name.clone())
|
.or_default()
|
||||||
.or_default()
|
.insert(plugin.name.clone());
|
||||||
.insert(plugin.name.clone());
|
let plugin_id = match PluginId::new(plugin.name.clone(), marketplace_name.clone()) {
|
||||||
let plugin_id = match PluginId::new(plugin.name.clone(), marketplace_name.clone()) {
|
Ok(plugin_id) => plugin_id,
|
||||||
Ok(plugin_id) => plugin_id,
|
Err(err) => {
|
||||||
Err(err) => {
|
warn!(
|
||||||
warn!(
|
remote_plugin_id = %plugin.id,
|
||||||
remote_plugin_id = %plugin.id,
|
plugin = %plugin.name,
|
||||||
plugin = %plugin.name,
|
marketplace = %marketplace_name,
|
||||||
marketplace = %marketplace_name,
|
error = %err,
|
||||||
error = %err,
|
"skipping remote installed plugin with invalid local cache id"
|
||||||
"skipping remote installed plugin with invalid local cache id"
|
);
|
||||||
);
|
failed_remote_plugin_ids.insert(plugin.id);
|
||||||
failed_remote_plugin_ids.insert(plugin.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let release_version = plugin
|
|
||||||
.release
|
|
||||||
.version
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|version| !version.is_empty());
|
|
||||||
if store.active_plugin_version(&plugin_id).as_deref() == release_version {
|
|
||||||
if let Err(err) = store.write_remote_plugin_id(&plugin_id, &plugin.id) {
|
|
||||||
warn!(
|
|
||||||
remote_plugin_id = %plugin.id,
|
|
||||||
plugin = %plugin.name,
|
|
||||||
marketplace = %marketplace_name,
|
|
||||||
error = %err,
|
|
||||||
"failed to persist identity for cached remote installed plugin"
|
|
||||||
);
|
|
||||||
failed_remote_plugin_ids.insert(plugin.id);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
let release_version = plugin
|
||||||
|
.release
|
||||||
|
.version
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|version| !version.is_empty());
|
||||||
|
if store.active_plugin_version(&plugin_id).as_deref() == release_version {
|
||||||
|
if let Err(err) = store.write_remote_plugin_id(&plugin_id, &plugin.id) {
|
||||||
|
warn!(
|
||||||
|
remote_plugin_id = %plugin.id,
|
||||||
|
plugin = %plugin.name,
|
||||||
|
marketplace = %marketplace_name,
|
||||||
|
error = %err,
|
||||||
|
"failed to persist identity for cached remote installed plugin"
|
||||||
|
);
|
||||||
|
failed_remote_plugin_ids.insert(plugin.id);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let bundle = match crate::remote_bundle::validate_remote_plugin_bundle(
|
let bundle = match crate::remote_bundle::validate_remote_plugin_bundle(
|
||||||
&plugin.id,
|
&plugin.id,
|
||||||
&marketplace_name,
|
&marketplace_name,
|
||||||
&plugin.name,
|
&plugin.name,
|
||||||
release_version,
|
release_version,
|
||||||
plugin.release.bundle_download_url.as_deref(),
|
plugin.release.bundle_download_url.as_deref(),
|
||||||
plugin.release.app_manifest.clone(),
|
plugin.release.app_manifest.clone(),
|
||||||
) {
|
) {
|
||||||
Ok(bundle) => bundle,
|
Ok(bundle) => bundle,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
remote_plugin_id = %plugin.id,
|
remote_plugin_id = %plugin.id,
|
||||||
plugin = %plugin.name,
|
plugin = %plugin.name,
|
||||||
marketplace = %marketplace_name,
|
marketplace = %marketplace_name,
|
||||||
error = %err,
|
error = %err,
|
||||||
"skipping remote installed plugin bundle download"
|
"skipping remote installed plugin bundle download"
|
||||||
);
|
);
|
||||||
failed_remote_plugin_ids.insert(plugin.id);
|
failed_remote_plugin_ids.insert(plugin.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match crate::remote_bundle::download_and_install_remote_plugin_bundle(
|
match crate::remote_bundle::download_and_install_remote_plugin_bundle(
|
||||||
config,
|
config,
|
||||||
codex_home.clone(),
|
codex_home.clone(),
|
||||||
bundle,
|
bundle,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
let plugin_id = result.plugin_id;
|
let plugin_id = result.plugin_id;
|
||||||
materialized_remote_plugins.insert(
|
materialized_remote_plugins.insert(
|
||||||
plugin_id.as_key(),
|
plugin_id.as_key(),
|
||||||
RemotePluginMaterialization {
|
RemotePluginMaterialization {
|
||||||
plugin_id,
|
plugin_id,
|
||||||
scope,
|
scope,
|
||||||
discoverability,
|
discoverability,
|
||||||
authenticated_account_id: authenticated_account_id.clone(),
|
authenticated_account_id: authenticated_account_id.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!(
|
warn!(
|
||||||
remote_plugin_id = %plugin.id,
|
remote_plugin_id = %plugin.id,
|
||||||
plugin = %plugin.name,
|
plugin = %plugin.name,
|
||||||
marketplace = %marketplace_name,
|
marketplace = %marketplace_name,
|
||||||
error = %err,
|
error = %err,
|
||||||
"failed to download remote installed plugin bundle"
|
"failed to download remote installed plugin bundle"
|
||||||
);
|
);
|
||||||
failed_remote_plugin_ids.insert(plugin.id);
|
failed_remote_plugin_ids.insert(plugin.id);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,6 +465,7 @@ mod tests {
|
|||||||
use wiremock::matchers::method;
|
use wiremock::matchers::method;
|
||||||
use wiremock::matchers::path;
|
use wiremock::matchers::path;
|
||||||
use wiremock::matchers::query_param;
|
use wiremock::matchers::query_param;
|
||||||
|
use wiremock::matchers::query_param_is_missing;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn remote_installed_plugin_sync_in_flight_dedupes_by_cache_root() {
|
fn remote_installed_plugin_sync_in_flight_dedupes_by_cache_root() {
|
||||||
@@ -526,7 +507,8 @@ mod tests {
|
|||||||
let remote_plugin_id = "plugins~Plugin_linear";
|
let remote_plugin_id = "plugins~Plugin_linear";
|
||||||
Mock::given(method("GET"))
|
Mock::given(method("GET"))
|
||||||
.and(path("/backend-api/ps/plugins/installed"))
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
.and(query_param("scope", "GLOBAL"))
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(query_param("limit", "200"))
|
||||||
.and(query_param("includeDownloadUrls", "true"))
|
.and(query_param("includeDownloadUrls", "true"))
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
"plugins": [{
|
"plugins": [{
|
||||||
@@ -549,28 +531,6 @@ mod tests {
|
|||||||
.expect(1)
|
.expect(1)
|
||||||
.mount(&server)
|
.mount(&server)
|
||||||
.await;
|
.await;
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/backend-api/ps/plugins/installed"))
|
|
||||||
.and(query_param("scope", "USER"))
|
|
||||||
.and(query_param("includeDownloadUrls", "true"))
|
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
||||||
"plugins": [],
|
|
||||||
"pagination": {"next_page_token": null},
|
|
||||||
})))
|
|
||||||
.expect(1)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/backend-api/ps/plugins/installed"))
|
|
||||||
.and(query_param("scope", "WORKSPACE"))
|
|
||||||
.and(query_param("includeDownloadUrls", "true"))
|
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
||||||
"plugins": [],
|
|
||||||
"pagination": {"next_page_token": null},
|
|
||||||
})))
|
|
||||||
.expect(1)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
let config = RemotePluginServiceConfig::new(
|
let config = RemotePluginServiceConfig::new(
|
||||||
format!("{}/backend-api", server.uri()),
|
format!("{}/backend-api", server.uri()),
|
||||||
crate::test_support::test_http_client_factory(),
|
crate::test_support::test_http_client_factory(),
|
||||||
@@ -607,6 +567,177 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sync_all_scopes_paginates_and_reconciles_each_marketplace() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||||
|
let cached_plugins = [
|
||||||
|
(
|
||||||
|
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||||
|
"global-plugin",
|
||||||
|
"GLOBAL",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
REMOTE_CREATED_BY_ME_MARKETPLACE_NAME,
|
||||||
|
"user-plugin",
|
||||||
|
"USER",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
REMOTE_WORKSPACE_MARKETPLACE_NAME,
|
||||||
|
"workspace-plugin",
|
||||||
|
"WORKSPACE",
|
||||||
|
Some("LISTED"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME,
|
||||||
|
"shared-plugin",
|
||||||
|
"WORKSPACE",
|
||||||
|
Some("PRIVATE"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (marketplace_name, plugin_name, _, _) in cached_plugins {
|
||||||
|
for cached_plugin_name in [plugin_name, "stale"] {
|
||||||
|
let manifest = codex_home
|
||||||
|
.path()
|
||||||
|
.join(PLUGINS_CACHE_DIR)
|
||||||
|
.join(marketplace_name)
|
||||||
|
.join(cached_plugin_name)
|
||||||
|
.join("1.2.3")
|
||||||
|
.join(".codex-plugin")
|
||||||
|
.join("plugin.json");
|
||||||
|
std::fs::create_dir_all(manifest.parent().expect("manifest parent"))
|
||||||
|
.expect("create cached plugin manifest parent");
|
||||||
|
std::fs::write(
|
||||||
|
&manifest,
|
||||||
|
format!(r#"{{"name":"{cached_plugin_name}","version":"1.2.3"}}"#),
|
||||||
|
)
|
||||||
|
.expect("write cached plugin manifest");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let installed_plugins = cached_plugins
|
||||||
|
.iter()
|
||||||
|
.map(|(_, plugin_name, scope, discoverability)| {
|
||||||
|
let mut plugin = json!({
|
||||||
|
"id": format!("plugins~Plugin_{plugin_name}"),
|
||||||
|
"name": plugin_name,
|
||||||
|
"scope": scope,
|
||||||
|
"installation_policy": "AVAILABLE",
|
||||||
|
"authentication_policy": "ON_USE",
|
||||||
|
"status": "ENABLED",
|
||||||
|
"release": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"display_name": plugin_name,
|
||||||
|
"description": "Installed plugin",
|
||||||
|
"interface": {},
|
||||||
|
},
|
||||||
|
"enabled": true,
|
||||||
|
});
|
||||||
|
if let Some(discoverability) = discoverability {
|
||||||
|
plugin["discoverability"] = json!(discoverability);
|
||||||
|
}
|
||||||
|
plugin
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(query_param("limit", "200"))
|
||||||
|
.and(query_param("includeDownloadUrls", "true"))
|
||||||
|
.and(query_param_is_missing("pageToken"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"plugins": &installed_plugins[..2],
|
||||||
|
"pagination": {"next_page_token": "page-2"},
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(query_param("limit", "200"))
|
||||||
|
.and(query_param("includeDownloadUrls", "true"))
|
||||||
|
.and(query_param("pageToken", "page-2"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
||||||
|
"plugins": &installed_plugins[2..],
|
||||||
|
"pagination": {"next_page_token": null},
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
let (config, selected_urls) = crate::test_support::recording_remote_plugin_service_config(
|
||||||
|
format!("{}/backend-api", server.uri()),
|
||||||
|
);
|
||||||
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||||
|
|
||||||
|
let outcome = sync_remote_installed_plugin_bundles_once(
|
||||||
|
codex_home.path().to_path_buf(),
|
||||||
|
&config,
|
||||||
|
Some(&auth),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("sync installed plugins across every marketplace");
|
||||||
|
let mut removed_cache_plugin_ids = cached_plugins
|
||||||
|
.iter()
|
||||||
|
.map(|(marketplace_name, _, _, _)| format!("stale@{marketplace_name}"))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
removed_cache_plugin_ids.sort();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
outcome,
|
||||||
|
RemoteInstalledPluginBundleSyncOutcome {
|
||||||
|
materialized_remote_plugins: Vec::new(),
|
||||||
|
removed_cache_plugin_ids,
|
||||||
|
failed_remote_plugin_ids: Vec::new(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
crate::test_support::recorded_http_client_urls(&selected_urls),
|
||||||
|
vec![
|
||||||
|
format!(
|
||||||
|
"{}/backend-api/ps/plugins/installed?limit=200&includeDownloadUrls=true",
|
||||||
|
server.uri()
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"{}/backend-api/ps/plugins/installed?limit=200&includeDownloadUrls=true&pageToken=page-2",
|
||||||
|
server.uri()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
for (marketplace_name, plugin_name, _, _) in cached_plugins {
|
||||||
|
let plugin_root = codex_home
|
||||||
|
.path()
|
||||||
|
.join(PLUGINS_CACHE_DIR)
|
||||||
|
.join(marketplace_name)
|
||||||
|
.join(plugin_name);
|
||||||
|
assert!(
|
||||||
|
plugin_root
|
||||||
|
.join("1.2.3/.codex-plugin/plugin.json")
|
||||||
|
.is_file()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<serde_json::Value>(
|
||||||
|
&std::fs::read_to_string(plugin_root.join(".codex-remote-plugin-install.json"))
|
||||||
|
.expect("read remote plugin install metadata")
|
||||||
|
)
|
||||||
|
.expect("parse remote plugin install metadata"),
|
||||||
|
json!({
|
||||||
|
"schema_version": 1,
|
||||||
|
"remote_plugin_id": format!("plugins~Plugin_{plugin_name}"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!codex_home
|
||||||
|
.path()
|
||||||
|
.join(PLUGINS_CACHE_DIR)
|
||||||
|
.join(marketplace_name)
|
||||||
|
.join("stale")
|
||||||
|
.exists()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stale_remote_plugin_cleanup_skips_cache_mutations_in_progress() {
|
fn stale_remote_plugin_cleanup_skips_cache_mutations_in_progress() {
|
||||||
let codex_home = tempfile::tempdir().expect("create codex home");
|
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use wiremock::ResponseTemplate;
|
|||||||
use wiremock::matchers::header_exists;
|
use wiremock::matchers::header_exists;
|
||||||
use wiremock::matchers::method;
|
use wiremock::matchers::method;
|
||||||
use wiremock::matchers::path;
|
use wiremock::matchers::path;
|
||||||
|
use wiremock::matchers::query_param;
|
||||||
|
use wiremock::matchers::query_param_is_missing;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn remote_plugin_list_routes_the_complete_query_url() {
|
async fn remote_plugin_list_routes_the_complete_query_url() {
|
||||||
@@ -46,6 +48,84 @@ async fn remote_plugin_list_routes_the_complete_query_url() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn remote_installed_plugins_paginate_across_all_scopes_without_download_urls() {
|
||||||
|
let server = MockServer::start().await;
|
||||||
|
let installed_plugin = |scope: RemotePluginScope, id: &str, name: &str| {
|
||||||
|
let mut plugin = directory_plugin(id, name);
|
||||||
|
plugin.scope = scope;
|
||||||
|
if scope == RemotePluginScope::Workspace {
|
||||||
|
plugin.discoverability = Some(RemotePluginShareDiscoverability::Listed);
|
||||||
|
}
|
||||||
|
let mut plugin = serde_json::to_value(plugin).expect("serialize installed plugin");
|
||||||
|
plugin["enabled"] = serde_json::json!(true);
|
||||||
|
plugin
|
||||||
|
};
|
||||||
|
let global = installed_plugin(RemotePluginScope::Global, "plugin-global", "global-plugin");
|
||||||
|
let user = installed_plugin(RemotePluginScope::User, "plugin-user", "user-plugin");
|
||||||
|
let workspace = installed_plugin(
|
||||||
|
RemotePluginScope::Workspace,
|
||||||
|
"plugin-workspace",
|
||||||
|
"workspace-plugin",
|
||||||
|
);
|
||||||
|
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(query_param("limit", "200"))
|
||||||
|
.and(query_param_is_missing("includeDownloadUrls"))
|
||||||
|
.and(query_param_is_missing("pageToken"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||||
|
"plugins": [user.clone(), workspace.clone()],
|
||||||
|
"pagination": {"next_page_token": "next page/+"},
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
Mock::given(method("GET"))
|
||||||
|
.and(path("/backend-api/ps/plugins/installed"))
|
||||||
|
.and(query_param_is_missing("scope"))
|
||||||
|
.and(query_param("limit", "200"))
|
||||||
|
.and(query_param_is_missing("includeDownloadUrls"))
|
||||||
|
.and(query_param("pageToken", "next page/+"))
|
||||||
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
||||||
|
"plugins": [global.clone()],
|
||||||
|
"pagination": {"next_page_token": null},
|
||||||
|
})))
|
||||||
|
.expect(1)
|
||||||
|
.mount(&server)
|
||||||
|
.await;
|
||||||
|
let (config, selected_urls) =
|
||||||
|
recording_remote_plugin_service_config(format!("{}/backend-api", server.uri()));
|
||||||
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
||||||
|
|
||||||
|
let installed_plugins = fetch_remote_installed_plugins(&config, Some(&auth))
|
||||||
|
.await
|
||||||
|
.expect("all-scopes installed plugin request should succeed");
|
||||||
|
let expected_plugins = [user, global, workspace]
|
||||||
|
.into_iter()
|
||||||
|
.map(|plugin| {
|
||||||
|
let plugin = serde_json::from_value(plugin).expect("deserialize installed plugin");
|
||||||
|
remote_installed_plugin_to_cache_entry(&plugin).expect("valid installed plugin")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(installed_plugins, expected_plugins);
|
||||||
|
assert_eq!(
|
||||||
|
recorded_http_client_urls(&selected_urls),
|
||||||
|
vec![
|
||||||
|
format!(
|
||||||
|
"{}/backend-api/ps/plugins/installed?limit=200",
|
||||||
|
server.uri()
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"{}/backend-api/ps/plugins/installed?limit=200&pageToken=next+page%2F%2B",
|
||||||
|
server.uri()
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cached_remote_plugin_catalog_scopes_returns_existing_scopes() {
|
fn cached_remote_plugin_catalog_scopes_returns_existing_scopes() {
|
||||||
let codex_home = tempfile::tempdir().expect("create codex home");
|
let codex_home = tempfile::tempdir().expect("create codex home");
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ use crate::plugins::test_support::load_plugins_config;
|
|||||||
use crate::plugins::test_support::write_file;
|
use crate::plugins::test_support::write_file;
|
||||||
use crate::plugins::test_support::write_openai_curated_marketplace;
|
use crate::plugins::test_support::write_openai_curated_marketplace;
|
||||||
use codex_core_plugins::PluginsManager;
|
use codex_core_plugins::PluginsManager;
|
||||||
use codex_core_plugins::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
|
||||||
use codex_core_plugins::remote::RemotePluginServiceConfig;
|
|
||||||
use codex_core_plugins::remote::fetch_and_cache_global_remote_plugin_catalog;
|
|
||||||
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
|
use codex_core_plugins::startup_sync::curated_plugins_repo_path;
|
||||||
use codex_protocol::protocol::Product;
|
|
||||||
use codex_tools::DiscoverablePluginInfo;
|
use codex_tools::DiscoverablePluginInfo;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
@@ -15,321 +11,16 @@ async fn list_discoverable_plugins(
|
|||||||
config: &crate::config::Config,
|
config: &crate::config::Config,
|
||||||
loaded_plugin_app_connector_ids: &[String],
|
loaded_plugin_app_connector_ids: &[String],
|
||||||
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
||||||
list_discoverable_plugins_with_auth(config, /*auth*/ None, loaded_plugin_app_connector_ids)
|
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_discoverable_plugins_with_auth(
|
|
||||||
config: &crate::config::Config,
|
|
||||||
auth: Option<&codex_login::CodexAuth>,
|
|
||||||
loaded_plugin_app_connector_ids: &[String],
|
|
||||||
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
|
||||||
let plugins_manager = PluginsManager::new_with_options(
|
|
||||||
config.codex_home.to_path_buf(),
|
|
||||||
Some(Product::Codex),
|
|
||||||
auth.map(codex_login::CodexAuth::api_auth_mode),
|
|
||||||
);
|
|
||||||
list_discoverable_plugins_with_manager_and_auth(
|
|
||||||
config,
|
|
||||||
&plugins_manager,
|
|
||||||
auth,
|
|
||||||
loaded_plugin_app_connector_ids,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn list_discoverable_plugins_with_manager_and_auth(
|
|
||||||
config: &crate::config::Config,
|
|
||||||
plugins_manager: &PluginsManager,
|
|
||||||
auth: Option<&codex_login::CodexAuth>,
|
|
||||||
loaded_plugin_app_connector_ids: &[String],
|
|
||||||
) -> anyhow::Result<Vec<DiscoverablePluginInfo>> {
|
|
||||||
super::list_tool_suggest_discoverable_plugins(
|
super::list_tool_suggest_discoverable_plugins(
|
||||||
config,
|
config,
|
||||||
plugins_manager,
|
&plugins_manager,
|
||||||
auth,
|
/*auth*/ None,
|
||||||
loaded_plugin_app_connector_ids,
|
loaded_plugin_app_connector_ids,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn list_tool_suggest_discoverable_plugins_includes_cached_remote_global_plugins() {
|
|
||||||
use codex_login::CodexAuth;
|
|
||||||
use serde_json::json;
|
|
||||||
use wiremock::Mock;
|
|
||||||
use wiremock::MockServer;
|
|
||||||
use wiremock::ResponseTemplate;
|
|
||||||
use wiremock::matchers::method;
|
|
||||||
use wiremock::matchers::path;
|
|
||||||
use wiremock::matchers::query_param;
|
|
||||||
|
|
||||||
let codex_home = tempdir().expect("tempdir should succeed");
|
|
||||||
write_file(
|
|
||||||
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
|
||||||
r#"[features]
|
|
||||||
plugins = true
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
let server = MockServer::start().await;
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/backend-api/ps/plugins/list"))
|
|
||||||
.and(query_param("scope", "GLOBAL"))
|
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"id": "plugins~Plugin_remote_github",
|
|
||||||
"name": "github",
|
|
||||||
"scope": "GLOBAL",
|
|
||||||
"installation_policy": "AVAILABLE",
|
|
||||||
"authentication_policy": "ON_USE",
|
|
||||||
"status": "AVAILABLE",
|
|
||||||
"release": {
|
|
||||||
"display_name": "Remote GitHub",
|
|
||||||
"description": "Remote GitHub long",
|
|
||||||
"app_ids": ["github"],
|
|
||||||
"interface": {
|
|
||||||
"short_description": "Remote GitHub short",
|
|
||||||
"long_description": null,
|
|
||||||
"developer_name": null,
|
|
||||||
"category": null,
|
|
||||||
"capabilities": [],
|
|
||||||
"website_url": null,
|
|
||||||
"privacy_policy_url": null,
|
|
||||||
"terms_of_service_url": null,
|
|
||||||
"brand_color": null,
|
|
||||||
"default_prompt": null,
|
|
||||||
"composer_icon_url": null,
|
|
||||||
"logo_url": null,
|
|
||||||
"screenshot_urls": []
|
|
||||||
},
|
|
||||||
"skills": [
|
|
||||||
{
|
|
||||||
"name": "github",
|
|
||||||
"description": "Use GitHub",
|
|
||||||
"interface": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "plugins~Plugin_remote_unlisted",
|
|
||||||
"name": "remote-unlisted",
|
|
||||||
"scope": "GLOBAL",
|
|
||||||
"installation_policy": "AVAILABLE",
|
|
||||||
"authentication_policy": "ON_USE",
|
|
||||||
"status": "AVAILABLE",
|
|
||||||
"release": {
|
|
||||||
"display_name": "Remote Unlisted",
|
|
||||||
"description": "Remote Unlisted long",
|
|
||||||
"app_ids": [],
|
|
||||||
"interface": {
|
|
||||||
"short_description": "Remote Unlisted short",
|
|
||||||
"long_description": null,
|
|
||||||
"developer_name": null,
|
|
||||||
"category": null,
|
|
||||||
"capabilities": [],
|
|
||||||
"website_url": null,
|
|
||||||
"privacy_policy_url": null,
|
|
||||||
"terms_of_service_url": null,
|
|
||||||
"brand_color": null,
|
|
||||||
"default_prompt": null,
|
|
||||||
"composer_icon_url": null,
|
|
||||||
"logo_url": null,
|
|
||||||
"screenshot_urls": []
|
|
||||||
},
|
|
||||||
"skills": [
|
|
||||||
{
|
|
||||||
"name": "remote-unlisted",
|
|
||||||
"description": "Use unlisted remote plugin",
|
|
||||||
"interface": null
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "plugins~Plugin_remote_slack_not_available",
|
|
||||||
"name": "slack",
|
|
||||||
"scope": "GLOBAL",
|
|
||||||
"installation_policy": "NOT_AVAILABLE",
|
|
||||||
"authentication_policy": "ON_USE",
|
|
||||||
"status": "AVAILABLE",
|
|
||||||
"release": {
|
|
||||||
"display_name": "Remote Slack",
|
|
||||||
"description": "Remote Slack long",
|
|
||||||
"app_ids": [],
|
|
||||||
"interface": {
|
|
||||||
"short_description": "Remote Slack short",
|
|
||||||
"long_description": null,
|
|
||||||
"developer_name": null,
|
|
||||||
"category": null,
|
|
||||||
"capabilities": [],
|
|
||||||
"website_url": null,
|
|
||||||
"privacy_policy_url": null,
|
|
||||||
"terms_of_service_url": null,
|
|
||||||
"brand_color": null,
|
|
||||||
"default_prompt": null,
|
|
||||||
"composer_icon_url": null,
|
|
||||||
"logo_url": null,
|
|
||||||
"screenshot_urls": []
|
|
||||||
},
|
|
||||||
"skills": []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "plugins~Plugin_remote_figma_admin_disabled",
|
|
||||||
"name": "figma",
|
|
||||||
"scope": "GLOBAL",
|
|
||||||
"installation_policy": "AVAILABLE",
|
|
||||||
"authentication_policy": "ON_USE",
|
|
||||||
"status": "DISABLED_BY_ADMIN",
|
|
||||||
"release": {
|
|
||||||
"display_name": "Remote Figma",
|
|
||||||
"description": "Remote Figma long",
|
|
||||||
"app_ids": [],
|
|
||||||
"interface": {
|
|
||||||
"short_description": "Remote Figma short",
|
|
||||||
"long_description": null,
|
|
||||||
"developer_name": null,
|
|
||||||
"category": null,
|
|
||||||
"capabilities": [],
|
|
||||||
"website_url": null,
|
|
||||||
"privacy_policy_url": null,
|
|
||||||
"terms_of_service_url": null,
|
|
||||||
"brand_color": null,
|
|
||||||
"default_prompt": null,
|
|
||||||
"composer_icon_url": null,
|
|
||||||
"logo_url": null,
|
|
||||||
"screenshot_urls": []
|
|
||||||
},
|
|
||||||
"skills": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"next_page_token": null
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
.expect(1)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
|
||||||
let mut config = load_plugins_config(codex_home.path()).await;
|
|
||||||
config.chatgpt_base_url = format!("{}/backend-api", server.uri());
|
|
||||||
let plugins_manager = PluginsManager::new(config.codex_home.to_path_buf());
|
|
||||||
fetch_and_cache_global_remote_plugin_catalog(
|
|
||||||
codex_home.path(),
|
|
||||||
&RemotePluginServiceConfig::new(
|
|
||||||
config.chatgpt_base_url.clone(),
|
|
||||||
config.http_client_factory(),
|
|
||||||
),
|
|
||||||
Some(&auth),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("remote plugin catalog cache should write");
|
|
||||||
|
|
||||||
let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(
|
|
||||||
&config,
|
|
||||||
&plugins_manager,
|
|
||||||
Some(&auth),
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
discoverable_plugins
|
|
||||||
.iter()
|
|
||||||
.all(|plugin| plugin.id != "github@openai-curated-remote")
|
|
||||||
);
|
|
||||||
|
|
||||||
for scope in ["GLOBAL", "USER", "WORKSPACE"] {
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/backend-api/ps/plugins/installed"))
|
|
||||||
.and(query_param("scope", scope))
|
|
||||||
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
|
|
||||||
"plugins": [],
|
|
||||||
"pagination": {
|
|
||||||
"next_page_token": null
|
|
||||||
}
|
|
||||||
})))
|
|
||||||
.expect(1)
|
|
||||||
.mount(&server)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
plugins_manager
|
|
||||||
.build_and_cache_remote_installed_plugin_marketplaces(
|
|
||||||
&config.plugins_config_input(),
|
|
||||||
Some(&auth),
|
|
||||||
&[REMOTE_GLOBAL_MARKETPLACE_NAME],
|
|
||||||
/*on_effective_plugins_changed*/ None,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("remote installed plugin cache should write");
|
|
||||||
|
|
||||||
let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(
|
|
||||||
&config,
|
|
||||||
&plugins_manager,
|
|
||||||
Some(&auth),
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
discoverable_plugins
|
|
||||||
.iter()
|
|
||||||
.filter(|plugin| plugin.id.ends_with("@openai-curated-remote"))
|
|
||||||
.map(|plugin| plugin.id.as_str())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec!["github@openai-curated-remote"]
|
|
||||||
);
|
|
||||||
let remote_plugins = discoverable_plugins
|
|
||||||
.into_iter()
|
|
||||||
.filter(|plugin| plugin.id == "github@openai-curated-remote")
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
remote_plugins,
|
|
||||||
vec![DiscoverablePluginInfo {
|
|
||||||
id: "github@openai-curated-remote".to_string(),
|
|
||||||
remote_plugin_id: Some("plugins~Plugin_remote_github".to_string()),
|
|
||||||
name: "Remote GitHub".to_string(),
|
|
||||||
description: Some("Remote GitHub short".to_string()),
|
|
||||||
has_skills: true,
|
|
||||||
mcp_server_names: Vec::new(),
|
|
||||||
app_connector_ids: vec!["github".to_string()],
|
|
||||||
}]
|
|
||||||
);
|
|
||||||
|
|
||||||
write_file(
|
|
||||||
&codex_home.path().join(crate::config::CONFIG_TOML_FILE),
|
|
||||||
r#"[features]
|
|
||||||
plugins = true
|
|
||||||
|
|
||||||
[tool_suggest]
|
|
||||||
disabled_tools = [
|
|
||||||
{ type = "plugin", id = "github@openai-curated-remote" }
|
|
||||||
]
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
let mut config_with_disabled_remote_plugin = load_plugins_config(codex_home.path()).await;
|
|
||||||
config_with_disabled_remote_plugin.chatgpt_base_url = config.chatgpt_base_url.clone();
|
|
||||||
let discoverable_plugins = list_discoverable_plugins_with_manager_and_auth(
|
|
||||||
&config_with_disabled_remote_plugin,
|
|
||||||
&plugins_manager,
|
|
||||||
Some(&auth),
|
|
||||||
&[],
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(
|
|
||||||
discoverable_plugins
|
|
||||||
.iter()
|
|
||||||
.all(|plugin| plugin.id != "github@openai-curated-remote")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn list_tool_suggest_discoverable_plugins_returns_empty_when_plugins_feature_disabled() {
|
async fn list_tool_suggest_discoverable_plugins_returns_empty_when_plugins_feature_disabled() {
|
||||||
let codex_home = tempdir().expect("tempdir should succeed");
|
let codex_home = tempdir().expect("tempdir should succeed");
|
||||||
|
|||||||
@@ -355,7 +355,6 @@ async fn mount_empty_remote_installed_plugins(server: &wiremock::MockServer) ->
|
|||||||
async fn mount_remote_calendar_installed_plugins(server: &wiremock::MockServer) {
|
async fn mount_remote_calendar_installed_plugins(server: &wiremock::MockServer) {
|
||||||
Mock::given(method("GET"))
|
Mock::given(method("GET"))
|
||||||
.and(path("/ps/plugins/installed"))
|
.and(path("/ps/plugins/installed"))
|
||||||
.and(query_param("scope", "GLOBAL"))
|
|
||||||
.respond_with(remote_installed_plugins_response(vec![json!({
|
.respond_with(remote_installed_plugins_response(vec![json!({
|
||||||
"id": REMOTE_CALENDAR_PLUGIN_ID,
|
"id": REMOTE_CALENDAR_PLUGIN_ID,
|
||||||
"name": "calendar",
|
"name": "calendar",
|
||||||
@@ -373,15 +372,6 @@ async fn mount_remote_calendar_installed_plugins(server: &wiremock::MockServer)
|
|||||||
.with_priority(1)
|
.with_priority(1)
|
||||||
.mount(server)
|
.mount(server)
|
||||||
.await;
|
.await;
|
||||||
for scope in ["WORKSPACE", "USER"] {
|
|
||||||
Mock::given(method("GET"))
|
|
||||||
.and(path("/ps/plugins/installed"))
|
|
||||||
.and(query_param("scope", scope))
|
|
||||||
.respond_with(remote_installed_plugins_response(Vec::new()))
|
|
||||||
.with_priority(1)
|
|
||||||
.mount(server)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
|||||||
Reference in new issue
Block a user