TUI Plugin Sharing 1 - add remote plugin identity (#26701)

This commit is contained in:
canvrno-oai authored and GitHub committed 2026-06-09 16:34:38 -07:00
1 parent 51b3cd51f6
commit fb8f1ea0d5
7 files changed
+573 -74

No files matched your search

@@ -1337,6 +1337,34 @@ pub(super) fn plugins_test_summary(
}
}
pub(super) fn plugins_test_remote_summary(
remote_plugin_id: &str,
name: &str,
display_name: Option<&str>,
description: Option<&str>,
installed: bool,
) -> PluginSummary {
PluginSummary {
id: remote_plugin_id.to_string(),
remote_plugin_id: Some(remote_plugin_id.to_string()),
local_version: None,
name: name.to_string(),
share_context: None,
source: PluginSource::Remote,
installed,
enabled: true,
install_policy: PluginInstallPolicy::Available,
auth_policy: PluginAuthPolicy::OnInstall,
availability: PluginAvailability::Available,
interface: Some(plugins_test_interface(
display_name,
description,
/*long_description*/ None,
)),
keywords: Vec::new(),
}
}
pub(super) fn plugins_test_curated_marketplace(
plugins: Vec<PluginSummary>,
) -> PluginMarketplaceEntry {
@@ -6,6 +6,7 @@ use codex_app_server_protocol::HookErrorInfo;
use codex_app_server_protocol::HooksListEntry;
use codex_app_server_protocol::HooksListResponse;
use codex_app_server_protocol::MarketplaceRemoveResponse;
use codex_app_server_protocol::PluginAvailability;
use codex_features::Stage;
use pretty_assertions::assert_eq;
@@ -714,6 +715,350 @@ async fn plugin_detail_popup_hides_disclosure_for_installed_plugins() {
);
}
#[tokio::test]
async fn plugins_popup_remote_row_opens_remote_detail() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let popup = render_loaded_plugins_popup(
&mut chat,
plugins_test_response(vec![PluginMarketplaceEntry {
name: "workspace-directory".to_string(),
path: None,
interface: Some(MarketplaceInterface {
display_name: Some("Workspace".to_string()),
}),
plugins: vec![plugins_test_remote_summary(
"plugins~Plugin_calendar",
"calendar",
Some("Calendar"),
Some("Workspace schedules."),
/*installed*/ false,
)],
}]),
);
let remote_row = popup
.lines()
.find(|line| line.contains("Calendar"))
.expect("expected remote plugin row");
assert!(
remote_row.contains("Available")
&& remote_row.contains("Press Enter to install or view plugin details."),
"expected remote plugin row to be viewable, got:\n{remote_row}"
);
while rx.try_recv().is_ok() {}
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match rx.try_recv() {
Ok(AppEvent::OpenPluginDetailLoading {
plugin_display_name,
}) => {
assert_eq!(plugin_display_name, "Calendar");
}
other => panic!("expected OpenPluginDetailLoading event, got {other:?}"),
}
match rx.try_recv() {
Ok(AppEvent::FetchPluginDetail { cwd: _, params }) => {
assert_eq!(params.marketplace_path, None);
assert_eq!(
params.remote_marketplace_name,
Some("workspace-directory".to_string())
);
assert_eq!(params.plugin_name, "plugins~Plugin_calendar");
}
other => panic!("expected FetchPluginDetail event, got {other:?}"),
}
}
#[tokio::test]
async fn plugin_detail_remote_install_uses_remote_location() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let summary = plugins_test_remote_summary(
"plugins~Plugin_linear",
"linear",
Some("Linear"),
Some("Issue tracking."),
/*installed*/ false,
);
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()],
}])),
);
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,
summary,
description: Some("Install shared Linear plugin.".to_string()),
skills: Vec::new(),
hooks: Vec::new(),
apps: Vec::new(),
app_templates: Vec::new(),
mcp_servers: Vec::new(),
},
}),
);
let popup = render_bottom_popup(&chat, /*width*/ 100);
assert!(
popup.contains("Install plugin") && popup.contains("Install this plugin now."),
"expected remote detail to offer install, got:\n{popup}"
);
while rx.try_recv().is_ok() {}
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match rx.try_recv() {
Ok(AppEvent::OpenPluginInstallLoading {
plugin_display_name,
}) => {
assert_eq!(plugin_display_name, "Linear");
}
other => panic!("expected OpenPluginInstallLoading event, got {other:?}"),
}
match rx.try_recv() {
Ok(AppEvent::FetchPluginInstall {
cwd: _,
location: crate::app_event::PluginLocation::Remote { marketplace_name },
plugin_name,
plugin_display_name,
}) => {
assert_eq!(marketplace_name, "workspace-shared-with-me-private");
assert_eq!(plugin_name, "plugins~Plugin_linear");
assert_eq!(plugin_display_name, "Linear");
}
other => panic!("expected remote FetchPluginInstall event, got {other:?}"),
}
}
#[tokio::test]
async fn plugin_detail_remote_uninstall_uses_remote_plugin_id() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let summary = plugins_test_remote_summary(
"plugins~Plugin_linear",
"linear",
Some("Linear"),
Some("Issue tracking."),
/*installed*/ true,
);
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()],
}])),
);
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,
summary,
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(),
},
}),
);
while rx.try_recv().is_ok() {}
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
match rx.try_recv() {
Ok(AppEvent::OpenPluginUninstallLoading {
plugin_display_name,
}) => {
assert_eq!(plugin_display_name, "Linear");
}
other => panic!("expected OpenPluginUninstallLoading event, got {other:?}"),
}
match rx.try_recv() {
Ok(AppEvent::FetchPluginUninstall {
plugin_id,
plugin_display_name,
..
}) => {
assert_eq!(plugin_id, "plugins~Plugin_linear");
assert_eq!(plugin_display_name, "Linear");
}
other => panic!("expected remote FetchPluginUninstall event, got {other:?}"),
}
}
#[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;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let summary = PluginSummary {
source: PluginSource::Remote,
..plugins_test_summary(
"linear@workspace-shared-with-me-private",
"linear",
Some("Linear"),
Some("Issue tracking."),
/*installed*/ true,
/*enabled*/ true,
PluginInstallPolicy::Available,
)
};
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()],
}])),
);
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,
summary,
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(),
},
}),
);
let popup = render_bottom_popup(&chat, /*width*/ 120);
assert!(
popup.contains("This remote plugin did not provide an uninstall identity.")
&& !popup.contains("Remove this plugin now."),
"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"
);
}
#[tokio::test]
async fn plugin_detail_admin_disabled_plugin_blocks_install() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let summary = PluginSummary {
availability: PluginAvailability::DisabledByAdmin,
..plugins_test_summary(
"plugin-admin-blocked",
"admin-blocked",
Some("Admin Blocked"),
Some("Blocked by policy."),
/*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();
chat.on_plugin_detail_loaded(
cwd.to_path_buf(),
Ok(PluginReadResponse {
plugin: plugins_test_detail(summary, Some("Blocked by policy."), &[], &[], &[], &[]),
}),
);
let popup = render_bottom_popup(&chat, /*width*/ 100);
assert!(
popup.contains("Admin Blocked · Disabled by 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}"
);
while rx.try_recv().is_ok() {}
assert!(
rx.try_recv().is_err(),
"expected no action after rendering disabled install state"
);
}
#[tokio::test]
async fn plugins_popup_admin_disabled_installed_plugin_has_no_toggle_hint() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.set_feature_enabled(Feature::Plugins, /*enabled*/ true);
let summary = PluginSummary {
availability: PluginAvailability::DisabledByAdmin,
..plugins_test_summary(
"plugin-admin-blocked",
"admin-blocked",
Some("Admin Blocked"),
Some("Blocked by policy."),
/*installed*/ true,
/*enabled*/ true,
PluginInstallPolicy::Available,
)
};
render_loaded_plugins_popup(
&mut chat,
plugins_test_response(vec![plugins_test_curated_marketplace(vec![summary])]),
);
let popup = render_bottom_popup(&chat, /*width*/ 100);
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}"
);
while rx.try_recv().is_ok() {}
let before = render_bottom_popup(&chat, /*width*/ 100);
chat.handle_key_event(KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE));
let after = render_bottom_popup(&chat, /*width*/ 100);
assert!(
rx.try_recv().is_err(),
"space should not toggle admin-disabled installed plugins"
);
assert_eq!(after, before);
}
#[tokio::test]
async fn plugin_detail_error_popup_skips_disabled_row_numbering() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;