Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 43 additions & 9 deletions codex-rs/tui/src/app/plugin_mentions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,15 @@ mod tests {
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginListResponse;
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 pretty_assertions::assert_eq;

#[test]
fn plugin_mentions_use_plugin_list_summaries_and_gui_eligibility() {
let active = plugin_summary("active");
let active_shared = shared_plugin_summary("active-shared");
let mut disabled_by_admin = plugin_summary("disabled-by-admin");
disabled_by_admin.availability = PluginAvailability::DisabledByAdmin;
let mut disabled = plugin_summary("disabled");
Expand All @@ -108,25 +111,56 @@ mod tests {
name: "server-marketplace".to_string(),
path: 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(),
featured_plugin_ids: Vec::new(),
};

assert_eq!(
plugin_mentions_from_list_response(response),
vec![PluginCapabilitySummary {
config_name: "active@server-marketplace".to_string(),
display_name: "active".to_string(),
description: Some("server-marketplace".to_string()),
has_skills: false,
mcp_server_names: Vec::new(),
app_connector_ids: Vec::new(),
}]
vec![
PluginCapabilitySummary {
config_name: "active@server-marketplace".to_string(),
display_name: "active".to_string(),
description: Some("server-marketplace".to_string()),
has_skills: false,
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 {
PluginSummary {
id: format!("{name}@server-marketplace"),
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! App-level orchestration tests for the TUI.

mod model_catalog;
mod plugin_catalog;
mod session_summary;
mod startup;

Expand Down
49 changes: 49 additions & 0 deletions codex-rs/tui/src/app/tests/plugin_catalog.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ expression: popup
Browse plugins from available marketplaces.
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
› [ ] Alpha Sync Disabled Space to enable; Enter view details.
[-] 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.

space enable/disable · ←/→ select marketplace · enter view details · esc close
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ expression: popup
Browse plugins from available marketplaces.
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
› [-] Slack Available Press Enter to install or view plugin details.
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/tui/src/chatwidget/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ mod history_replay;
mod mcp_startup;
mod permissions;
mod plan_mode;
#[path = "tests/plugin_catalog_tests.rs"]
mod plugin_catalog;
mod popups_and_settings;
mod review_mode;
mod side;
Expand Down
72 changes: 71 additions & 1 deletion codex-rs/tui/src/chatwidget/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)));
Expand Down
Loading
Loading