mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat: Add vertical remote plugin collection support (#23584)
- Adds an explicit vertical marketplace kind for plugin/list that fail-open fetches collection=vertical only when full remote plugins are disabled. - Renames the global remote marketplace/cache identity to openai-curated-remote and materializes remote installs with backend release versions and app manifests.
This commit is contained in:
committed by
GitHub
Unverified
parent
9dda71dbae
commit
dc255b0d8a
@@ -150,7 +150,7 @@ async fn load_config(codex_home: &Path, cwd: &Path) -> PluginsConfigInput {
|
||||
|
||||
fn remote_installed_linear_plugin() -> RemoteInstalledPlugin {
|
||||
RemoteInstalledPlugin {
|
||||
marketplace_name: "chatgpt-global".to_string(),
|
||||
marketplace_name: "openai-curated-remote".to_string(),
|
||||
id: "plugins~Plugin_linear".to_string(),
|
||||
name: "linear".to_string(),
|
||||
enabled: true,
|
||||
@@ -401,11 +401,11 @@ async fn build_remote_installed_plugin_marketplaces_from_cache_uses_remote_metad
|
||||
.build_remote_installed_plugin_marketplaces_from_cache(&[RemotePluginScope::Global])
|
||||
.expect("remote installed cache should be present");
|
||||
assert_eq!(marketplaces.len(), 1);
|
||||
assert_eq!(marketplaces[0].name, "chatgpt-global");
|
||||
assert_eq!(marketplaces[0].display_name, "ChatGPT Plugins");
|
||||
assert_eq!(marketplaces[0].name, "openai-curated-remote");
|
||||
assert_eq!(marketplaces[0].display_name, "OpenAI Curated Remote");
|
||||
assert_eq!(marketplaces[0].plugins.len(), 1);
|
||||
let plugin = &marketplaces[0].plugins[0];
|
||||
assert_eq!(plugin.id, "linear@chatgpt-global");
|
||||
assert_eq!(plugin.id, "linear@openai-curated-remote");
|
||||
assert_eq!(plugin.remote_plugin_id, "plugins~Plugin_linear");
|
||||
assert_eq!(plugin.name, "linear");
|
||||
assert_eq!(plugin.installed, true);
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_plugin::PluginId;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use reqwest::RequestBuilder;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::HashSet;
|
||||
@@ -46,19 +47,20 @@ pub use share::load_plugin_share_remote_ids_by_local_path;
|
||||
pub use share::save_remote_plugin_share;
|
||||
pub use share::update_remote_plugin_share_targets;
|
||||
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "chatgpt-global";
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_NAME: &str = "openai-curated-remote";
|
||||
pub const REMOTE_WORKSPACE_MARKETPLACE_NAME: &str = "workspace-directory";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_MARKETPLACE_NAME: &str = "workspace-shared-with-me";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_NAME: &str =
|
||||
"workspace-shared-with-me-private";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_NAME: &str =
|
||||
"workspace-shared-with-me-unlisted";
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME: &str = "ChatGPT Plugins";
|
||||
pub const REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME: &str = "OpenAI Curated Remote";
|
||||
pub const REMOTE_WORKSPACE_MARKETPLACE_DISPLAY_NAME: &str = "Workspace Directory";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_PRIVATE_MARKETPLACE_DISPLAY_NAME: &str = "Shared with me";
|
||||
pub const REMOTE_WORKSPACE_SHARED_WITH_ME_UNLISTED_MARKETPLACE_DISPLAY_NAME: &str =
|
||||
"Shared with me (unlisted)";
|
||||
|
||||
const OPENAI_CURATED_REMOTE_COLLECTION_KEY: &str = "vertical";
|
||||
const REMOTE_PLUGIN_CATALOG_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const REMOTE_PLUGIN_LIST_PAGE_LIMIT: u32 = 200;
|
||||
const MAX_REMOTE_DEFAULT_PROMPT_LEN: usize = 128;
|
||||
@@ -158,6 +160,7 @@ pub struct RemotePluginDetail {
|
||||
pub description: Option<String>,
|
||||
pub release_version: Option<String>,
|
||||
pub bundle_download_url: Option<String>,
|
||||
pub app_manifest: Option<JsonValue>,
|
||||
pub skills: Vec<RemotePluginSkill>,
|
||||
pub app_ids: Vec<String>,
|
||||
}
|
||||
@@ -391,6 +394,8 @@ struct RemotePluginReleaseResponse {
|
||||
#[serde(default)]
|
||||
app_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
app_manifest: Option<JsonValue>,
|
||||
#[serde(default)]
|
||||
keywords: Vec<String>,
|
||||
interface: RemotePluginReleaseInterfaceResponse,
|
||||
#[serde(default)]
|
||||
@@ -595,6 +600,31 @@ pub async fn fetch_remote_marketplaces(
|
||||
Ok(marketplaces)
|
||||
}
|
||||
|
||||
pub async fn fetch_openai_curated_remote_collection_marketplace(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: Option<&CodexAuth>,
|
||||
) -> Result<Option<RemoteMarketplace>, RemotePluginCatalogError> {
|
||||
let auth = ensure_chatgpt_auth(auth)?;
|
||||
let scope = RemotePluginScope::Global;
|
||||
let (directory_plugins, installed_plugins) = tokio::try_join!(
|
||||
fetch_directory_plugins_for_scope_with_collection(
|
||||
config,
|
||||
auth,
|
||||
scope,
|
||||
OPENAI_CURATED_REMOTE_COLLECTION_KEY,
|
||||
),
|
||||
fetch_installed_plugins_for_scope(config, auth, scope),
|
||||
)?;
|
||||
|
||||
build_remote_marketplace(
|
||||
REMOTE_GLOBAL_MARKETPLACE_NAME,
|
||||
REMOTE_GLOBAL_MARKETPLACE_DISPLAY_NAME,
|
||||
directory_plugins,
|
||||
installed_plugins,
|
||||
/*include_installed_only*/ false,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_remote_marketplace(
|
||||
name: &str,
|
||||
display_name: &str,
|
||||
@@ -866,6 +896,7 @@ async fn build_remote_plugin_detail(
|
||||
description: non_empty_string(Some(&plugin.release.description)),
|
||||
release_version: plugin.release.version,
|
||||
bundle_download_url: plugin.release.bundle_download_url,
|
||||
app_manifest: plugin.release.app_manifest,
|
||||
skills,
|
||||
app_ids: plugin.release.app_ids,
|
||||
})
|
||||
@@ -1174,12 +1205,40 @@ async fn fetch_directory_plugins_for_scope(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
scope: RemotePluginScope,
|
||||
) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogError> {
|
||||
fetch_directory_plugins_for_scope_with_optional_collection(
|
||||
config, auth, scope, /*collection*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_directory_plugins_for_scope_with_collection(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
scope: RemotePluginScope,
|
||||
collection: &str,
|
||||
) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogError> {
|
||||
fetch_directory_plugins_for_scope_with_optional_collection(
|
||||
config,
|
||||
auth,
|
||||
scope,
|
||||
Some(collection),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_directory_plugins_for_scope_with_optional_collection(
|
||||
config: &RemotePluginServiceConfig,
|
||||
auth: &CodexAuth,
|
||||
scope: RemotePluginScope,
|
||||
collection: Option<&str>,
|
||||
) -> Result<Vec<RemotePluginDirectoryItem>, RemotePluginCatalogError> {
|
||||
let mut plugins = Vec::new();
|
||||
let mut page_token = None;
|
||||
loop {
|
||||
let response =
|
||||
get_remote_plugin_list_page(config, auth, scope, page_token.as_deref()).await?;
|
||||
get_remote_plugin_list_page(config, auth, scope, page_token.as_deref(), collection)
|
||||
.await?;
|
||||
plugins.extend(response.plugins);
|
||||
let Some(next_page_token) = response.pagination.next_page_token else {
|
||||
break;
|
||||
@@ -1249,6 +1308,7 @@ async fn get_remote_plugin_list_page(
|
||||
auth: &CodexAuth,
|
||||
scope: RemotePluginScope,
|
||||
page_token: Option<&str>,
|
||||
collection: Option<&str>,
|
||||
) -> Result<RemotePluginListResponse, RemotePluginCatalogError> {
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let url = format!("{base_url}/ps/plugins/list");
|
||||
@@ -1256,6 +1316,9 @@ async fn get_remote_plugin_list_page(
|
||||
let mut request = authenticated_request(client.get(&url), auth)?;
|
||||
request = request.query(&[("scope", scope.api_value())]);
|
||||
request = request.query(&[("limit", REMOTE_PLUGIN_LIST_PAGE_LIMIT)]);
|
||||
if let Some(collection) = collection {
|
||||
request = request.query(&[("collection", collection)]);
|
||||
}
|
||||
if let Some(page_token) = page_token {
|
||||
request = request.query(&[("pageToken", page_token)]);
|
||||
}
|
||||
|
||||
@@ -208,6 +208,7 @@ pub async fn sync_remote_installed_plugin_bundles_once(
|
||||
&plugin.name,
|
||||
release_version,
|
||||
plugin.release.bundle_download_url.as_deref(),
|
||||
plugin.release.app_manifest.clone(),
|
||||
) {
|
||||
Ok(bundle) => bundle,
|
||||
Err(err) => {
|
||||
@@ -511,7 +512,7 @@ mod tests {
|
||||
&installed_plugin_names_by_marketplace,
|
||||
)
|
||||
.expect("cleanup after install guard is dropped");
|
||||
assert_eq!(removed, vec!["linear@chatgpt-global".to_string()]);
|
||||
assert_eq!(removed, vec!["linear@openai-curated-remote".to_string()]);
|
||||
assert!(!cached_manifest.exists());
|
||||
}
|
||||
|
||||
|
||||
@@ -85,6 +85,7 @@ pub async fn checkout_remote_plugin_share(
|
||||
&plugin_name,
|
||||
detail.release_version.as_deref(),
|
||||
detail.bundle_download_url.as_deref(),
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.map_err(|err| {
|
||||
RemotePluginCatalogError::UnexpectedResponse(format!(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::remote::REMOTE_GLOBAL_MARKETPLACE_NAME;
|
||||
use crate::store::PluginInstallResult;
|
||||
use crate::store::PluginStore;
|
||||
use crate::store::PluginStoreError;
|
||||
@@ -10,6 +11,7 @@ use codex_utils_plugins::find_plugin_manifest_path;
|
||||
use flate2::read::GzDecoder;
|
||||
use reqwest::Response;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Value as JsonValue;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
@@ -33,6 +35,7 @@ const TEST_ALLOW_LOOPBACK_HTTP_REMOTE_PLUGIN_BUNDLES_ENV: &str =
|
||||
pub struct ValidatedRemotePluginBundle {
|
||||
pub plugin_id: PluginId,
|
||||
pub plugin_version: String,
|
||||
app_manifest: Option<JsonValue>,
|
||||
bundle_download_url: String,
|
||||
}
|
||||
|
||||
@@ -137,6 +140,7 @@ pub fn validate_remote_plugin_bundle(
|
||||
plugin_name: &str,
|
||||
release_version: Option<&str>,
|
||||
bundle_download_url: Option<&str>,
|
||||
app_manifest: Option<JsonValue>,
|
||||
) -> Result<ValidatedRemotePluginBundle, RemotePluginBundleInstallError> {
|
||||
let plugin_id = PluginId::new(plugin_name.to_string(), remote_marketplace_name.to_string())
|
||||
.map_err(|source| RemotePluginBundleInstallError::InvalidPluginId {
|
||||
@@ -187,6 +191,7 @@ pub fn validate_remote_plugin_bundle(
|
||||
Ok(ValidatedRemotePluginBundle {
|
||||
plugin_id,
|
||||
plugin_version,
|
||||
app_manifest,
|
||||
bundle_download_url,
|
||||
})
|
||||
}
|
||||
@@ -367,6 +372,7 @@ fn install_remote_plugin_bundle(
|
||||
|
||||
extract_plugin_bundle_tar_gz(&bundle_bytes, extract_dir.path())?;
|
||||
let plugin_root = find_extracted_plugin_root(extract_dir.path())?;
|
||||
prepare_extracted_remote_plugin_root(&plugin_root, &bundle)?;
|
||||
let plugin_root = AbsolutePathBuf::try_from(plugin_root).map_err(|err| {
|
||||
RemotePluginBundleInstallError::InvalidBundle(format!(
|
||||
"failed to resolve extracted remote plugin bundle root: {err}"
|
||||
@@ -436,6 +442,90 @@ fn extract_remote_plugin_bundle_to_path(
|
||||
Ok(destination)
|
||||
}
|
||||
|
||||
fn prepare_extracted_remote_plugin_root(
|
||||
plugin_root: &Path,
|
||||
bundle: &ValidatedRemotePluginBundle,
|
||||
) -> Result<(), RemotePluginBundleInstallError> {
|
||||
if bundle.plugin_id.marketplace_name != REMOTE_GLOBAL_MARKETPLACE_NAME {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
overwrite_plugin_manifest_version(plugin_root, &bundle.plugin_version)?;
|
||||
if let Some(app_manifest) = &bundle.app_manifest {
|
||||
overwrite_plugin_app_manifest(plugin_root, app_manifest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn overwrite_plugin_manifest_version(
|
||||
plugin_root: &Path,
|
||||
plugin_version: &str,
|
||||
) -> Result<(), RemotePluginBundleInstallError> {
|
||||
let manifest_path = find_plugin_manifest_path(plugin_root).ok_or_else(|| {
|
||||
RemotePluginBundleInstallError::InvalidBundle(
|
||||
"remote plugin bundle did not contain a valid plugin.json".to_string(),
|
||||
)
|
||||
})?;
|
||||
let contents = fs::read_to_string(&manifest_path).map_err(|source| {
|
||||
RemotePluginBundleInstallError::io("failed to read remote plugin manifest", source)
|
||||
})?;
|
||||
let mut manifest: JsonValue = serde_json::from_str(&contents).map_err(|err| {
|
||||
RemotePluginBundleInstallError::InvalidBundle(format!(
|
||||
"failed to parse remote plugin manifest: {err}"
|
||||
))
|
||||
})?;
|
||||
let Some(manifest_object) = manifest.as_object_mut() else {
|
||||
return Err(RemotePluginBundleInstallError::InvalidBundle(
|
||||
"remote plugin manifest must be a JSON object".to_string(),
|
||||
));
|
||||
};
|
||||
manifest_object.insert(
|
||||
"version".to_string(),
|
||||
JsonValue::String(plugin_version.to_string()),
|
||||
);
|
||||
write_json_file(
|
||||
&manifest_path,
|
||||
&manifest,
|
||||
"failed to write remote plugin manifest",
|
||||
)
|
||||
}
|
||||
|
||||
fn overwrite_plugin_app_manifest(
|
||||
plugin_root: &Path,
|
||||
app_manifest: &JsonValue,
|
||||
) -> Result<(), RemotePluginBundleInstallError> {
|
||||
let app_manifest_path = crate::manifest::load_plugin_manifest(plugin_root)
|
||||
.and_then(|manifest| manifest.paths.apps.map(|path| path.to_path_buf()))
|
||||
.unwrap_or_else(|| plugin_root.join(".app.json"));
|
||||
write_json_file(
|
||||
&app_manifest_path,
|
||||
app_manifest,
|
||||
"failed to write remote plugin app manifest",
|
||||
)
|
||||
}
|
||||
|
||||
fn write_json_file(
|
||||
path: &Path,
|
||||
value: &JsonValue,
|
||||
context: &'static str,
|
||||
) -> Result<(), RemotePluginBundleInstallError> {
|
||||
let parent = path.parent().ok_or_else(|| {
|
||||
RemotePluginBundleInstallError::InvalidBundle(format!(
|
||||
"remote plugin output path has no parent: {}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|source| RemotePluginBundleInstallError::io(context, source))?;
|
||||
let mut contents = serde_json::to_vec_pretty(value).map_err(|err| {
|
||||
RemotePluginBundleInstallError::InvalidBundle(format!(
|
||||
"failed to serialize remote plugin JSON override: {err}"
|
||||
))
|
||||
})?;
|
||||
contents.push(b'\n');
|
||||
fs::write(path, contents).map_err(|source| RemotePluginBundleInstallError::io(context, source))
|
||||
}
|
||||
|
||||
fn extract_plugin_bundle_tar_gz(
|
||||
bytes: &[u8],
|
||||
destination: &Path,
|
||||
@@ -624,15 +714,16 @@ mod tests {
|
||||
fn validate_remote_plugin_bundle_uses_detail_name_for_local_plugin_id() {
|
||||
let bundle = validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"chatgpt-global",
|
||||
"openai-curated-remote",
|
||||
"linear",
|
||||
Some("1.2.3"),
|
||||
Some("https://example.com/linear.tar.gz"),
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.expect("valid install plan");
|
||||
|
||||
assert_eq!(bundle.plugin_id.plugin_name, "linear");
|
||||
assert_eq!(bundle.plugin_id.marketplace_name, "chatgpt-global");
|
||||
assert_eq!(bundle.plugin_id.marketplace_name, "openai-curated-remote");
|
||||
assert_eq!(bundle.plugin_version, "1.2.3");
|
||||
assert_eq!(
|
||||
bundle.bundle_download_url.as_str(),
|
||||
@@ -644,10 +735,11 @@ mod tests {
|
||||
fn validate_remote_plugin_bundle_rejects_missing_release_version() {
|
||||
let err = validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"chatgpt-global",
|
||||
"openai-curated-remote",
|
||||
"linear",
|
||||
/*release_version*/ None,
|
||||
Some("https://example.com/linear.tar.gz"),
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.expect_err("missing release version should be rejected");
|
||||
|
||||
@@ -661,10 +753,11 @@ mod tests {
|
||||
fn validate_remote_plugin_bundle_rejects_invalid_release_version() {
|
||||
let err = validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"chatgpt-global",
|
||||
"openai-curated-remote",
|
||||
"linear",
|
||||
Some("../1.2.3"),
|
||||
Some("https://example.com/linear.tar.gz"),
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.expect_err("invalid release version should be rejected");
|
||||
|
||||
@@ -678,10 +771,11 @@ mod tests {
|
||||
fn validate_remote_plugin_bundle_rejects_missing_download_url() {
|
||||
let err = validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"chatgpt-global",
|
||||
"openai-curated-remote",
|
||||
"linear",
|
||||
Some("1.2.3"),
|
||||
/*bundle_download_url*/ None,
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.expect_err("missing bundle download URL should be rejected");
|
||||
|
||||
@@ -695,10 +789,11 @@ mod tests {
|
||||
fn validate_remote_plugin_bundle_rejects_unsupported_download_url_scheme() {
|
||||
let err = validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"chatgpt-global",
|
||||
"openai-curated-remote",
|
||||
"linear",
|
||||
Some("1.2.3"),
|
||||
Some("http://example.com/linear.tar.gz"),
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.expect_err("plain HTTP URLs should be rejected before cloud install");
|
||||
|
||||
@@ -755,6 +850,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn install_preserves_non_global_bundle_manifest_metadata() {
|
||||
let codex_home = tempdir().expect("tempdir");
|
||||
let bundle = validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"workspace-shared-with-me",
|
||||
"linear",
|
||||
Some("backend-version"),
|
||||
Some("https://example.com/linear.tar.gz"),
|
||||
Some(serde_json::json!({
|
||||
"apps": {
|
||||
"remote": {
|
||||
"id": "remote-app"
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
.expect("valid install plan");
|
||||
|
||||
let result = install_remote_plugin_bundle(
|
||||
codex_home.path().to_path_buf(),
|
||||
bundle,
|
||||
tar_gz_bytes(&[
|
||||
(
|
||||
".codex-plugin/plugin.json",
|
||||
br#"{"name":"linear","version":"bundle-version"}"#,
|
||||
/*mode*/ 0o644,
|
||||
),
|
||||
(
|
||||
".app.json",
|
||||
br#"{"apps":{"bundled":{"id":"bundled-app"}}}"#,
|
||||
/*mode*/ 0o644,
|
||||
),
|
||||
]),
|
||||
)
|
||||
.expect("install bundle");
|
||||
|
||||
assert_eq!(result.plugin_version, "backend-version");
|
||||
let installed_manifest: JsonValue = serde_json::from_str(
|
||||
&std::fs::read_to_string(
|
||||
result
|
||||
.installed_path
|
||||
.join(".codex-plugin/plugin.json")
|
||||
.as_path(),
|
||||
)
|
||||
.expect("read installed plugin manifest"),
|
||||
)
|
||||
.expect("parse installed plugin manifest");
|
||||
assert_eq!(
|
||||
installed_manifest,
|
||||
serde_json::json!({
|
||||
"name": "linear",
|
||||
"version": "bundle-version",
|
||||
})
|
||||
);
|
||||
let installed_app_manifest: JsonValue = serde_json::from_str(
|
||||
&std::fs::read_to_string(result.installed_path.join(".app.json").as_path())
|
||||
.expect("read installed app manifest"),
|
||||
)
|
||||
.expect("parse installed app manifest");
|
||||
assert_eq!(
|
||||
installed_app_manifest,
|
||||
serde_json::json!({
|
||||
"apps": {
|
||||
"bundled": {
|
||||
"id": "bundled-app",
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_extracted_plugin_root_uses_local_manifest_discovery() {
|
||||
let extraction_root = tempdir().expect("tempdir");
|
||||
@@ -866,10 +1033,11 @@ mod tests {
|
||||
fn valid_remote_plugin_bundle() -> ValidatedRemotePluginBundle {
|
||||
validate_remote_plugin_bundle(
|
||||
REMOTE_PLUGIN_ID,
|
||||
"chatgpt-global",
|
||||
"openai-curated-remote",
|
||||
"linear",
|
||||
Some("1.2.3"),
|
||||
Some("https://example.com/linear.tar.gz"),
|
||||
/*app_manifest*/ None,
|
||||
)
|
||||
.expect("valid install plan")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user