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
@@ -90,12 +90,15 @@ mod tests {
|
|||||||
use codex_app_server_protocol::PluginInstallPolicy;
|
use codex_app_server_protocol::PluginInstallPolicy;
|
||||||
use codex_app_server_protocol::PluginListResponse;
|
use codex_app_server_protocol::PluginListResponse;
|
||||||
use codex_app_server_protocol::PluginMarketplaceEntry;
|
use codex_app_server_protocol::PluginMarketplaceEntry;
|
||||||
|
use codex_app_server_protocol::PluginShareContext;
|
||||||
|
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||||
use codex_app_server_protocol::PluginSource;
|
use codex_app_server_protocol::PluginSource;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn plugin_mentions_use_plugin_list_summaries_and_gui_eligibility() {
|
fn plugin_mentions_use_plugin_list_summaries_and_gui_eligibility() {
|
||||||
let active = plugin_summary("active");
|
let active = plugin_summary("active");
|
||||||
|
let active_shared = shared_plugin_summary("active-shared");
|
||||||
let mut disabled_by_admin = plugin_summary("disabled-by-admin");
|
let mut disabled_by_admin = plugin_summary("disabled-by-admin");
|
||||||
disabled_by_admin.availability = PluginAvailability::DisabledByAdmin;
|
disabled_by_admin.availability = PluginAvailability::DisabledByAdmin;
|
||||||
let mut disabled = plugin_summary("disabled");
|
let mut disabled = plugin_summary("disabled");
|
||||||
@@ -108,7 +111,13 @@ mod tests {
|
|||||||
name: "server-marketplace".to_string(),
|
name: "server-marketplace".to_string(),
|
||||||
path: None,
|
path: None,
|
||||||
interface: None,
|
interface: None,
|
||||||
plugins: vec![active, disabled_by_admin, disabled, uninstalled],
|
plugins: vec![
|
||||||
|
active,
|
||||||
|
active_shared,
|
||||||
|
disabled_by_admin,
|
||||||
|
disabled,
|
||||||
|
uninstalled,
|
||||||
|
],
|
||||||
}],
|
}],
|
||||||
marketplace_load_errors: Vec::new(),
|
marketplace_load_errors: Vec::new(),
|
||||||
featured_plugin_ids: Vec::new(),
|
featured_plugin_ids: Vec::new(),
|
||||||
@@ -116,17 +125,42 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
plugin_mentions_from_list_response(response),
|
plugin_mentions_from_list_response(response),
|
||||||
vec![PluginCapabilitySummary {
|
vec![
|
||||||
config_name: "active@server-marketplace".to_string(),
|
PluginCapabilitySummary {
|
||||||
display_name: "active".to_string(),
|
config_name: "active@server-marketplace".to_string(),
|
||||||
description: Some("server-marketplace".to_string()),
|
display_name: "active".to_string(),
|
||||||
has_skills: false,
|
description: Some("server-marketplace".to_string()),
|
||||||
mcp_server_names: Vec::new(),
|
has_skills: false,
|
||||||
app_connector_ids: Vec::new(),
|
mcp_server_names: Vec::new(),
|
||||||
}]
|
app_connector_ids: Vec::new(),
|
||||||
|
},
|
||||||
|
PluginCapabilitySummary {
|
||||||
|
config_name: "active-shared@server-marketplace".to_string(),
|
||||||
|
display_name: "active-shared".to_string(),
|
||||||
|
description: Some("server-marketplace".to_string()),
|
||||||
|
has_skills: false,
|
||||||
|
mcp_server_names: Vec::new(),
|
||||||
|
app_connector_ids: Vec::new(),
|
||||||
|
}
|
||||||
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shared_plugin_summary(name: &str) -> PluginSummary {
|
||||||
|
PluginSummary {
|
||||||
|
share_context: Some(PluginShareContext {
|
||||||
|
remote_plugin_id: format!("plugins~{name}"),
|
||||||
|
remote_version: Some("7".to_string()),
|
||||||
|
discoverability: Some(PluginShareDiscoverability::Private),
|
||||||
|
share_url: Some(format!("https://chatgpt.com/codex/plugins/share/{name}")),
|
||||||
|
creator_account_user_id: None,
|
||||||
|
creator_name: Some("Test User".to_string()),
|
||||||
|
share_principals: None,
|
||||||
|
}),
|
||||||
|
..plugin_summary(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn plugin_summary(name: &str) -> PluginSummary {
|
fn plugin_summary(name: &str) -> PluginSummary {
|
||||||
PluginSummary {
|
PluginSummary {
|
||||||
id: format!("{name}@server-marketplace"),
|
id: format!("{name}@server-marketplace"),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! App-level orchestration tests for the TUI.
|
//! App-level orchestration tests for the TUI.
|
||||||
|
|
||||||
mod model_catalog;
|
mod model_catalog;
|
||||||
|
mod plugin_catalog;
|
||||||
mod session_summary;
|
mod session_summary;
|
||||||
mod startup;
|
mod startup;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use codex_app_server_protocol::PluginUninstallResponse;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn successful_plugin_uninstall_dispatches_plugin_list_refresh() -> Result<()> {
|
||||||
|
let (mut app, mut app_event_rx, _op_rx) = make_test_app_with_channels().await;
|
||||||
|
let cwd = app.chat_widget.config_ref().cwd.to_path_buf();
|
||||||
|
while app_event_rx.try_recv().is_ok() {}
|
||||||
|
|
||||||
|
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||||
|
let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(
|
||||||
|
app.chat_widget.config_ref(),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
let control = Box::pin(app.handle_event(
|
||||||
|
&mut tui,
|
||||||
|
&mut app_server,
|
||||||
|
AppEvent::PluginUninstallLoaded {
|
||||||
|
cwd: cwd.clone(),
|
||||||
|
plugin_id: "plugin-docs".to_string(),
|
||||||
|
plugin_display_name: "Docs".to_string(),
|
||||||
|
result: Ok(PluginUninstallResponse {}),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
assert!(matches!(control, AppRunControl::Continue));
|
||||||
|
|
||||||
|
let refresh_result = tokio::time::timeout(Duration::from_secs(5), async {
|
||||||
|
loop {
|
||||||
|
match app_event_rx.recv().await {
|
||||||
|
Some(AppEvent::PluginsLoaded {
|
||||||
|
cwd: event_cwd,
|
||||||
|
result,
|
||||||
|
}) if event_cwd == cwd => break result,
|
||||||
|
Some(_) => {}
|
||||||
|
None => panic!("app event channel closed before plugin refresh completed"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("dispatcher should initiate a plugin list refresh");
|
||||||
|
refresh_result.expect("plugin list refresh should succeed");
|
||||||
|
|
||||||
|
app_server.shutdown().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/chatwidget/tests/popups_and_settings.rs
|
||||||
|
expression: strip_osc8_for_snapshot(&popup)
|
||||||
|
---
|
||||||
|
Plugins
|
||||||
|
Docs · Can be installed · Local
|
||||||
|
Data shared with this app is subject to the app's terms of service and privacy policy. Learn more.
|
||||||
|
Workspace docs.
|
||||||
|
|
||||||
|
› 1. Back to plugins Return to the plugin list.
|
||||||
|
2. Install plugin Install this plugin now.
|
||||||
|
Source Local
|
||||||
|
Auth Auth on install
|
||||||
|
Version remote 7
|
||||||
|
Sharing Private · creator Test User · https://chatgpt.com/codex/plugins/share/docs
|
||||||
|
Skills No plugin skills.
|
||||||
|
Hooks No plugin hooks.
|
||||||
|
|
||||||
|
Press esc to close.
|
||||||
+2
-2
@@ -6,12 +6,12 @@ expression: popup
|
|||||||
Browse plugins from available marketplaces.
|
Browse plugins from available marketplaces.
|
||||||
Installed 1 of 4 available plugins.
|
Installed 1 of 4 available plugins.
|
||||||
|
|
||||||
[All Plugins] Installed (1) OpenAI Curated Repo Marketplace Add Marketplace
|
[All Plugins] Installed (1) OpenAI Curated Workspace Shared with me Repo Marketplace Add Marketplace
|
||||||
|
|
||||||
Type to search plugins
|
Type to search plugins
|
||||||
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
|
||||||
[-] Bravo Search Available · OpenAI Curated · Search docs and tickets.
|
[-] Bravo Search Available · OpenAI Curated · Search docs and tickets.
|
||||||
[-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugi…
|
[-] Hidden Repo Plugin Available · Repo Marketplace · Should not be shown in /plugins.
|
||||||
[-] Starter Available by default · OpenAI Curated · Included by default.
|
[-] Starter Available by default · OpenAI Curated · Included by default.
|
||||||
|
|
||||||
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
space enable/disable · ←/→ select marketplace · enter view details · esc close
|
||||||
+1
-1
@@ -7,7 +7,7 @@ expression: popup
|
|||||||
Browse plugins from available marketplaces.
|
Browse plugins from available marketplaces.
|
||||||
Installed 0 of 3 available plugins.
|
Installed 0 of 3 available plugins.
|
||||||
|
|
||||||
[All Plugins] Installed (0) OpenAI Curated Add Marketplace
|
[All Plugins] Installed (0) OpenAI Curated Workspace Shared with me Add Marketplace
|
||||||
|
|
||||||
sla
|
sla
|
||||||
› [-] Slack Available Press Enter to install or view plugin details.
|
› [-] Slack Available Press Enter to install or view plugin details.
|
||||||
|
|||||||
@@ -244,6 +244,8 @@ mod history_replay;
|
|||||||
mod mcp_startup;
|
mod mcp_startup;
|
||||||
mod permissions;
|
mod permissions;
|
||||||
mod plan_mode;
|
mod plan_mode;
|
||||||
|
#[path = "tests/plugin_catalog_tests.rs"]
|
||||||
|
mod plugin_catalog;
|
||||||
mod popups_and_settings;
|
mod popups_and_settings;
|
||||||
mod review_mode;
|
mod review_mode;
|
||||||
mod side;
|
mod side;
|
||||||
|
|||||||
@@ -1284,6 +1284,13 @@ pub(super) fn plugins_test_absolute_path(path: &str) -> AbsolutePathBuf {
|
|||||||
.abs()
|
.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(
|
pub(super) fn plugins_test_interface(
|
||||||
display_name: Option<&str>,
|
display_name: Option<&str>,
|
||||||
short_description: 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(
|
pub(super) fn plugins_test_curated_marketplace(
|
||||||
plugins: Vec<PluginSummary>,
|
plugins: Vec<PluginSummary>,
|
||||||
) -> PluginMarketplaceEntry {
|
) -> PluginMarketplaceEntry {
|
||||||
@@ -1408,11 +1430,23 @@ pub(super) fn plugins_test_response(
|
|||||||
|
|
||||||
pub(super) fn render_loaded_plugins_popup(
|
pub(super) fn render_loaded_plugins_popup(
|
||||||
chat: &mut ChatWidget,
|
chat: &mut ChatWidget,
|
||||||
response: PluginListResponse,
|
mut response: PluginListResponse,
|
||||||
) -> String {
|
) -> String {
|
||||||
let cwd = chat.config.cwd.clone();
|
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.on_plugins_loaded(cwd.to_path_buf(), Ok(response));
|
||||||
chat.add_plugins_output();
|
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)
|
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 {
|
pub(super) fn plugins_test_popup_row_position(popup: &str, needle: &str) -> usize {
|
||||||
popup
|
popup
|
||||||
.find(needle)
|
.find(needle)
|
||||||
.unwrap_or_else(|| panic!("expected popup to contain {needle}: {popup}"))
|
.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) {
|
pub(super) fn type_plugins_search_query(chat: &mut ChatWidget, query: &str) {
|
||||||
for ch in query.chars() {
|
for ch in query.chars() {
|
||||||
chat.handle_key_event(KeyEvent::from(KeyCode::Char(ch)));
|
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::HookErrorInfo;
|
||||||
use codex_app_server_protocol::HooksListEntry;
|
use codex_app_server_protocol::HooksListEntry;
|
||||||
use codex_app_server_protocol::HooksListResponse;
|
use codex_app_server_protocol::HooksListResponse;
|
||||||
|
use codex_app_server_protocol::MarketplaceLoadErrorInfo;
|
||||||
use codex_app_server_protocol::MarketplaceRemoveResponse;
|
use codex_app_server_protocol::MarketplaceRemoveResponse;
|
||||||
use codex_app_server_protocol::PluginAvailability;
|
use codex_app_server_protocol::PluginAvailability;
|
||||||
use codex_app_server_protocol::PluginShareContext;
|
use codex_app_server_protocol::PluginShareContext;
|
||||||
|
use codex_app_server_protocol::PluginShareDiscoverability;
|
||||||
|
use codex_app_server_protocol::PluginSource;
|
||||||
use codex_features::Stage;
|
use codex_features::Stage;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
@@ -197,7 +200,8 @@ async fn plugins_popup_snapshot_shows_all_marketplaces_and_sorts_installed_then_
|
|||||||
PluginInstallPolicy::Available,
|
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_chatwidget_snapshot!("plugins_popup_curated_marketplace", popup);
|
||||||
assert!(
|
assert!(
|
||||||
popup.contains("Hidden Repo Plugin"),
|
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]
|
#[tokio::test]
|
||||||
async fn plugins_popup_truncates_long_descriptions_in_list_rows() {
|
async fn plugins_popup_truncates_long_descriptions_in_list_rows() {
|
||||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
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() {}
|
while rx.try_recv().is_ok() {}
|
||||||
for _ in 0..3 {
|
let popup = select_plugins_tab_containing(
|
||||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
&mut chat,
|
||||||
}
|
/*width*/ 100,
|
||||||
|
"Add a marketplace from a Git repo or local root.",
|
||||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
popup.contains("Add a marketplace from a Git repo or local root."),
|
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));
|
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() {}
|
while rx.try_recv().is_ok() {}
|
||||||
for _ in 0..3 {
|
let popup = select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace.");
|
||||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
|
||||||
}
|
|
||||||
|
|
||||||
let popup = render_bottom_popup(&chat, /*width*/ 100);
|
|
||||||
assert!(
|
assert!(
|
||||||
popup.contains("Repo Marketplace.")
|
popup.contains("Repo Marketplace.")
|
||||||
&& popup.contains("ctrl + u upgrade")
|
&& popup.contains("ctrl + u upgrade")
|
||||||
@@ -513,10 +534,8 @@ async fn plugins_popup_removes_user_configured_marketplace_flow() {
|
|||||||
);
|
);
|
||||||
while rx.try_recv().is_ok() {}
|
while rx.try_recv().is_ok() {}
|
||||||
|
|
||||||
for _ in 0..3 {
|
let repo_tab =
|
||||||
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
select_plugins_tab_containing(&mut chat, /*width*/ 100, "Repo Marketplace.");
|
||||||
}
|
|
||||||
let repo_tab = render_bottom_popup(&chat, /*width*/ 100);
|
|
||||||
assert!(
|
assert!(
|
||||||
repo_tab.contains("Repo Marketplace.")
|
repo_tab.contains("Repo Marketplace.")
|
||||||
&& repo_tab.contains("ctrl + u upgrade")
|
&& 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();
|
let cwd = chat.config.cwd.clone();
|
||||||
chat.on_plugins_loaded(
|
chat.on_plugins_loaded(
|
||||||
cwd.to_path_buf(),
|
cwd.to_path_buf(),
|
||||||
Ok(plugins_test_response(vec![PluginMarketplaceEntry {
|
Ok(plugins_test_response(vec![
|
||||||
name: "workspace-shared-with-me-private".to_string(),
|
plugins_test_remote_marketplace(
|
||||||
path: None,
|
"workspace-shared-with-me-private",
|
||||||
interface: Some(MarketplaceInterface {
|
"Shared with me",
|
||||||
display_name: Some("Shared with me".to_string()),
|
vec![summary.clone()],
|
||||||
}),
|
),
|
||||||
plugins: vec![summary.clone()],
|
])),
|
||||||
}])),
|
|
||||||
);
|
);
|
||||||
chat.add_plugins_output();
|
chat.add_plugins_output();
|
||||||
chat.on_plugin_detail_loaded(
|
chat.on_plugin_detail_loaded(
|
||||||
cwd.to_path_buf(),
|
cwd.to_path_buf(),
|
||||||
Ok(PluginReadResponse {
|
Ok(PluginReadResponse {
|
||||||
plugin: PluginDetail {
|
plugin: plugins_test_remote_detail(
|
||||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
"workspace-shared-with-me-private",
|
||||||
marketplace_path: None,
|
|
||||||
summary,
|
summary,
|
||||||
share_url: None,
|
Some("Installed shared Linear plugin."),
|
||||||
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(),
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -894,7 +905,7 @@ async fn plugin_detail_remote_uninstall_uses_remote_plugin_id() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn plugin_detail_remote_without_remote_id_disables_uninstall_action() {
|
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);
|
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
||||||
|
|
||||||
let summary = PluginSummary {
|
let summary = PluginSummary {
|
||||||
@@ -912,31 +923,23 @@ async fn plugin_detail_remote_without_remote_id_disables_uninstall_action() {
|
|||||||
let cwd = chat.config.cwd.clone();
|
let cwd = chat.config.cwd.clone();
|
||||||
chat.on_plugins_loaded(
|
chat.on_plugins_loaded(
|
||||||
cwd.to_path_buf(),
|
cwd.to_path_buf(),
|
||||||
Ok(plugins_test_response(vec![PluginMarketplaceEntry {
|
Ok(plugins_test_response(vec![
|
||||||
name: "workspace-shared-with-me-private".to_string(),
|
plugins_test_remote_marketplace(
|
||||||
path: None,
|
"workspace-shared-with-me-private",
|
||||||
interface: Some(MarketplaceInterface {
|
"Shared with me",
|
||||||
display_name: Some("Shared with me".to_string()),
|
vec![summary.clone()],
|
||||||
}),
|
),
|
||||||
plugins: vec![summary.clone()],
|
])),
|
||||||
}])),
|
|
||||||
);
|
);
|
||||||
chat.add_plugins_output();
|
chat.add_plugins_output();
|
||||||
chat.on_plugin_detail_loaded(
|
chat.on_plugin_detail_loaded(
|
||||||
cwd.to_path_buf(),
|
cwd.to_path_buf(),
|
||||||
Ok(PluginReadResponse {
|
Ok(PluginReadResponse {
|
||||||
plugin: PluginDetail {
|
plugin: plugins_test_remote_detail(
|
||||||
marketplace_name: "workspace-shared-with-me-private".to_string(),
|
"workspace-shared-with-me-private",
|
||||||
marketplace_path: None,
|
|
||||||
summary,
|
summary,
|
||||||
share_url: None,
|
Some("Installed shared Linear plugin."),
|
||||||
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(),
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -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}"
|
"expected missing remote ID to disable uninstall, got:\n{popup}"
|
||||||
);
|
);
|
||||||
|
|
||||||
while rx.try_recv().is_ok() {}
|
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||||
assert!(
|
assert_eq!(
|
||||||
rx.try_recv().is_err(),
|
render_bottom_popup(&chat, /*width*/ 120),
|
||||||
"expected no action after rendering disabled uninstall state"
|
popup,
|
||||||
|
"expected navigation to skip the disabled uninstall row"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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;
|
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||||
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
|
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!(
|
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."),
|
&& !popup.contains("Install this plugin now."),
|
||||||
"expected admin-disabled detail to block install, got:\n{popup}"
|
"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])]),
|
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!(
|
assert!(
|
||||||
popup.contains("Disabled by admin")
|
popup.contains("Disabled by admin")
|
||||||
&& popup.contains("Press Enter to view plugin details.")
|
&& popup.contains("Press Enter to view plugin details.")
|
||||||
&& !popup.contains("Space to disable"),
|
&& !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() {}
|
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));
|
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!(
|
assert!(
|
||||||
rx.try_recv().is_err(),
|
rx.try_recv().is_err(),
|
||||||
"space should not toggle admin-disabled installed plugins"
|
"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}"
|
"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() {}
|
while rx.try_recv().is_ok() {}
|
||||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
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.on_plugins_loaded(cwd.clone(), Ok(response.clone()));
|
||||||
chat.add_plugins_output();
|
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));
|
chat.handle_key_event(KeyEvent::from(KeyCode::Right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ where
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_screen_size_and_cursor_position(
|
pub(crate) fn with_screen_size_and_cursor_position(
|
||||||
backend: B,
|
backend: B,
|
||||||
screen_size: Size,
|
screen_size: Size,
|
||||||
cursor_pos: Position,
|
cursor_pos: Position,
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ mod frame_requester;
|
|||||||
mod job_control;
|
mod job_control;
|
||||||
mod keyboard_modes;
|
mod keyboard_modes;
|
||||||
mod terminal_stderr;
|
mod terminal_stderr;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod test_support;
|
||||||
|
|
||||||
/// Target frame interval for UI redraw scheduling.
|
/// Target frame interval for UI redraw scheduling.
|
||||||
pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME_INTERVAL;
|
pub(crate) const TARGET_FRAME_INTERVAL: Duration = frame_rate_limiter::MIN_FRAME_INTERVAL;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
use std::io;
|
||||||
|
use std::io::stdout;
|
||||||
|
|
||||||
|
use ratatui::backend::CrosstermBackend;
|
||||||
|
use ratatui::layout::Position;
|
||||||
|
use ratatui::layout::Size;
|
||||||
|
|
||||||
|
use super::Tui;
|
||||||
|
use super::terminal_stderr::TerminalStderrGuard;
|
||||||
|
use crate::custom_terminal::Terminal;
|
||||||
|
|
||||||
|
pub(crate) fn make_test_tui() -> io::Result<Tui> {
|
||||||
|
let backend = CrosstermBackend::new(stdout());
|
||||||
|
let terminal = Terminal::with_screen_size_and_cursor_position(
|
||||||
|
backend,
|
||||||
|
Size {
|
||||||
|
width: 80,
|
||||||
|
height: 24,
|
||||||
|
},
|
||||||
|
Position { x: 0, y: 0 },
|
||||||
|
);
|
||||||
|
let stderr_guard = TerminalStderrGuard::install()?;
|
||||||
|
Ok(Tui::new(
|
||||||
|
terminal,
|
||||||
|
/*enhanced_keys_supported*/ false,
|
||||||
|
stderr_guard,
|
||||||
|
))
|
||||||
|
}
|
||||||
Reference in new issue
Block a user