mirror of
https://github.com/openai/codex.git
synced 2026-11-07 16:35:32 +00:00
TUI Plugin Sharing 4 - cover remote plugin catalog flows (#26704)
Remote plugin catalogs now span workspace, shared, and local sources, so their TUI behavior needs focused regression coverage across loading, navigation, actions, and refreshes. This PR: - Covers product labels and rendered loading/error states for Workspace, Shared with me, Shared with me (link), and Local tabs, including tab persistence across refresh and detail navigation. - Covers remote/local deduplication, Installed-tab remote detail routing, marketplace load-error handling, and disabled install/uninstall navigation. - Adds a full detail snapshot for local shared-plugin metadata plus focused snapshots for marketplace labels and admin-disabled status. - Verifies shared plugins remain eligible for mentions and successful uninstalls trigger a catalog refresh.
This commit is contained in:
1 parent
97dc6abb11
commit
fbd575ab4a
13 files changed
+643
-82
No files matched your search
@@ -1284,6 +1284,13 @@ pub(super) fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
.abs()
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_personal_marketplace_path() -> AbsolutePathBuf {
|
||||
dirs::home_dir()
|
||||
.expect("home directory should be available")
|
||||
.join(".agents/plugins/marketplace.json")
|
||||
.abs()
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_interface(
|
||||
display_name: Option<&str>,
|
||||
short_description: Option<&str>,
|
||||
@@ -1372,6 +1379,21 @@ pub(super) fn plugins_test_remote_summary(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_remote_marketplace(
|
||||
name: &str,
|
||||
display_name: &str,
|
||||
plugins: Vec<PluginSummary>,
|
||||
) -> PluginMarketplaceEntry {
|
||||
PluginMarketplaceEntry {
|
||||
name: name.to_string(),
|
||||
path: None,
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some(display_name.to_string()),
|
||||
}),
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_curated_marketplace(
|
||||
plugins: Vec<PluginSummary>,
|
||||
) -> PluginMarketplaceEntry {
|
||||
@@ -1408,11 +1430,23 @@ pub(super) fn plugins_test_response(
|
||||
|
||||
pub(super) fn render_loaded_plugins_popup(
|
||||
chat: &mut ChatWidget,
|
||||
response: PluginListResponse,
|
||||
mut response: PluginListResponse,
|
||||
) -> String {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
let remote_marketplaces = response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.filter(|marketplace| marketplace.path.is_none())
|
||||
.cloned()
|
||||
.collect();
|
||||
response
|
||||
.marketplaces
|
||||
.retain(|marketplace| marketplace.path.is_some());
|
||||
let response_for_refresh = response.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||
chat.add_plugins_output();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response_for_refresh));
|
||||
chat.on_plugin_remote_sections_loaded(cwd.to_path_buf(), remote_marketplaces, Vec::new());
|
||||
render_bottom_popup(chat, /*width*/ 100)
|
||||
}
|
||||
|
||||
@@ -1470,12 +1504,48 @@ pub(super) fn plugins_test_detail(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_remote_detail(
|
||||
marketplace_name: &str,
|
||||
summary: PluginSummary,
|
||||
description: Option<&str>,
|
||||
) -> PluginDetail {
|
||||
PluginDetail {
|
||||
marketplace_name: marketplace_name.to_string(),
|
||||
marketplace_path: None,
|
||||
summary,
|
||||
share_url: None,
|
||||
description: description.map(str::to_string),
|
||||
skills: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
apps: Vec::new(),
|
||||
app_templates: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn plugins_test_popup_row_position(popup: &str, needle: &str) -> usize {
|
||||
popup
|
||||
.find(needle)
|
||||
.unwrap_or_else(|| panic!("expected popup to contain {needle}: {popup}"))
|
||||
}
|
||||
|
||||
pub(super) fn select_plugins_tab_containing(
|
||||
chat: &mut ChatWidget,
|
||||
width: u16,
|
||||
visible_text: &str,
|
||||
) -> String {
|
||||
for _ in 0..8 {
|
||||
let popup = render_bottom_popup(chat, width);
|
||||
if popup.contains(visible_text) {
|
||||
return popup;
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
|
||||
let popup = render_bottom_popup(chat, width);
|
||||
panic!("expected plugins tab containing {visible_text:?}, got:\n{popup}");
|
||||
}
|
||||
|
||||
pub(super) fn type_plugins_search_query(chat: &mut ChatWidget, query: &str) {
|
||||
for ch in query.chars() {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch)));
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_uses_product_labels_for_remote_and_personal_tabs() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![
|
||||
plugins_test_remote_marketplace(
|
||||
"workspace-directory",
|
||||
"Raw Workspace Directory",
|
||||
vec![plugins_test_remote_summary(
|
||||
"plugins~Plugin_buildkite",
|
||||
"buildkite",
|
||||
Some("Buildkite"),
|
||||
Some("Workspace CI."),
|
||||
/*installed*/ false,
|
||||
)],
|
||||
),
|
||||
plugins_test_remote_marketplace(
|
||||
"workspace-shared-with-me-private",
|
||||
"Raw Shared Private",
|
||||
vec![plugins_test_remote_summary(
|
||||
"plugins~Plugin_docs",
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Shared docs."),
|
||||
/*installed*/ false,
|
||||
)],
|
||||
),
|
||||
plugins_test_remote_marketplace(
|
||||
"workspace-shared-with-me-unlisted",
|
||||
"Raw Shared Link",
|
||||
vec![plugins_test_remote_summary(
|
||||
"plugins~Plugin_link",
|
||||
"link",
|
||||
Some("Link Share"),
|
||||
Some("Shared by link."),
|
||||
/*installed*/ false,
|
||||
)],
|
||||
),
|
||||
PluginMarketplaceEntry {
|
||||
name: "codex-curated".to_string(),
|
||||
path: Some(plugins_test_personal_marketplace_path()),
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Personal".to_string()),
|
||||
}),
|
||||
plugins: vec![plugins_test_summary(
|
||||
"plugin-local-docs",
|
||||
"local-docs",
|
||||
Some("Local Docs"),
|
||||
Some("Local editable docs."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
let rows = [
|
||||
(
|
||||
"[Workspace]",
|
||||
"Workspace.",
|
||||
"Buildkite",
|
||||
"Raw Workspace Directory.",
|
||||
),
|
||||
(
|
||||
"[Shared with me]",
|
||||
"Shared with me.",
|
||||
"Docs",
|
||||
"Raw Shared Private.",
|
||||
),
|
||||
(
|
||||
"[Shared with me (link)]",
|
||||
"Shared with me (link).",
|
||||
"Link Share",
|
||||
"Raw Shared Link.",
|
||||
),
|
||||
("[Local]", "Local.", "Local Docs", "Personal."),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(selected_tab, product_label, plugin_name, raw_label)| {
|
||||
let popup = select_plugins_tab_containing(&mut chat, /*width*/ 120, selected_tab);
|
||||
assert!(
|
||||
popup.contains(product_label)
|
||||
&& popup.contains(plugin_name)
|
||||
&& !popup.contains(raw_label),
|
||||
"expected {selected_tab} to use its product label, got:\n{popup}"
|
||||
);
|
||||
popup
|
||||
.lines()
|
||||
.find(|line| line.contains(plugin_name))
|
||||
.expect("expected plugin row")
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
insta::assert_snapshot!(
|
||||
rows,
|
||||
@r###"
|
||||
› [-] Buildkite Available Press Enter to install or view plugin details.
|
||||
› [-] Docs Available Press Enter to install or view plugin details.
|
||||
› [-] Link Share Available Press Enter to install or view plugin details.
|
||||
› [-] Local Docs Available Press Enter to install or view plugin details.
|
||||
"###
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_preserves_workspace_tab_across_load_and_detail_navigation() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let workspace_marketplace = plugins_test_remote_marketplace(
|
||||
"workspace-directory",
|
||||
"Raw Workspace Directory",
|
||||
vec![plugins_test_remote_summary(
|
||||
"plugins~Plugin_buildkite",
|
||||
"buildkite",
|
||||
Some("Buildkite"),
|
||||
Some("Buildkite pipelines."),
|
||||
/*installed*/ false,
|
||||
)],
|
||||
);
|
||||
chat.add_plugins_output();
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(Vec::new()),
|
||||
])),
|
||||
);
|
||||
let loading_popup =
|
||||
select_plugins_tab_containing(&mut chat, /*width*/ 100, "Loading Workspace plugins.");
|
||||
assert!(
|
||||
loading_popup.contains("Loading Workspace plugins."),
|
||||
"expected Workspace loading tab before remote sections resolve, got:\n{loading_popup}"
|
||||
);
|
||||
|
||||
chat.on_plugin_remote_sections_loaded(
|
||||
cwd.to_path_buf(),
|
||||
vec![workspace_marketplace.clone()],
|
||||
Vec::new(),
|
||||
);
|
||||
let workspace_popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
workspace_popup.contains("Workspace.")
|
||||
&& workspace_popup.contains("Buildkite")
|
||||
&& !workspace_popup.contains("Loading Workspace plugins."),
|
||||
"expected remote section refresh to keep the Workspace tab active, got:\n{workspace_popup}"
|
||||
);
|
||||
|
||||
chat.open_plugin_detail_loading_popup("Buildkite");
|
||||
chat.open_plugins_list(
|
||||
cwd.to_path_buf(),
|
||||
plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(Vec::new()),
|
||||
workspace_marketplace,
|
||||
]),
|
||||
);
|
||||
let reopened_popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
reopened_popup.contains("Workspace.") && reopened_popup.contains("Buildkite"),
|
||||
"expected Back to plugins to preserve the Workspace tab, got:\n{reopened_popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_remote_local_dedupe_prefers_installed_remote_after_mapped_shares() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let remote_plugin_id = "plugins~Plugin_docs";
|
||||
let local_summary = PluginSummary {
|
||||
remote_plugin_id: Some(remote_plugin_id.to_string()),
|
||||
..plugins_test_summary(
|
||||
"plugin-docs",
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Local curated docs plugin."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)
|
||||
};
|
||||
let cwd = chat.config.cwd.clone();
|
||||
render_loaded_plugins_popup(
|
||||
&mut chat,
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![local_summary])]),
|
||||
);
|
||||
chat.on_plugin_remote_sections_loaded(
|
||||
cwd.to_path_buf(),
|
||||
vec![plugins_test_remote_marketplace(
|
||||
"workspace-shared-with-me-private",
|
||||
"Shared with me",
|
||||
vec![plugins_test_remote_summary(
|
||||
remote_plugin_id,
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Remote installed docs plugin."),
|
||||
/*installed*/ true,
|
||||
)],
|
||||
)],
|
||||
Vec::new(),
|
||||
);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let PluginsCacheState::Ready(response) = &chat.plugins_cache else {
|
||||
panic!("expected cached plugins after remote section refresh");
|
||||
};
|
||||
assert_eq!(
|
||||
response
|
||||
.marketplaces
|
||||
.iter()
|
||||
.map(|marketplace| marketplace.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
OPENAI_CURATED_MARKETPLACE_NAME,
|
||||
"workspace-shared-with-me-private"
|
||||
]
|
||||
);
|
||||
let all_plugins_row = popup
|
||||
.lines()
|
||||
.find(|line| line.contains("Docs"))
|
||||
.expect("expected all-plugins row");
|
||||
assert!(
|
||||
popup.contains("Installed 1 of 1 available plugins."),
|
||||
"expected header count to reflect deduped plugin rows, got:\n{popup}"
|
||||
);
|
||||
assert!(
|
||||
all_plugins_row.contains("Installed")
|
||||
&& !all_plugins_row.contains("Local curated docs plugin."),
|
||||
"expected installed remote duplicate to win when local row is not a mapped share, got:\n{all_plugins_row}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_not_installable_plugin_disables_install_action() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = plugins_test_summary(
|
||||
"plugin-internal",
|
||||
"internal",
|
||||
Some("Internal"),
|
||||
Some("Internal only."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::NotAvailable,
|
||||
);
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_curated_marketplace(vec![summary.clone()]),
|
||||
])),
|
||||
);
|
||||
chat.add_plugins_output();
|
||||
chat.on_plugin_detail_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(PluginReadResponse {
|
||||
plugin: plugins_test_detail(summary, Some("Internal only."), &[], &[], &[], &[]),
|
||||
}),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let install_row = popup
|
||||
.lines()
|
||||
.find(|line| line.contains("Install plugin"))
|
||||
.expect("expected install row");
|
||||
assert!(
|
||||
install_row.contains("This plugin is not installable from this marketplace."),
|
||||
"expected disabled not-installable row, got:\n{install_row}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
assert_eq!(
|
||||
render_bottom_popup(&chat, /*width*/ 100),
|
||||
popup,
|
||||
"expected navigation to skip the disabled install row"
|
||||
);
|
||||
}
|
||||
@@ -5,9 +5,12 @@ use codex_app_server_protocol::AppInfo;
|
||||
use codex_app_server_protocol::HookErrorInfo;
|
||||
use codex_app_server_protocol::HooksListEntry;
|
||||
use codex_app_server_protocol::HooksListResponse;
|
||||
use codex_app_server_protocol::MarketplaceLoadErrorInfo;
|
||||
use codex_app_server_protocol::MarketplaceRemoveResponse;
|
||||
use codex_app_server_protocol::PluginAvailability;
|
||||
use codex_app_server_protocol::PluginShareContext;
|
||||
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||
use codex_app_server_protocol::PluginSource;
|
||||
use codex_features::Stage;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -197,7 +200,8 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_
|
||||
PluginInstallPolicy::Available,
|
||||
)]),
|
||||
]);
|
||||
let popup = render_loaded_plugins_popup(&mut chat, response);
|
||||
render_loaded_plugins_popup(&mut chat, response);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 320);
|
||||
assert_chatwidget_snapshot!("plugins_popup_curated_marketplace", popup);
|
||||
assert!(
|
||||
popup.contains("Hidden Repo Plugin"),
|
||||
@@ -214,6 +218,27 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_keeps_loaded_marketplace_state_with_load_errors() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let mut response = plugins_test_response(vec![plugins_test_curated_marketplace(Vec::new())]);
|
||||
response
|
||||
.marketplace_load_errors
|
||||
.push(MarketplaceLoadErrorInfo {
|
||||
marketplace_path: plugins_test_absolute_path("marketplaces/broken"),
|
||||
message: "failed to load marketplace manifest".to_string(),
|
||||
});
|
||||
|
||||
let popup = render_loaded_plugins_popup(&mut chat, response);
|
||||
assert!(
|
||||
popup.contains("No marketplace plugins available")
|
||||
&& !popup.contains("Marketplace unavailable"),
|
||||
"expected /plugins to keep the loaded marketplace state, got:\n{popup}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugins_popup_truncates_long_descriptions_in_list_rows() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -272,14 +297,14 @@ async fn plugins_popup_add_marketplace_tab_opens_prompt_and_submits_source() {
|
||||
);
|
||||
|
||||
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);
|
||||
let popup = select_plugins_tab_containing(
|
||||
&mut chat,
|
||||
/*width*/ 100,
|
||||
"Add a marketplace from a Git repo or local root.",
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Add a marketplace from a Git repo or local root."),
|
||||
"expected Add Marketplace tab, got:\n{popup}"
|
||||
"expected Add marketplace tab, got:\n{popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
@@ -349,11 +374,7 @@ async fn plugins_popup_upgrades_user_configured_git_marketplace_from_marketplace
|
||||
);
|
||||
|
||||
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);
|
||||
let popup = select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace.");
|
||||
assert!(
|
||||
popup.contains("Repo Marketplace.")
|
||||
&& popup.contains("ctrl + u upgrade")
|
||||
@@ -513,10 +534,8 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
|
||||
);
|
||||
while rx.try_recv().is_ok() {}
|
||||
|
||||
for _ in 0..3 {
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
let repo_tab = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let repo_tab =
|
||||
select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace.");
|
||||
assert!(
|
||||
repo_tab.contains("Repo Marketplace.")
|
||||
&& repo_tab.contains("ctrl + u upgrade")
|
||||
@@ -839,31 +858,23 @@ async fn plugin_detail_remote_uninstall_uses_remote_plugin_id() {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(plugins_test_response(vec![PluginMarketplaceEntry {
|
||||
name: "workspace-shared-with-me-private".to_string(),
|
||||
path: None,
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Shared with me".to_string()),
|
||||
}),
|
||||
plugins: vec![summary.clone()],
|
||||
}])),
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_remote_marketplace(
|
||||
"workspace-shared-with-me-private",
|
||||
"Shared with me",
|
||||
vec![summary.clone()],
|
||||
),
|
||||
])),
|
||||
);
|
||||
chat.add_plugins_output();
|
||||
chat.on_plugin_detail_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(PluginReadResponse {
|
||||
plugin: PluginDetail {
|
||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
||||
marketplace_path: None,
|
||||
plugin: plugins_test_remote_detail(
|
||||
"workspace-shared-with-me-private",
|
||||
summary,
|
||||
share_url: None,
|
||||
description: Some("Installed shared Linear plugin.".to_string()),
|
||||
skills: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
apps: Vec::new(),
|
||||
app_templates: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
},
|
||||
Some("Installed shared Linear plugin."),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -894,7 +905,7 @@ async fn plugin_detail_remote_uninstall_uses_remote_plugin_id() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_remote_without_remote_id_disables_uninstall_action() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = PluginSummary {
|
||||
@@ -912,31 +923,23 @@ async fn plugin_detail_remote_without_remote_id_disables_uninstall_action() {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(plugins_test_response(vec![PluginMarketplaceEntry {
|
||||
name: "workspace-shared-with-me-private".to_string(),
|
||||
path: None,
|
||||
interface: Some(MarketplaceInterface {
|
||||
display_name: Some("Shared with me".to_string()),
|
||||
}),
|
||||
plugins: vec![summary.clone()],
|
||||
}])),
|
||||
Ok(plugins_test_response(vec![
|
||||
plugins_test_remote_marketplace(
|
||||
"workspace-shared-with-me-private",
|
||||
"Shared with me",
|
||||
vec![summary.clone()],
|
||||
),
|
||||
])),
|
||||
);
|
||||
chat.add_plugins_output();
|
||||
chat.on_plugin_detail_loaded(
|
||||
cwd.to_path_buf(),
|
||||
Ok(PluginReadResponse {
|
||||
plugin: PluginDetail {
|
||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
||||
marketplace_path: None,
|
||||
plugin: plugins_test_remote_detail(
|
||||
"workspace-shared-with-me-private",
|
||||
summary,
|
||||
share_url: None,
|
||||
description: Some("Installed shared Linear plugin.".to_string()),
|
||||
skills: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
apps: Vec::new(),
|
||||
app_templates: Vec::new(),
|
||||
mcp_servers: Vec::new(),
|
||||
},
|
||||
Some("Installed shared Linear plugin."),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -947,15 +950,58 @@ async fn plugin_detail_remote_without_remote_id_disables_uninstall_action() {
|
||||
"expected missing remote ID to disable uninstall, got:\n{popup}"
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"expected no action after rendering disabled uninstall state"
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
assert_eq!(
|
||||
render_bottom_popup(&chat, /*width*/ 120),
|
||||
popup,
|
||||
"expected navigation to skip the disabled uninstall row"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_admin_disabled_plugin_blocks_install() {
|
||||
async fn plugin_detail_popup_shows_local_share_context_as_read_only_snapshot() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
let summary = PluginSummary {
|
||||
share_context: Some(PluginShareContext {
|
||||
remote_plugin_id: "plugins~Plugin_docs".to_string(),
|
||||
remote_version: Some("7".to_string()),
|
||||
discoverability: Some(PluginShareDiscoverability::Private),
|
||||
share_url: Some("https://chatgpt.com/codex/plugins/share/docs".to_string()),
|
||||
creator_account_user_id: None,
|
||||
creator_name: Some("Test User".to_string()),
|
||||
share_principals: None,
|
||||
}),
|
||||
..plugins_test_summary(
|
||||
"plugin-docs",
|
||||
"docs",
|
||||
Some("Docs"),
|
||||
Some("Workspace docs."),
|
||||
/*installed*/ false,
|
||||
/*enabled*/ true,
|
||||
PluginInstallPolicy::Available,
|
||||
)
|
||||
};
|
||||
let response = plugins_test_response(vec![plugins_test_curated_marketplace(vec![
|
||||
summary.clone(),
|
||||
])]);
|
||||
let cwd = chat.config.cwd.clone();
|
||||
chat.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||
chat.add_plugins_output();
|
||||
let mut detail = plugins_test_detail(summary, Some("Workspace docs."), &[], &[], &[], &[]);
|
||||
detail.marketplace_path = Some(plugins_test_personal_marketplace_path());
|
||||
chat.on_plugin_detail_loaded(cwd.to_path_buf(), Ok(PluginReadResponse { plugin: detail }));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 120);
|
||||
assert_chatwidget_snapshot!(
|
||||
"plugin_detail_popup_local_share_read_only",
|
||||
strip_osc8_for_snapshot(&popup)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn plugin_detail_popup_shows_admin_disabled_status_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||
|
||||
@@ -984,10 +1030,17 @@ async fn plugin_detail_admin_disabled_plugin_blocks_install() {
|
||||
}),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 120);
|
||||
let status_row = popup
|
||||
.lines()
|
||||
.find(|line| line.contains("Disabled by admin"))
|
||||
.expect("expected admin-disabled status row");
|
||||
insta::assert_snapshot!(
|
||||
status_row,
|
||||
@" Admin Blocked · Disabled by admin · ChatGPT Marketplace"
|
||||
);
|
||||
assert!(
|
||||
popup.contains("Admin Blocked · Disabled by admin")
|
||||
&& popup.contains("This plugin is disabled by your workspace admin.")
|
||||
popup.contains("This plugin is disabled by your workspace admin.")
|
||||
&& !popup.contains("Install this plugin now."),
|
||||
"expected admin-disabled detail to block install, got:\n{popup}"
|
||||
);
|
||||
@@ -1021,18 +1074,18 @@ async fn plugins_popup_admin_disabled_installed_plugin_has_no_toggle_hint() {
|
||||
plugins_test_response(vec![plugins_test_curated_marketplace(vec![summary])]),
|
||||
);
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 120);
|
||||
assert!(
|
||||
popup.contains("Disabled by admin")
|
||||
&& popup.contains("Press Enter to view plugin details.")
|
||||
&& !popup.contains("Space to disable"),
|
||||
"expected admin-disabled installed plugin to omit toggle hint, got:\n{popup}"
|
||||
"expected admin-disabled installed row to omit toggle hint, got:\n{popup}"
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
let before = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let before = render_bottom_popup(&chat, /*width*/ 120);
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
|
||||
let after = render_bottom_popup(&chat, /*width*/ 100);
|
||||
let after = render_bottom_popup(&chat, /*width*/ 120);
|
||||
assert!(
|
||||
rx.try_recv().is_err(),
|
||||
"space should not toggle admin-disabled installed plugins"
|
||||
@@ -1232,6 +1285,14 @@ async fn plugins_popup_installed_remote_row_keeps_remote_detail_when_local_share
|
||||
"expected installed remote duplicate to win over local mapped share, got:\n{popup}"
|
||||
);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
let installed_popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
assert!(
|
||||
installed_popup.contains("Showing 1 installed plugins.")
|
||||
&& installed_popup.contains("Docs"),
|
||||
"expected installed remote duplicate in the Installed tab, got:\n{installed_popup}"
|
||||
);
|
||||
|
||||
while rx.try_recv().is_ok() {}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
@@ -1765,7 +1826,14 @@ async fn plugins_popup_refresh_preserves_duplicate_marketplace_tab_by_path() {
|
||||
chat.on_plugins_loaded(cwd.clone(), Ok(response.clone()));
|
||||
chat.add_plugins_output();
|
||||
|
||||
for _ in 0..4 {
|
||||
for _ in 0..10 {
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
||||
if popup.contains("Duplicate Marketplace (2/2).")
|
||||
&& popup.contains("Repo Plugin")
|
||||
&& !popup.contains("Home Plugin")
|
||||
{
|
||||
break;
|
||||
}
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user