mirror of
https://github.com/openai/codex.git
synced 2026-09-09 15:58:47 +00:00
## 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
689 lines
24 KiB
Rust
689 lines
24 KiB
Rust
use super::*;
|
|
use crate::test_support::recorded_http_client_urls;
|
|
use crate::test_support::recording_remote_plugin_service_config;
|
|
use pretty_assertions::assert_eq;
|
|
use wiremock::Mock;
|
|
use wiremock::MockServer;
|
|
use wiremock::ResponseTemplate;
|
|
use wiremock::matchers::header_exists;
|
|
use wiremock::matchers::method;
|
|
use wiremock::matchers::path;
|
|
use wiremock::matchers::query_param;
|
|
use wiremock::matchers::query_param_is_missing;
|
|
|
|
#[tokio::test]
|
|
async fn remote_plugin_list_routes_the_complete_query_url() {
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("GET"))
|
|
.and(path("/backend-api/ps/plugins/list"))
|
|
.and(header_exists("user-agent"))
|
|
.and(header_exists("originator"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
|
|
"plugins": [],
|
|
"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();
|
|
|
|
get_remote_plugin_list_page(
|
|
&config,
|
|
&auth,
|
|
RemotePluginScope::Global,
|
|
Some("next page/+"),
|
|
Some("vertical & special"),
|
|
)
|
|
.await
|
|
.expect("plugin list request should succeed");
|
|
|
|
assert_eq!(
|
|
recorded_http_client_urls(&selected_urls),
|
|
vec![format!(
|
|
"{}/backend-api/ps/plugins/list?scope=GLOBAL&limit=200&collection=vertical+%26+special&pageToken=next+page%2F%2B",
|
|
server.uri()
|
|
)]
|
|
);
|
|
}
|
|
|
|
#[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]
|
|
fn cached_remote_plugin_catalog_scopes_returns_existing_scopes() {
|
|
let codex_home = tempfile::tempdir().expect("create codex home");
|
|
let config = RemotePluginServiceConfig::new(
|
|
"https://chatgpt.com/backend-api".to_string(),
|
|
crate::test_support::test_http_client_factory(),
|
|
);
|
|
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
|
|
for scope in [RemotePluginScope::Global, RemotePluginScope::Workspace] {
|
|
catalog_cache::write_cached_directory_plugins(
|
|
codex_home.path(),
|
|
&config,
|
|
&auth,
|
|
scope,
|
|
&[],
|
|
);
|
|
}
|
|
|
|
assert_eq!(
|
|
cached_remote_plugin_catalog_scopes(codex_home.path(), &config, Some(&auth)),
|
|
BTreeSet::from([RemotePluginScope::Global, RemotePluginScope::Workspace])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn build_remote_marketplace_preserves_directory_order_and_appends_installed_only_plugins() {
|
|
let directory_plugins = vec![
|
|
directory_plugin("plugin-z", "zulu"),
|
|
directory_plugin("plugin-m", "mike"),
|
|
];
|
|
let installed_plugins = vec![RemotePluginInstalledItem {
|
|
plugin: directory_plugin("plugin-a", "alpha"),
|
|
installed_at: None,
|
|
enabled: true,
|
|
disabled_skill_names: Vec::new(),
|
|
}];
|
|
|
|
let marketplace = build_remote_marketplace(
|
|
"marketplace",
|
|
"Marketplace",
|
|
directory_plugins,
|
|
installed_plugins,
|
|
/*include_installed_only*/ true,
|
|
)
|
|
.expect("marketplace should be valid")
|
|
.expect("marketplace should not be empty");
|
|
|
|
assert_eq!(
|
|
marketplace
|
|
.plugins
|
|
.into_iter()
|
|
.map(|plugin| plugin.remote_plugin_id)
|
|
.collect::<Vec<_>>(),
|
|
vec!["plugin-z", "plugin-m", "plugin-a"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn installation_policy_source_is_preserved_across_remote_summary_paths() {
|
|
let mut directory_plugin = directory_plugin("plugin-linear", "linear");
|
|
directory_plugin.installation_policy_source =
|
|
Some(RemotePluginInstallPolicySource::ImplicitCanonicalApp);
|
|
let installed_plugin = RemotePluginInstalledItem {
|
|
plugin: directory_plugin.clone(),
|
|
installed_at: None,
|
|
enabled: true,
|
|
disabled_skill_names: Vec::new(),
|
|
};
|
|
|
|
let marketplace = build_remote_marketplace(
|
|
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
|
REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
|
vec![directory_plugin],
|
|
vec![installed_plugin.clone()],
|
|
/*include_installed_only*/ false,
|
|
)
|
|
.expect("marketplace should be valid")
|
|
.expect("marketplace should not be empty");
|
|
assert_eq!(
|
|
marketplace
|
|
.plugins
|
|
.into_iter()
|
|
.map(|plugin| plugin.install_policy_source)
|
|
.collect::<Vec<_>>(),
|
|
vec![Some(PluginInstallPolicySource::ImplicitCanonicalApp)]
|
|
);
|
|
|
|
let mut installed_plugin = installed_plugin;
|
|
installed_plugin.plugin.installation_policy_source =
|
|
Some(RemotePluginInstallPolicySource::WorkspaceSetting);
|
|
let installed_plugin = remote_installed_plugin_to_cache_entry(&installed_plugin)
|
|
.expect("installed plugin should be valid");
|
|
let marketplaces = group_remote_installed_plugins_by_marketplaces(
|
|
&[installed_plugin],
|
|
&[REMOTE_GLOBAL_MARKETPLACE_NAME],
|
|
);
|
|
assert_eq!(
|
|
marketplaces
|
|
.into_iter()
|
|
.flat_map(|marketplace| marketplace.plugins)
|
|
.map(|plugin| plugin.install_policy_source)
|
|
.collect::<Vec<_>>(),
|
|
vec![Some(PluginInstallPolicySource::WorkspaceSetting)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn plan_eligibility_is_preserved_across_remote_summary_paths() {
|
|
let mut directory_plugin = directory_plugin("plugin-gmail", "gmail");
|
|
directory_plugin.installation_policy = PluginInstallPolicy::NotAvailable;
|
|
directory_plugin.availability = PluginAvailability::DisabledByAdmin;
|
|
directory_plugin.disabled_reason = Some(PluginDisabledReason::PlanNotEligible);
|
|
directory_plugin.eligible_plan_types = Some(vec![
|
|
"plus".to_string(),
|
|
"pro".to_string(),
|
|
"enterprise_cbp_automation".to_string(),
|
|
]);
|
|
let installed_plugin = RemotePluginInstalledItem {
|
|
plugin: directory_plugin.clone(),
|
|
installed_at: None,
|
|
enabled: false,
|
|
disabled_skill_names: Vec::new(),
|
|
};
|
|
|
|
let marketplace = build_remote_marketplace(
|
|
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
|
REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
|
vec![directory_plugin],
|
|
Vec::new(),
|
|
/*include_installed_only*/ false,
|
|
)
|
|
.expect("marketplace should be valid")
|
|
.expect("marketplace should not be empty");
|
|
let expected = vec![(
|
|
PluginAvailability::DisabledByAdmin,
|
|
Some(PluginDisabledReason::PlanNotEligible),
|
|
Some(vec![
|
|
"plus".to_string(),
|
|
"pro".to_string(),
|
|
"enterprise_cbp_automation".to_string(),
|
|
]),
|
|
)];
|
|
assert_eq!(
|
|
marketplace
|
|
.plugins
|
|
.into_iter()
|
|
.map(|plugin| (
|
|
plugin.availability,
|
|
plugin.disabled_reason,
|
|
plugin.eligible_plan_types,
|
|
))
|
|
.collect::<Vec<_>>(),
|
|
expected
|
|
);
|
|
|
|
let installed_plugin = remote_installed_plugin_to_cache_entry(&installed_plugin)
|
|
.expect("installed plugin should be valid");
|
|
let marketplaces = group_remote_installed_plugins_by_marketplaces(
|
|
&[installed_plugin],
|
|
&[REMOTE_GLOBAL_MARKETPLACE_NAME],
|
|
);
|
|
assert_eq!(
|
|
marketplaces
|
|
.into_iter()
|
|
.flat_map(|marketplace| marketplace.plugins)
|
|
.map(|plugin| (
|
|
plugin.availability,
|
|
plugin.disabled_reason,
|
|
plugin.eligible_plan_types,
|
|
))
|
|
.collect::<Vec<_>>(),
|
|
expected
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_plugin_disabled_reason_preserves_remote_catalog_compatibility() {
|
|
let plugin = directory_plugin("plugin-gmail", "gmail");
|
|
let mut plugin_json = serde_json::to_value(plugin).expect("plugin should serialize");
|
|
plugin_json["disabled_reason"] = serde_json::json!("future_disabled_reason");
|
|
|
|
let plugin: RemotePluginDirectoryItem =
|
|
serde_json::from_value(plugin_json).expect("unknown reason should deserialize");
|
|
|
|
assert_eq!(plugin.disabled_reason, Some(PluginDisabledReason::Unknown));
|
|
}
|
|
|
|
#[test]
|
|
fn installation_interstitial_requirement_is_preserved_across_remote_summary_paths() {
|
|
let mut directory_plugin = directory_plugin("plugin-linear", "linear");
|
|
directory_plugin.must_show_installation_interstitial = Some(true);
|
|
let marketplace = build_remote_marketplace(
|
|
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
|
REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
|
vec![directory_plugin.clone()],
|
|
Vec::new(),
|
|
/*include_installed_only*/ false,
|
|
)
|
|
.expect("marketplace should be valid")
|
|
.expect("marketplace should not be empty");
|
|
assert_eq!(
|
|
marketplace
|
|
.plugins
|
|
.into_iter()
|
|
.map(|plugin| plugin.must_show_installation_interstitial)
|
|
.collect::<Vec<_>>(),
|
|
vec![Some(true)]
|
|
);
|
|
|
|
directory_plugin.must_show_installation_interstitial = Some(false);
|
|
let installed_plugin = remote_installed_plugin_to_cache_entry(&RemotePluginInstalledItem {
|
|
plugin: directory_plugin,
|
|
installed_at: None,
|
|
enabled: true,
|
|
disabled_skill_names: Vec::new(),
|
|
})
|
|
.expect("installed plugin should be valid");
|
|
let marketplaces = group_remote_installed_plugins_by_marketplaces(
|
|
&[installed_plugin],
|
|
&[REMOTE_GLOBAL_MARKETPLACE_NAME],
|
|
);
|
|
assert_eq!(
|
|
marketplaces
|
|
.into_iter()
|
|
.flat_map(|marketplace| marketplace.plugins)
|
|
.map(|plugin| plugin.must_show_installation_interstitial)
|
|
.collect::<Vec<_>>(),
|
|
vec![Some(false)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_installation_interstitial_requirement_deserializes_to_none() {
|
|
let plugin = directory_plugin("plugin-linear", "linear");
|
|
let mut plugin_json = serde_json::to_value(plugin).expect("plugin should serialize");
|
|
plugin_json
|
|
.as_object_mut()
|
|
.expect("plugin should serialize to an object")
|
|
.remove("must_show_installation_interstitial");
|
|
|
|
let plugin: RemotePluginDirectoryItem =
|
|
serde_json::from_value(plugin_json).expect("missing requirement should deserialize");
|
|
|
|
assert_eq!(plugin.must_show_installation_interstitial, None);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_installation_policy_source_maps_to_none() {
|
|
let plugin = directory_plugin("plugin-linear", "linear");
|
|
let mut plugin_json = serde_json::to_value(plugin).expect("plugin should serialize");
|
|
plugin_json["installation_policy_source"] =
|
|
serde_json::Value::String("FUTURE_POLICY_SOURCE".to_string());
|
|
let plugin: RemotePluginDirectoryItem =
|
|
serde_json::from_value(plugin_json).expect("unknown source should deserialize");
|
|
|
|
let summary = build_remote_plugin_summary(&plugin, /*installed_plugin*/ None)
|
|
.expect("summary should be valid");
|
|
|
|
assert_eq!(summary.install_policy_source, None);
|
|
}
|
|
|
|
#[test]
|
|
fn scheduled_task_metadata_distinguishes_unavailable_from_empty() {
|
|
let release = serde_json::json!({
|
|
"display_name": "Example",
|
|
"description": "Example plugin",
|
|
"interface": {},
|
|
});
|
|
let without_metadata: RemotePluginReleaseResponse =
|
|
serde_json::from_value(release.clone()).expect("release should deserialize");
|
|
assert_eq!(without_metadata.scheduled_tasks, None);
|
|
|
|
let mut with_empty_metadata = release;
|
|
with_empty_metadata["scheduled_tasks"] = serde_json::json!([]);
|
|
let with_empty_metadata: RemotePluginReleaseResponse =
|
|
serde_json::from_value(with_empty_metadata).expect("release should deserialize");
|
|
assert_eq!(with_empty_metadata.scheduled_tasks, Some(Vec::new()));
|
|
}
|
|
|
|
#[test]
|
|
fn workspace_share_context_preserves_publish_capability() {
|
|
let mut plugin = directory_plugin("plugin-workspace", "workspace plugin");
|
|
plugin.scope = RemotePluginScope::Workspace;
|
|
plugin.discoverability = Some(RemotePluginShareDiscoverability::Private);
|
|
plugin.can_publish_to_workspace = Some(true);
|
|
|
|
let context = remote_plugin_share_context(&plugin)
|
|
.expect("workspace plugin should be valid")
|
|
.expect("workspace plugin should have share context");
|
|
|
|
assert_eq!(context.can_publish_to_workspace, Some(true));
|
|
}
|
|
|
|
fn directory_plugin(id: &str, name: &str) -> RemotePluginDirectoryItem {
|
|
RemotePluginDirectoryItem {
|
|
id: id.to_string(),
|
|
name: name.to_string(),
|
|
scope: RemotePluginScope::Global,
|
|
discoverability: None,
|
|
creator_account_user_id: None,
|
|
creator_name: None,
|
|
share_url: None,
|
|
share_principals: None,
|
|
can_publish_to_workspace: None,
|
|
installation_policy: PluginInstallPolicy::Available,
|
|
installation_policy_source: None,
|
|
must_show_installation_interstitial: None,
|
|
authentication_policy: PluginAuthPolicy::OnUse,
|
|
availability: PluginAvailability::Available,
|
|
disabled_reason: None,
|
|
eligible_plan_types: None,
|
|
release: RemotePluginReleaseResponse {
|
|
version: None,
|
|
display_name: name.to_string(),
|
|
description: String::new(),
|
|
bundle_download_url: None,
|
|
app_ids: Vec::new(),
|
|
app_manifest: None,
|
|
app_templates: Vec::new(),
|
|
keywords: Vec::new(),
|
|
interface: RemotePluginReleaseInterfaceResponse {
|
|
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,
|
|
brand_color: None,
|
|
default_prompt: None,
|
|
default_prompts: None,
|
|
composer_icon_url: None,
|
|
logo_url: None,
|
|
logo_url_dark: None,
|
|
screenshot_urls: Vec::new(),
|
|
},
|
|
skills: Vec::new(),
|
|
mcp_servers: Vec::new(),
|
|
scheduled_tasks: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn remote_plugin_interface_maps_dark_logo_url() {
|
|
let mut plugin = directory_plugin("plugin-linear", "linear");
|
|
plugin.release.interface.logo_url_dark =
|
|
Some("https://example.com/linear/logo-dark.png".to_string());
|
|
|
|
assert_eq!(
|
|
remote_plugin_interface_to_info(&plugin)
|
|
.expect("plugin interface")
|
|
.logo_url_dark,
|
|
Some("https://example.com/linear/logo-dark.png".to_string())
|
|
);
|
|
}
|
|
fn item(name: &str, display_name: &str) -> RecommendedPluginItem {
|
|
RecommendedPluginItem {
|
|
id: format!("plugin_{name}"),
|
|
name: name.to_string(),
|
|
status: None,
|
|
installation_policy: None,
|
|
release: RecommendedPluginRelease {
|
|
display_name: display_name.to_string(),
|
|
app_ids: Vec::new(),
|
|
},
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn recommended_plugins_enabled_flag_selects_endpoint_or_legacy_mode() {
|
|
let disabled: RecommendedPluginsResponse = serde_json::from_value(serde_json::json!({
|
|
"enabled": false,
|
|
"plugins": [{"id": "plugin_github", "name": "github", "release": {"display_name": "GitHub"}}]
|
|
}))
|
|
.expect("response should deserialize");
|
|
assert_eq!(
|
|
recommended_plugins_mode(disabled),
|
|
RecommendedPluginsMode::Legacy
|
|
);
|
|
|
|
for response in [
|
|
serde_json::json!({"plugins": []}),
|
|
serde_json::json!({"enabled": null, "plugins": []}),
|
|
] {
|
|
let response: RecommendedPluginsResponse =
|
|
serde_json::from_value(response).expect("response should deserialize");
|
|
assert_eq!(
|
|
recommended_plugins_mode(response),
|
|
RecommendedPluginsMode::Legacy
|
|
);
|
|
}
|
|
|
|
let enabled: RecommendedPluginsResponse = serde_json::from_value(serde_json::json!({
|
|
"enabled": true,
|
|
"plugins": []
|
|
}))
|
|
.expect("response should deserialize");
|
|
assert_eq!(
|
|
recommended_plugins_mode(enabled),
|
|
RecommendedPluginsMode::Endpoint {
|
|
plugins: Vec::new()
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recommended_plugins_require_remote_install_identity() {
|
|
let response = serde_json::from_value::<RecommendedPluginsResponse>(serde_json::json!({
|
|
"enabled": true,
|
|
"plugins": [{
|
|
"name": "github",
|
|
"release": {"display_name": "GitHub"}
|
|
}]
|
|
}));
|
|
|
|
assert!(response.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn recommended_plugins_are_validated_deduplicated_sorted_and_capped() {
|
|
let mut plugins = (0..=52)
|
|
.rev()
|
|
.map(|index| item(&format!("plugin-{index:02}"), &format!("Plugin {index:02}")))
|
|
.collect::<Vec<_>>();
|
|
plugins.push(item("plugin-00", "Duplicate"));
|
|
plugins.push(item("not/a/plugin", "Invalid"));
|
|
plugins.push(RecommendedPluginItem {
|
|
id: "plugin_disabled".to_string(),
|
|
name: "disabled".to_string(),
|
|
status: Some(PluginAvailability::DisabledByAdmin),
|
|
installation_policy: Some(PluginInstallPolicy::Available),
|
|
release: RecommendedPluginRelease {
|
|
display_name: "Disabled".to_string(),
|
|
app_ids: Vec::new(),
|
|
},
|
|
});
|
|
plugins.push(RecommendedPluginItem {
|
|
id: "plugin_not_available".to_string(),
|
|
name: "not-available".to_string(),
|
|
status: Some(PluginAvailability::Available),
|
|
installation_policy: Some(PluginInstallPolicy::NotAvailable),
|
|
release: RecommendedPluginRelease {
|
|
display_name: "Not Available".to_string(),
|
|
app_ids: Vec::new(),
|
|
},
|
|
});
|
|
|
|
let mode = recommended_plugins_mode(RecommendedPluginsResponse {
|
|
enabled: Some(true),
|
|
plugins,
|
|
});
|
|
let RecommendedPluginsMode::Endpoint { plugins } = mode else {
|
|
panic!("expected endpoint mode");
|
|
};
|
|
|
|
assert_eq!(plugins.len(), MAX_RECOMMENDED_PLUGINS);
|
|
assert_eq!(
|
|
plugins.first(),
|
|
Some(&RecommendedPlugin {
|
|
config_id: "plugin-00@openai-curated-remote".to_string(),
|
|
remote_plugin_id: "plugin_plugin-00".to_string(),
|
|
display_name: "Plugin 00".to_string(),
|
|
app_connector_ids: Vec::new(),
|
|
})
|
|
);
|
|
assert_eq!(
|
|
plugins.last(),
|
|
Some(&RecommendedPlugin {
|
|
config_id: "plugin-49@openai-curated-remote".to_string(),
|
|
remote_plugin_id: "plugin_plugin-49".to_string(),
|
|
display_name: "Plugin 49".to_string(),
|
|
app_connector_ids: Vec::new(),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recommended_plugins_bound_model_visible_fields() {
|
|
let overlong_name = "n".repeat(MAX_RECOMMENDED_PLUGIN_NAME_LEN + 1);
|
|
let overlong_display_name = "D".repeat(MAX_RECOMMENDED_PLUGIN_DISPLAY_NAME_LEN + 1);
|
|
let mode = recommended_plugins_mode(RecommendedPluginsResponse {
|
|
enabled: Some(true),
|
|
plugins: vec![
|
|
item(&overlong_name, "Ignored"),
|
|
item("bounded", &overlong_display_name),
|
|
],
|
|
});
|
|
|
|
assert_eq!(
|
|
mode,
|
|
RecommendedPluginsMode::Endpoint {
|
|
plugins: vec![RecommendedPlugin {
|
|
config_id: "bounded@openai-curated-remote".to_string(),
|
|
remote_plugin_id: "plugin_bounded".to_string(),
|
|
display_name: "D".repeat(MAX_RECOMMENDED_PLUGIN_DISPLAY_NAME_LEN),
|
|
app_connector_ids: Vec::new(),
|
|
}],
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recommended_plugins_preserve_install_identity_and_normalize_app_ids() {
|
|
let mode = recommended_plugins_mode(RecommendedPluginsResponse {
|
|
enabled: Some(true),
|
|
plugins: vec![RecommendedPluginItem {
|
|
id: "plugin_connector_sample".to_string(),
|
|
name: "sample".to_string(),
|
|
status: Some(PluginAvailability::Available),
|
|
installation_policy: Some(PluginInstallPolicy::Available),
|
|
release: RecommendedPluginRelease {
|
|
display_name: "Sample".to_string(),
|
|
app_ids: vec![
|
|
"connector_one".to_string(),
|
|
String::new(),
|
|
"connector_two".to_string(),
|
|
"connector_one".to_string(),
|
|
],
|
|
},
|
|
}],
|
|
});
|
|
|
|
assert_eq!(
|
|
mode,
|
|
RecommendedPluginsMode::Endpoint {
|
|
plugins: vec![RecommendedPlugin {
|
|
config_id: "sample@openai-curated-remote".to_string(),
|
|
remote_plugin_id: "plugin_connector_sample".to_string(),
|
|
display_name: "Sample".to_string(),
|
|
app_connector_ids: vec!["connector_one".to_string(), "connector_two".to_string(),],
|
|
}],
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recommended_plugins_ignore_invalid_remote_plugin_ids() {
|
|
let mode = recommended_plugins_mode(RecommendedPluginsResponse {
|
|
enabled: Some(true),
|
|
plugins: vec![RecommendedPluginItem {
|
|
id: "not/a/plugin".to_string(),
|
|
name: "sample".to_string(),
|
|
status: None,
|
|
installation_policy: None,
|
|
release: RecommendedPluginRelease {
|
|
display_name: "Sample".to_string(),
|
|
app_ids: Vec::new(),
|
|
},
|
|
}],
|
|
});
|
|
|
|
assert_eq!(
|
|
mode,
|
|
RecommendedPluginsMode::Endpoint {
|
|
plugins: Vec::new(),
|
|
}
|
|
);
|
|
}
|