/plugins: add marketplace upgrade flow (#20478)

This PR adds marketplace upgrade to the `/plugins` menu so users can
update configured marketplaces. It adds a `Ctrl+U` shortcut on eligible
marketplace tabs, a loading state, and the app-server request flow
needed to perform `marketplace/upgrade`. After a successful upgrade, the
TUI refreshes plugin data, plugin mentions, and user config so updated
marketplace contents show up across the menu and other plugin surfaces.
It also preserves the current marketplace tab on no-op and failure paths
and surfaces backend error details directly in the TUI.

- Add a `Ctrl+U` upgrade option for user-configured marketplace tabs in
`/plugins`
- Show the upgrade footer hint only on upgradeable marketplace tabs
- Show a loading state during `marketplace/upgrade`
- Surface already-up-to-date and per-marketplace failure results from
the backend
- Refresh plugin data, plugin mentions, and user config after successful
upgrades
- Add tests and snapshot updates for the shortcut flow, loading state,
and failure messaging

Steps to test:
1. Add a `/plugin` marketplace to Codex TUI.
2. Open `/plugins`, move to that marketplace tab, and confirm the footer
shows `Ctrl+U` to upgrade.
3. Press `Ctrl+U` and confirm the popup switches into an upgrade loading
state.
4. When the request finishes, confirm you see the expected result:
updated marketplace contents on success, an already-up-to-date message
on no-op, or backend error details on failure. On no-op or failure,
confirm the popup stays on the same marketplace tab.
This commit is contained in:
canvrno-oai
2026-05-01 11:26:29 -07:00
committed by GitHub
Unverified
parent 2817866a32
commit 610eefb86b
8 changed files with 432 additions and 18 deletions
@@ -9,7 +9,11 @@ use codex_app_server_protocol::MarketplaceAddParams;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceRemoveParams;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::MarketplaceUpgradeParams;
use codex_app_server_protocol::MarketplaceUpgradeResponse;
use codex_app_server_protocol::RequestId;
use codex_utils_absolute_path::AbsolutePathBuf;
impl App {
@@ -168,6 +172,26 @@ impl App {
});
}
pub(super) fn fetch_marketplace_upgrade(
&mut self,
app_server: &AppServerSession,
cwd: PathBuf,
marketplace_name: Option<String>,
) {
let request_handle = app_server.request_handle();
let app_event_tx = self.app_event_tx.clone();
tokio::spawn(async move {
let cwd_for_event = cwd.clone();
let result = fetch_marketplace_upgrade(request_handle, marketplace_name)
.await
.map_err(|err| format!("Failed to upgrade marketplace: {err}"));
app_event_tx.send(AppEvent::MarketplaceUpgradeLoaded {
cwd: cwd_for_event,
result,
});
});
}
pub(super) fn fetch_plugin_install(
&mut self,
app_server: &AppServerSession,
@@ -685,6 +709,20 @@ pub(super) async fn fetch_marketplace_remove(
.await
.wrap_err("marketplace/remove failed in TUI")
}
pub(super) async fn fetch_marketplace_upgrade(
request_handle: AppServerRequestHandle,
marketplace_name: Option<String>,
) -> Result<MarketplaceUpgradeResponse> {
let request_id = RequestId::String(format!("marketplace-upgrade-{}", Uuid::new_v4()));
request_handle
.request_typed(ClientRequest::MarketplaceUpgrade {
request_id,
params: MarketplaceUpgradeParams { marketplace_name },
})
.await
.wrap_err("marketplace/upgrade failed in TUI")
}
pub(super) async fn fetch_plugin_install(
request_handle: AppServerRequestHandle,
marketplace_path: AbsolutePathBuf,
+29
View File
@@ -408,6 +408,10 @@ impl App {
self.chat_widget
.open_marketplace_remove_loading_popup(&marketplace_display_name);
}
AppEvent::OpenMarketplaceUpgradeLoading { marketplace_name } => {
self.chat_widget
.open_marketplace_upgrade_loading_popup(marketplace_name.as_deref());
}
AppEvent::OpenPluginDetailLoading {
plugin_display_name,
} => {
@@ -435,6 +439,12 @@ impl App {
AppEvent::FetchMarketplaceAdd { cwd, source } => {
self.fetch_marketplace_add(app_server, cwd, source);
}
AppEvent::FetchMarketplaceUpgrade {
cwd,
marketplace_name,
} => {
self.fetch_marketplace_upgrade(app_server, cwd, marketplace_name);
}
AppEvent::MarketplaceAddLoaded {
cwd,
source,
@@ -450,6 +460,25 @@ impl App {
self.fetch_plugins_list(app_server, cwd);
}
}
AppEvent::MarketplaceUpgradeLoaded { cwd, result } => {
let marketplace_contents_changed =
matches!(&result, Ok(response) if !response.upgraded_roots.is_empty());
if marketplace_contents_changed {
if let Err(err) = self.refresh_in_memory_config_from_disk().await {
tracing::warn!(
error = %err,
"failed to refresh config after marketplace upgrade"
);
}
self.chat_widget.refresh_plugin_mentions();
self.chat_widget.submit_op(AppCommand::reload_user_config());
}
self.chat_widget
.on_marketplace_upgrade_loaded(cwd.clone(), result);
if self.chat_widget.config_ref().cwd.as_path() == cwd.as_path() {
self.fetch_plugins_list(app_server, cwd);
}
}
AppEvent::FetchMarketplaceRemove {
cwd,
marketplace_name,
+18
View File
@@ -15,6 +15,7 @@ use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
use codex_app_server_protocol::AppInfo;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::MarketplaceUpgradeResponse;
use codex_app_server_protocol::McpServerStatus;
use codex_app_server_protocol::McpServerStatusDetail;
use codex_app_server_protocol::PluginInstallResponse;
@@ -354,6 +355,23 @@ pub(crate) enum AppEvent {
result: Result<MarketplaceRemoveResponse, String>,
},
/// Replace the plugins popup with a marketplace-upgrade loading state.
OpenMarketplaceUpgradeLoading {
marketplace_name: Option<String>,
},
/// Upgrade configured Git marketplaces.
FetchMarketplaceUpgrade {
cwd: PathBuf,
marketplace_name: Option<String>,
},
/// Result of upgrading configured Git marketplaces.
MarketplaceUpgradeLoaded {
cwd: PathBuf,
result: Result<MarketplaceUpgradeResponse, String>,
},
/// Replace the plugins popup with a plugin-detail loading state.
OpenPluginDetailLoading {
plugin_display_name: String,
+4
View File
@@ -5032,6 +5032,7 @@ impl ChatWidget {
} if modifiers.contains(KeyModifiers::CONTROL) && c.eq_ignore_ascii_case(&'c')
)
&& !key_hint::ctrl(KeyCode::Char('r')).is_press(key_event)
&& !key_hint::ctrl(KeyCode::Char('u')).is_press(key_event)
{
self.bottom_pane.handle_key_event(key_event);
if self.bottom_pane.no_modal_or_popup_active() {
@@ -10521,6 +10522,9 @@ impl ChatWidget {
&mut self,
plugins: Option<Vec<PluginCapabilitySummary>>,
) {
if self.bottom_pane.plugins() == plugins.as_ref() {
return;
}
self.bottom_pane.set_plugin_mentions(plugins);
}
+208 -15
View File
@@ -24,6 +24,7 @@ use crate::render::renderable::Renderable;
use crate::tui::FrameRequester;
use codex_app_server_protocol::MarketplaceAddResponse;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::MarketplaceUpgradeResponse;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInstallResponse;
@@ -37,6 +38,7 @@ use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
@@ -311,6 +313,26 @@ impl ChatWidget {
}
}
pub(crate) fn open_marketplace_upgrade_loading_popup(
&mut self,
marketplace_name: Option<&str>,
) {
self.plugins_active_tab_id = self
.bottom_pane
.active_tab_id_for_active_view(PLUGINS_SELECTION_VIEW_ID)
.map(str::to_string)
.or_else(|| self.plugins_active_tab_id.clone());
let params = self.marketplace_upgrade_loading_popup_params(marketplace_name);
if !self
.bottom_pane
.replace_selection_view_if_active(PLUGINS_SELECTION_VIEW_ID, params)
{
self.bottom_pane.show_selection_view(
self.marketplace_upgrade_loading_popup_params(marketplace_name),
);
}
}
pub(crate) fn open_marketplace_remove_confirmation(
&mut self,
marketplace_name: String,
@@ -569,8 +591,101 @@ impl ChatWidget {
}
}
pub(crate) fn on_marketplace_upgrade_loaded(
&mut self,
cwd: PathBuf,
result: Result<MarketplaceUpgradeResponse, String>,
) {
if self.config.cwd.as_path() != cwd.as_path() {
return;
}
match result {
Ok(response) => {
if response.upgraded_roots.len() == 1 {
self.plugins_active_tab_id =
Some(marketplace_tab_id_from_path(&response.upgraded_roots[0]));
}
let selected_count = response.selected_marketplaces.len();
let upgraded_count = response.upgraded_roots.len();
let error_count = response.errors.len();
if selected_count == 0 {
self.add_info_message(
"No configured Git marketplaces to upgrade.".to_string(),
Some("Only configured Git marketplaces can be upgraded.".to_string()),
);
return;
}
if upgraded_count == 0 && error_count == 0 {
let message = if selected_count == 1 {
format!(
"Marketplace {} is already up to date.",
response.selected_marketplaces[0]
)
} else {
format!(
"Checked {selected_count} marketplaces; all are already up to date."
)
};
self.add_info_message(
message,
Some(format!(
"Checked: {}",
response.selected_marketplaces.join(", ")
)),
);
return;
}
if upgraded_count > 0 {
let noun = if upgraded_count == 1 {
"marketplace"
} else {
"marketplaces"
};
self.add_info_message(
format!("Upgraded {upgraded_count} {noun}."),
Some(format!(
"Updated roots: {}",
response
.upgraded_roots
.iter()
.map(|root| root.as_path().display().to_string())
.collect::<Vec<_>>()
.join(", ")
)),
);
}
if error_count > 0 {
let noun = if error_count == 1 {
"marketplace"
} else {
"marketplaces"
};
self.add_error_message(format!(
"Failed to upgrade {error_count} {noun}: {}",
response
.errors
.iter()
.map(|err| format!("{}: {}", err.marketplace_name, err.message))
.collect::<Vec<_>>()
.join("; ")
));
}
}
Err(err) => {
self.add_error_message(err);
}
}
}
pub(crate) fn handle_plugins_popup_key_event(&mut self, key_event: KeyEvent) -> bool {
if !key_hint::ctrl(KeyCode::Char('r')).is_press(key_event) {
let remove_marketplace = key_hint::ctrl(KeyCode::Char('r')).is_press(key_event);
let upgrade_marketplace = key_hint::ctrl(KeyCode::Char('u')).is_press(key_event);
if !remove_marketplace && !upgrade_marketplace {
return false;
}
@@ -591,10 +706,33 @@ impl ChatWidget {
return false;
};
self.open_marketplace_remove_confirmation(
marketplace.name.clone(),
marketplace_display_name(marketplace),
);
if remove_marketplace {
self.open_marketplace_remove_confirmation(
marketplace.name.clone(),
marketplace_display_name(marketplace),
);
return true;
}
if marketplace.path.is_none()
|| !marketplace_is_user_configured_git(&self.config, &marketplace.name)
{
return false;
}
if key_event.kind != KeyEventKind::Press {
return true;
}
let cwd = self.config.cwd.to_path_buf();
let marketplace_name = Some(marketplace.name.clone());
self.open_marketplace_upgrade_loading_popup(marketplace_name.as_deref());
self.app_event_tx
.send(AppEvent::OpenMarketplaceUpgradeLoading {
marketplace_name: marketplace_name.clone(),
});
self.app_event_tx.send(AppEvent::FetchMarketplaceUpgrade {
cwd,
marketplace_name,
});
true
}
@@ -1010,6 +1148,31 @@ impl ChatWidget {
}
}
fn marketplace_upgrade_loading_popup_params(
&self,
marketplace_name: Option<&str>,
) -> SelectionViewParams {
let loading_text = marketplace_name
.map(|name| format!("Upgrading {name} marketplace..."))
.unwrap_or_else(|| "Upgrading marketplaces...".to_string());
SelectionViewParams {
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
header: Box::new(DelayedLoadingHeader::new(
self.frame_requester.clone(),
self.config.animations,
loading_text.clone(),
/*note*/ None,
)),
items: vec![SelectionItem {
name: loading_text,
description: Some("This updates when marketplace upgrade completes.".to_string()),
is_disabled: true,
..Default::default()
}],
..Default::default()
}
}
fn plugin_detail_loading_popup_params(&self, plugin_display_name: &str) -> SelectionViewParams {
SelectionViewParams {
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
@@ -1358,10 +1521,17 @@ impl ChatWidget {
.filter(|(_, plugin, _)| plugin.installed)
.count();
let tab_id = marketplace_tab_id(marketplace);
if marketplace_is_user_configured(&self.config, &marketplace.name) {
let can_remove_marketplace =
marketplace_is_user_configured(&self.config, &marketplace.name);
let can_upgrade_marketplace = marketplace.path.is_some()
&& marketplace_is_user_configured_git(&self.config, &marketplace.name);
if can_remove_marketplace || can_upgrade_marketplace {
tab_footer_hints.push((
tab_id.clone(),
plugins_popup_hint_line(/*can_remove_marketplace*/ true),
plugins_popup_hint_line(
/*can_remove_marketplace*/ can_remove_marketplace,
/*can_upgrade_marketplace*/ can_upgrade_marketplace,
),
));
}
let header = if self.newly_installed_marketplace_tab_id.as_deref() == Some(&tab_id) {
@@ -1397,7 +1567,7 @@ impl ChatWidget {
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
header: Box::new(()),
footer_hint: Some(plugins_popup_hint_line(
/*can_remove_marketplace*/ false,
/*can_remove_marketplace*/ false, /*can_upgrade_marketplace*/ false,
)),
tab_footer_hints,
tabs,
@@ -1687,13 +1857,23 @@ impl ChatWidget {
}
}
fn plugins_popup_hint_line(can_remove_marketplace: bool) -> Line<'static> {
if can_remove_marketplace {
Line::from(
"space enable/disable · ←/→ select marketplace · enter view details · ctrl + r remove marketplace · esc close",
)
} else {
Line::from("space enable/disable · ←/→ select marketplace · enter view details · esc close")
fn plugins_popup_hint_line(
can_remove_marketplace: bool,
can_upgrade_marketplace: bool,
) -> Line<'static> {
match (can_remove_marketplace, can_upgrade_marketplace) {
(true, true) => Line::from(
"ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close",
),
(true, false) => {
Line::from("ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close")
}
(false, true) => {
Line::from("ctrl + u upgrade · space toggle · ←/→ tabs · enter details · esc close")
}
(false, false) => Line::from(
"space enable/disable · ←/→ select marketplace · enter view details · esc close",
),
}
}
@@ -1833,6 +2013,19 @@ fn marketplace_is_user_configured(config: &Config, marketplace_name: &str) -> bo
.is_some_and(|marketplaces| marketplaces.contains_key(marketplace_name))
}
fn marketplace_is_user_configured_git(config: &Config, marketplace_name: &str) -> bool {
config
.config_layer_stack
.get_user_layer()
.and_then(|user_layer| user_layer.config.get("marketplaces"))
.and_then(toml::Value::as_table)
.and_then(|marketplaces| marketplaces.get(marketplace_name))
.and_then(toml::Value::as_table)
.and_then(|marketplace| marketplace.get("source_type"))
.and_then(toml::Value::as_str)
.is_some_and(|source_type| source_type == "git")
}
fn plugin_display_name(plugin: &PluginSummary) -> String {
plugin
.interface
@@ -12,4 +12,4 @@ expression: popup
Type to search plugins
[-] Debug Plugin Available Press Enter to install or view plugin details.
space enable/disable · ←/→ select marketplace · enter view details · ctrl + r remove marketplace ·
ctrl + u upgrade · ctrl + r remove · space toggle · ←/→ tabs · enter details · esc close
+2
View File
@@ -77,6 +77,8 @@ pub(super) use codex_app_server_protocol::ItemGuardianApprovalReviewStartedNotif
pub(super) use codex_app_server_protocol::ItemStartedNotification;
pub(super) use codex_app_server_protocol::MarketplaceAddResponse;
pub(super) use codex_app_server_protocol::MarketplaceInterface;
pub(super) use codex_app_server_protocol::MarketplaceUpgradeErrorInfo;
pub(super) use codex_app_server_protocol::MarketplaceUpgradeResponse;
pub(super) use codex_app_server_protocol::McpServerStartupState;
pub(super) use codex_app_server_protocol::McpServerStatusDetail;
pub(super) use codex_app_server_protocol::McpServerStatusUpdatedNotification;
@@ -108,6 +108,61 @@ async fn plugins_popup_loading_state_snapshot() {
assert_chatwidget_snapshot!("plugins_popup_loading_state", popup);
}
#[tokio::test]
async fn marketplace_upgrade_loading_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
chat.open_marketplace_upgrade_loading_popup(Some("debug"));
let popup = render_bottom_popup(&chat, /*width*/ 100);
let upgrade_lines = popup
.lines()
.map(str::trim)
.filter(|line| line.contains("Upgrading"))
.collect::<Vec<_>>()
.join(" | ");
insta::assert_snapshot!(
upgrade_lines,
@"Upgrading debug marketplace... | Upgrading debug marketplace... This updates when marketplace upgrade completes."
);
}
#[tokio::test]
async fn marketplace_upgrade_failure_includes_backend_messages_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let cwd = chat.config.cwd.clone();
chat.on_marketplace_upgrade_loaded(
cwd.to_path_buf(),
Ok(MarketplaceUpgradeResponse {
selected_marketplaces: vec!["debug".to_string(), "tools".to_string()],
upgraded_roots: Vec::new(),
errors: vec![
MarketplaceUpgradeErrorInfo {
marketplace_name: "debug".to_string(),
message: "git ls-remote marketplace source failed with status 128: authentication failed".to_string(),
},
MarketplaceUpgradeErrorInfo {
marketplace_name: "tools".to_string(),
message: "failed to validate upgraded marketplace root: marketplace root does not contain a supported manifest".to_string(),
},
],
}),
);
let rendered = drain_insert_history(&mut rx)
.iter()
.map(|lines| lines_to_single_string(lines))
.collect::<Vec<_>>()
.join("\n");
insta::assert_snapshot!(
rendered.trim(),
@"■ Failed to upgrade 2 marketplaces: debug: git ls-remote marketplace source failed with status 128: authentication failed; tools: failed to validate upgraded marketplace root: marketplace root does not contain a supported manifest"
);
}
#[tokio::test]
async fn hooks_popup_shows_list_diagnostics() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -305,6 +360,78 @@ async fn plugins_popup_add_marketplace_tab_opens_prompt_and_submits_source() {
}
}
#[tokio::test]
async fn plugins_popup_upgrades_user_configured_git_marketplace_from_marketplace_tab() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let cwd = chat.config.cwd.to_path_buf();
let temp = tempdir().expect("tempdir");
let config_toml_path = temp.path().join("config.toml").abs();
chat.config.config_layer_stack = ConfigLayerStack::default().with_user_config(
&config_toml_path,
toml::from_str::<TomlValue>(
"[marketplaces.repo]\nsource_type = \"git\"\nsource = \"https://github.com/owner/repo.git\"\n",
)
.expect("marketplace config"),
);
render_loaded_plugins_popup(
&mut chat,
plugins_test_response(vec![
plugins_test_curated_marketplace(Vec::new()),
plugins_test_repo_marketplace(vec![plugins_test_summary(
"plugin-debug",
"debug",
Some("Debug Plugin"),
Some("Debug marketplace plugin."),
/*installed*/ false,
/*enabled*/ true,
PluginInstallPolicy::Available,
)]),
]),
);
while rx.try_recv().is_ok() {}
for _ in 0..3 {
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
}
let popup = render_bottom_popup(&chat, /*width*/ 100);
assert!(
popup.contains("Repo Marketplace.")
&& popup.contains("ctrl + u upgrade")
&& popup.contains("ctrl + r remove")
&& popup.contains("Debug Plugin"),
"expected upgradeable user-configured marketplace tab, got:\n{popup}"
);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
chat.handle_key_event(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL));
match rx.try_recv() {
Ok(AppEvent::OpenMarketplaceUpgradeLoading { marketplace_name }) => {
assert_eq!(marketplace_name, Some("repo".to_string()));
}
other => panic!("expected OpenMarketplaceUpgradeLoading event, got {other:?}"),
}
match rx.try_recv() {
Ok(AppEvent::FetchMarketplaceUpgrade {
cwd: event_cwd,
marketplace_name,
}) => {
assert_eq!(event_cwd, cwd);
assert_eq!(marketplace_name, Some("repo".to_string()));
}
other => panic!("expected FetchMarketplaceUpgrade event, got {other:?}"),
}
let no_more_events = rx.try_recv();
assert!(
no_more_events.is_err(),
"expected no duplicate marketplace upgrade events, got {no_more_events:?}"
);
}
#[tokio::test]
async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
@@ -369,6 +496,8 @@ async fn marketplace_add_success_refreshes_to_new_marketplace_tab() {
assert_chatwidget_snapshot!("plugins_popup_newly_installed_marketplace", popup);
assert!(
popup.contains("Debug Marketplace installed successfully.")
&& popup.contains("ctrl + u upgrade")
&& popup.contains("ctrl + r remove")
&& popup.contains("Debug Plugin"),
"expected marketplace add refresh to switch to the new marketplace tab, got:\n{popup}"
);
@@ -425,7 +554,8 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
let repo_tab = render_bottom_popup(&chat, /*width*/ 100);
assert!(
repo_tab.contains("Repo Marketplace.")
&& repo_tab.contains("ctrl + r remove marketplace")
&& repo_tab.contains("ctrl + u upgrade")
&& repo_tab.contains("ctrl + r remove")
&& repo_tab.contains("Debug Plugin"),
"expected removable user-configured marketplace tab, got:\n{repo_tab}"
);
@@ -493,7 +623,7 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
refreshed.contains("Browse plugins from available marketplaces.")
&& !refreshed.contains("Repo Marketplace")
&& !refreshed.contains("Debug Plugin")
&& !refreshed.contains("ctrl + r remove marketplace"),
&& !refreshed.contains("ctrl + r remove"),
"expected refreshed plugin list without removed marketplace, got:\n{refreshed}"
);
}