use std::path::{Path, PathBuf};
use super::{render_slots, shell_quote};
pub const MARKETPLACE_NAME_SLOT: &str = "__TAPES_MARKETPLACE_NAME__";
pub const MARKETPLACE_DISPLAY_NAME_SLOT: &str = "__TAPES_MARKETPLACE_DISPLAY_NAME__";
pub const MARKETPLACE_PLUGIN_NAME_SLOT: &str = "__TAPES_PLUGIN_NAME__";
pub const MARKETPLACE_PLUGIN_SOURCE_PATH_SLOT: &str = "__TAPES_PLUGIN_SOURCE_PATH__";
pub const MARKETPLACE_MANIFEST_TEMPLATE: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/assets/codex-app/marketplace.json"
));
pub const MARKETPLACE_MANIFEST_PATH: &str = ".agents/plugins/marketplace.json";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct MarketplaceIdentity<'a> {
pub name: &'a str,
pub plugin_name: &'a str,
pub display_name: &'a str,
}
impl<'a> MarketplaceIdentity<'a> {
#[must_use]
pub const fn new(name: &'a str, plugin_name: &'a str) -> Self {
Self {
name,
plugin_name,
display_name: name,
}
}
#[must_use]
pub const fn with_display_name(mut self, display_name: &'a str) -> Self {
self.display_name = display_name;
self
}
}
#[must_use]
pub fn plugin_source_dir(plugin_name: &str) -> PathBuf {
Path::new("plugins").join(plugin_name)
}
#[must_use]
pub fn plugin_manifest_path(plugin_name: &str) -> PathBuf {
plugin_source_dir(plugin_name)
.join(".codex-plugin")
.join("plugin.json")
}
#[must_use]
pub fn hooks_manifest_path(plugin_name: &str) -> PathBuf {
plugin_source_dir(plugin_name)
.join("hooks")
.join("hooks.json")
}
#[must_use]
pub fn plugin_spec(plugin_name: &str, marketplace_name: &str) -> String {
format!("{plugin_name}@{marketplace_name}")
}
#[must_use]
pub fn render_marketplace_manifest(identity: &MarketplaceIdentity) -> String {
let source_path = format!("./{}", plugin_source_dir(identity.plugin_name).display());
render_slots(
MARKETPLACE_MANIFEST_TEMPLATE,
&[
(MARKETPLACE_NAME_SLOT, identity.name),
(MARKETPLACE_DISPLAY_NAME_SLOT, identity.display_name),
(MARKETPLACE_PLUGIN_NAME_SLOT, identity.plugin_name),
(MARKETPLACE_PLUGIN_SOURCE_PATH_SLOT, &source_path),
],
)
}
#[must_use]
pub fn plugin_disabled_in_config(config_text: &str, plugin_spec: &str) -> bool {
use toml_edit::{Document, Item};
let Ok(document) = config_text.parse::<Document>() else {
return false;
};
document
.get("plugins")
.and_then(Item::as_table_like)
.and_then(|plugins| plugins.get(plugin_spec))
.and_then(Item::as_table_like)
.and_then(|plugin| plugin.get("enabled"))
.and_then(Item::as_bool)
== Some(false)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InstallGoal {
Install,
Refresh,
Verify,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum MarketplaceOutcome {
Added,
AlreadyAdded,
Replaced {
marketplace_name: String,
},
Failed {
detail: String,
},
}
impl MarketplaceOutcome {
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::Added => "added".to_owned(),
Self::AlreadyAdded => "already added".to_owned(),
Self::Replaced { marketplace_name } => format!(
"replaced an existing '{marketplace_name}' marketplace that pointed at a \
different source"
),
Self::Failed { detail } => format!("failed: {detail}"),
}
}
#[must_use]
pub fn failed(&self) -> bool {
matches!(self, Self::Failed { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum InstallOutcome {
Installed,
AlreadyInstalled,
Refreshed,
Failed {
detail: String,
},
RemovedNotReinstalled {
detail: String,
},
Skipped,
}
impl InstallOutcome {
#[must_use]
pub fn describe(&self) -> String {
match self {
Self::Installed => "installed".to_owned(),
Self::AlreadyInstalled => "already installed".to_owned(),
Self::Refreshed => "refreshed to the new bundled version".to_owned(),
Self::Failed { detail } | Self::RemovedNotReinstalled { detail } => {
format!("failed: {detail}")
}
Self::Skipped => "skipped (marketplace registration failed)".to_owned(),
}
}
#[must_use]
pub fn needs_manual_retry(&self) -> bool {
matches!(
self,
Self::Failed { .. } | Self::RemovedNotReinstalled { .. } | Self::Skipped
)
}
#[must_use]
pub fn confirmed_delivery(&self) -> bool {
matches!(self, Self::Installed | Self::Refreshed)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManagerRun {
CliAbsent,
SkippedDisabled,
Steps {
marketplace: MarketplaceOutcome,
install: InstallOutcome,
},
}
pub const SKIPPED_DISABLED_REASON: &str = "the plugin is disabled in Codex config; enable it in the app, then install again \
(installing now would force-re-enable it)";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginManager {
codex_program: PathBuf,
marketplace_root: PathBuf,
marketplace_name: String,
plugin_name: String,
}
impl PluginManager {
#[must_use]
pub fn new(
codex_program: impl Into<PathBuf>,
marketplace_root: impl Into<PathBuf>,
marketplace_name: impl Into<String>,
plugin_name: impl Into<String>,
) -> Self {
Self {
codex_program: codex_program.into(),
marketplace_root: marketplace_root.into(),
marketplace_name: marketplace_name.into(),
plugin_name: plugin_name.into(),
}
}
#[must_use]
pub fn marketplace_root(&self) -> &Path {
&self.marketplace_root
}
#[must_use]
pub fn plugin_spec(&self) -> String {
plugin_spec(&self.plugin_name, &self.marketplace_name)
}
#[must_use]
pub fn manual_commands(&self) -> [String; 2] {
[
format!(
"codex plugin marketplace add {}",
shell_quote(&self.marketplace_root.to_string_lossy())
),
self.install_command(),
]
}
#[must_use]
pub fn install_command(&self) -> String {
format!("codex plugin add {}", shell_quote(&self.plugin_spec()))
}
#[must_use]
pub fn register(&self, goal: InstallGoal, plugin_disabled: bool) -> ManagerRun {
if plugin_disabled {
return ManagerRun::SkippedDisabled;
}
let Some(marketplace) = self.register_marketplace() else {
return ManagerRun::CliAbsent;
};
let install = if marketplace.failed() {
InstallOutcome::Skipped
} else {
self.install(goal)
};
ManagerRun::Steps {
marketplace,
install,
}
}
fn register_marketplace(&self) -> Option<MarketplaceOutcome> {
match self.run_marketplace_add() {
Invocation::Missing => None,
Invocation::Ran { success: true, .. } => Some(MarketplaceOutcome::Added),
Invocation::Ran { detail, .. } => {
let lowered = detail.to_ascii_lowercase();
if lowered.contains("different source") {
Some(self.replace_marketplace())
} else if says_already(&lowered) {
Some(MarketplaceOutcome::AlreadyAdded)
} else {
Some(MarketplaceOutcome::Failed { detail })
}
}
}
}
fn replace_marketplace(&self) -> MarketplaceOutcome {
let name = &self.marketplace_name;
match self.run(&["plugin", "marketplace", "remove", name]) {
Invocation::Missing => {
return MarketplaceOutcome::Failed {
detail: CLI_VANISHED.to_owned(),
};
}
Invocation::Ran {
success: false,
detail,
} => {
return MarketplaceOutcome::Failed {
detail: format!(
"an existing '{name}' marketplace points at a different source and \
`codex plugin marketplace remove {name}` failed: {detail}"
),
};
}
Invocation::Ran { success: true, .. } => {}
}
match self.run_marketplace_add() {
Invocation::Ran { success: true, .. } => MarketplaceOutcome::Replaced {
marketplace_name: name.clone(),
},
Invocation::Ran { detail, .. } => MarketplaceOutcome::Failed {
detail: format!(
"removed the previous '{name}' marketplace but re-adding the managed one \
failed: {detail}"
),
},
Invocation::Missing => MarketplaceOutcome::Failed {
detail: CLI_VANISHED.to_owned(),
},
}
}
fn install(&self, goal: InstallGoal) -> InstallOutcome {
match self.run_plugin_add() {
Invocation::Missing => InstallOutcome::Failed {
detail: CLI_VANISHED.to_owned(),
},
Invocation::Ran { success: true, .. } => {
if goal == InstallGoal::Refresh {
InstallOutcome::Refreshed
} else {
InstallOutcome::Installed
}
}
Invocation::Ran { detail, .. } => {
if says_already(&detail.to_ascii_lowercase()) {
match goal {
InstallGoal::Verify => InstallOutcome::AlreadyInstalled,
InstallGoal::Install | InstallGoal::Refresh => self.force_refresh(),
}
} else {
InstallOutcome::Failed { detail }
}
}
}
}
fn force_refresh(&self) -> InstallOutcome {
let spec = self.plugin_spec();
match self.run(&["plugin", "remove", &spec]) {
Invocation::Missing => {
return InstallOutcome::Failed {
detail: CLI_VANISHED.to_owned(),
};
}
Invocation::Ran {
success: false,
detail,
} => {
if !says_nothing_to_remove(&detail.to_ascii_lowercase()) {
return InstallOutcome::Failed {
detail: format!(
"the installed plugin is stale and `codex plugin remove` failed: \
{detail}"
),
};
}
}
Invocation::Ran { success: true, .. } => {}
}
match self.run_plugin_add() {
Invocation::Ran { success: true, .. } => InstallOutcome::Refreshed,
Invocation::Ran { detail, .. } => {
if says_already(&detail.to_ascii_lowercase()) {
InstallOutcome::Failed {
detail: "codex plugin add still reports an existing install after \
remove; refresh manually"
.to_owned(),
}
} else {
InstallOutcome::RemovedNotReinstalled { detail }
}
}
Invocation::Missing => InstallOutcome::RemovedNotReinstalled {
detail: CLI_VANISHED.to_owned(),
},
}
}
fn run_marketplace_add(&self) -> Invocation {
let root = self.marketplace_root.clone();
let mut command = std::process::Command::new(&self.codex_program);
command.args(["plugin", "marketplace", "add"]).arg(root);
run_invocation(command)
}
fn run_plugin_add(&self) -> Invocation {
self.run(&["plugin", "add", &self.plugin_spec()])
}
fn run(&self, args: &[&str]) -> Invocation {
let mut command = std::process::Command::new(&self.codex_program);
command.args(args);
run_invocation(command)
}
}
const CLI_VANISHED: &str = "codex CLI disappeared between commands";
enum Invocation {
Missing,
Ran { success: bool, detail: String },
}
fn says_already(lowered_detail: &str) -> bool {
["already added", "already installed", "already exists"]
.iter()
.any(|phrase| lowered_detail.contains(phrase))
}
fn says_nothing_to_remove(lowered_detail: &str) -> bool {
["not installed", "not configured", "already removed"]
.iter()
.any(|phrase| lowered_detail.contains(phrase))
}
fn run_invocation(mut command: std::process::Command) -> Invocation {
let output = match command.stdin(std::process::Stdio::null()).output() {
Ok(output) => output,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Invocation::Missing;
}
Err(error) => {
return Invocation::Ran {
success: false,
detail: error.to_string(),
};
}
};
if output.status.success() {
return Invocation::Ran {
success: true,
detail: String::new(),
};
}
Invocation::Ran {
success: false,
detail: summarize_output(&output),
}
}
fn summarize_output(output: &std::process::Output) -> String {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let mut detail = stderr
.lines()
.chain(stdout.lines())
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("; ");
if detail.chars().count() > MAX_DETAIL_CHARS {
detail = detail.chars().take(MAX_DETAIL_CHARS).collect::<String>() + "…";
}
if detail.is_empty() {
detail = format!("exited with {}", output.status);
}
detail
}
const MAX_DETAIL_CHARS: usize = 200;
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::plugin::codex_app::{HookPluginIdentity, render_plugin_manifest};
fn identity() -> MarketplaceIdentity<'static> {
MarketplaceIdentity::new("acme", "acme-codex").with_display_name("Acme")
}
fn manager(codex_program: PathBuf, root: &Path) -> PluginManager {
PluginManager::new(
codex_program,
root.join("marketplace"),
"acme",
"acme-codex",
)
}
fn missing_codex(root: &Path) -> PathBuf {
root.join("codex-not-installed")
}
#[cfg(unix)]
fn write_codex_shim(root: &Path, body: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt;
let log = root.join("invocations.log");
let path = root.join("codex");
std::fs::write(
&path,
format!("#!/bin/sh\necho \"$@\" >> \"{}\"\n{body}\n", log.display()),
)
.unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
for attempt in 1.. {
match std::process::Command::new(&path).output() {
Ok(_) => break,
Err(err)
if err.kind() == std::io::ErrorKind::ExecutableFileBusy && attempt < 100 =>
{
std::thread::sleep(std::time::Duration::from_millis(5));
}
Err(err) => panic!("warm-up exec of {} failed: {err}", path.display()),
}
}
let _ = std::fs::remove_file(&log);
path
}
#[cfg(unix)]
fn shim_log(root: &Path) -> Vec<String> {
std::fs::read_to_string(root.join("invocations.log"))
.unwrap_or_default()
.lines()
.map(str::to_owned)
.collect()
}
#[cfg(unix)]
fn add_marketplace(root: &Path) -> String {
format!(
"plugin marketplace add {}",
root.join("marketplace").display()
)
}
#[test]
fn the_rendered_marketplace_offers_the_plugin_at_the_path_the_helpers_name() {
let rendered = render_marketplace_manifest(&identity());
let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert!(!rendered.contains("__TAPES_"), "{rendered}");
assert_eq!(parsed["name"], "acme");
assert_eq!(parsed["interface"]["displayName"], "Acme");
let plugins = parsed["plugins"].as_array().unwrap();
assert_eq!(plugins.len(), 1);
assert_eq!(plugins[0]["name"], "acme-codex");
assert_eq!(plugins[0]["source"]["source"], "local");
let offered = plugins[0]["source"]["path"].as_str().unwrap();
let offered = Path::new(offered.trim_start_matches("./"));
assert_eq!(offered, plugin_source_dir("acme-codex"));
for path in [
plugin_manifest_path("acme-codex"),
hooks_manifest_path("acme-codex"),
] {
assert!(
path.starts_with(offered),
"{} escapes {offered:?}",
path.display()
);
}
}
#[test]
fn the_offered_name_the_manifest_name_and_the_spec_agree() {
let marketplace: serde_json::Value =
serde_json::from_str(&render_marketplace_manifest(&identity())).unwrap();
let manifest: serde_json::Value = serde_json::from_str(&render_plugin_manifest(
&HookPluginIdentity::new("acme-codex", "1.0.0"),
))
.unwrap();
assert_eq!(marketplace["plugins"][0]["name"], manifest["name"]);
assert_eq!(
plugin_spec("acme-codex", "acme"),
format!(
"{}@{}",
marketplace["plugins"][0]["name"].as_str().unwrap(),
marketplace["name"].as_str().unwrap()
)
);
}
#[test]
fn a_minimal_marketplace_identity_fills_every_slot() {
let rendered = render_marketplace_manifest(&MarketplaceIdentity::new("bare", "bare-codex"));
let parsed: serde_json::Value = serde_json::from_str(&rendered).unwrap();
assert!(!rendered.contains("__TAPES_"), "{rendered}");
assert_eq!(parsed["interface"]["displayName"], "bare");
}
#[test]
fn the_marketplace_template_carries_no_vendor_branding() {
let lowered = MARKETPLACE_MANIFEST_TEMPLATE.to_ascii_lowercase();
for token in ["paper", "papercompute", "tapesctl"] {
assert!(!lowered.contains(token), "the template mentions {token:?}");
}
}
#[test]
fn every_marketplace_slot_is_filled_and_none_is_unknown() {
for slot in [
MARKETPLACE_NAME_SLOT,
MARKETPLACE_DISPLAY_NAME_SLOT,
MARKETPLACE_PLUGIN_NAME_SLOT,
MARKETPLACE_PLUGIN_SOURCE_PATH_SLOT,
] {
assert!(
MARKETPLACE_MANIFEST_TEMPLATE.contains(&format!("\"{slot}\"")),
"template is missing slot {slot}"
);
}
assert_eq!(MARKETPLACE_MANIFEST_TEMPLATE.matches("__TAPES_").count(), 4);
}
#[test]
fn an_absent_cli_is_reported_rather_than_failed() {
let root = tempfile::tempdir().unwrap();
let manager = manager(missing_codex(root.path()), root.path());
for goal in [
InstallGoal::Install,
InstallGoal::Refresh,
InstallGoal::Verify,
] {
assert_eq!(manager.register(goal, false), ManagerRun::CliAbsent);
}
}
#[cfg(unix)]
#[test]
fn a_clean_run_adds_the_marketplace_then_the_plugin() {
let root = tempfile::tempdir().unwrap();
let manager = manager(write_codex_shim(root.path(), "exit 0"), root.path());
let run = manager.register(InstallGoal::Install, false);
assert_eq!(
run,
ManagerRun::Steps {
marketplace: MarketplaceOutcome::Added,
install: InstallOutcome::Installed,
}
);
assert_eq!(
shim_log(root.path()),
vec![
add_marketplace(root.path()),
"plugin add acme-codex@acme".to_owned(),
]
);
}
#[cfg(unix)]
#[test]
fn already_wording_is_trusted_only_when_the_caller_confirmed_delivery() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
"echo 'error: marketplace already exists' >&2\nexit 1",
),
root.path(),
);
let run = manager.register(InstallGoal::Verify, false);
assert_eq!(
run,
ManagerRun::Steps {
marketplace: MarketplaceOutcome::AlreadyAdded,
install: InstallOutcome::AlreadyInstalled,
}
);
}
#[cfg(unix)]
#[test]
fn unrecognised_failure_wording_skips_the_install() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(root.path(), "echo 'boom: no permission' >&2\nexit 2"),
root.path(),
);
let run = manager.register(InstallGoal::Install, false);
assert_eq!(
run,
ManagerRun::Steps {
marketplace: MarketplaceOutcome::Failed {
detail: "boom: no permission".to_owned()
},
install: InstallOutcome::Skipped,
}
);
assert_eq!(
shim_log(root.path()).len(),
1,
"the plugin add must not run"
);
}
#[cfg(unix)]
#[test]
fn a_cooperative_add_refreshes_without_the_remove_fallback() {
let root = tempfile::tempdir().unwrap();
let manager = manager(write_codex_shim(root.path(), "exit 0"), root.path());
let run = manager.register(InstallGoal::Refresh, false);
assert_eq!(
run,
ManagerRun::Steps {
marketplace: MarketplaceOutcome::Added,
install: InstallOutcome::Refreshed,
}
);
assert_eq!(
shim_log(root.path()),
vec![
add_marketplace(root.path()),
"plugin add acme-codex@acme".to_owned(),
]
);
}
#[cfg(unix)]
fn add_is_sticky_until_removed(root: &Path) -> PathBuf {
write_codex_shim(
root,
&format!(
"case \"$*\" in\n \
*'plugin remove'*) touch \"{removed}\"; exit 0 ;;\n \
*'plugin add'*) if [ -f \"{removed}\" ]; then exit 0; \
else echo 'plugin is already installed' >&2; exit 1; fi ;;\n \
*) exit 0 ;;\nesac",
removed = root.join("removed.sentinel").display()
),
)
}
#[cfg(unix)]
#[test]
fn an_uncooperative_add_falls_back_to_remove_then_re_add() {
let root = tempfile::tempdir().unwrap();
let manager = manager(add_is_sticky_until_removed(root.path()), root.path());
let run = manager.register(InstallGoal::Refresh, false);
assert_eq!(
run,
ManagerRun::Steps {
marketplace: MarketplaceOutcome::Added,
install: InstallOutcome::Refreshed,
}
);
assert_eq!(
shim_log(root.path()),
vec![
add_marketplace(root.path()),
"plugin add acme-codex@acme".to_owned(),
"plugin remove acme-codex@acme".to_owned(),
"plugin add acme-codex@acme".to_owned(),
],
"fallback order must be add, remove, re-add"
);
}
#[cfg(unix)]
#[test]
fn an_unconfirmed_existing_install_is_forced_fresh() {
let root = tempfile::tempdir().unwrap();
let manager = manager(add_is_sticky_until_removed(root.path()), root.path());
let run = manager.register(InstallGoal::Install, false);
assert!(
matches!(
run,
ManagerRun::Steps {
install: InstallOutcome::Refreshed,
..
}
),
"{run:?}"
);
}
#[cfg(unix)]
#[test]
fn a_failed_remove_keeps_the_stale_install_in_place() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
"case \"$*\" in\n \
*'plugin remove'*) echo 'remove blew up' >&2; exit 2 ;;\n \
*'plugin add'*) echo 'plugin is already installed' >&2; exit 1 ;;\n \
*) exit 0 ;;\nesac",
),
root.path(),
);
let run = manager.register(InstallGoal::Refresh, false);
let ManagerRun::Steps { install, .. } = run else {
panic!("expected steps");
};
let InstallOutcome::Failed { detail } = install else {
panic!("expected a plain failure, got {install:?}");
};
assert!(detail.contains("codex plugin remove"), "{detail}");
assert!(detail.contains("remove blew up"), "{detail}");
assert_eq!(
shim_log(root.path())
.iter()
.filter(|line| line.starts_with("plugin add"))
.count(),
1,
"no re-add may follow a failed remove"
);
}
#[cfg(unix)]
#[test]
fn a_remove_that_had_nothing_to_remove_proceeds_to_the_re_add() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
&format!(
"case \"$*\" in\n \
*'plugin remove'*) touch \"{done}\"; echo 'plugin is not installed' >&2; \
exit 1 ;;\n \
*'plugin add'*) if [ -f \"{done}\" ]; then exit 0; \
else echo 'plugin is already installed' >&2; exit 1; fi ;;\n \
*) exit 0 ;;\nesac",
done = root.path().join("removed.sentinel").display()
),
),
root.path(),
);
let run = manager.register(InstallGoal::Refresh, false);
assert!(
matches!(
run,
ManagerRun::Steps {
install: InstallOutcome::Refreshed,
..
}
),
"{run:?}"
);
}
#[cfg(unix)]
#[test]
fn a_failed_re_add_after_a_successful_remove_says_so() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
&format!(
"case \"$*\" in\n \
*'plugin remove'*) touch \"{removed}\"; exit 0 ;;\n \
*'plugin add'*) if [ -f \"{removed}\" ]; then echo 'network exploded' >&2; \
exit 2; else echo 'plugin is already installed' >&2; exit 1; fi ;;\n \
*) exit 0 ;;\nesac",
removed = root.path().join("removed.sentinel").display()
),
),
root.path(),
);
let run = manager.register(InstallGoal::Refresh, false);
let ManagerRun::Steps { install, .. } = run else {
panic!("expected steps");
};
assert_eq!(
install,
InstallOutcome::RemovedNotReinstalled {
detail: "network exploded".to_owned()
}
);
assert!(install.needs_manual_retry());
assert!(!install.confirmed_delivery());
}
#[cfg(unix)]
#[test]
fn a_same_named_marketplace_at_another_source_is_replaced() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
&format!(
"case \"$*\" in\n \
*'plugin marketplace remove'*) touch \"{removed}\"; exit 0 ;;\n \
*'plugin marketplace add'*) if [ -f \"{removed}\" ]; then exit 0; \
else echo \"Error: marketplace 'acme' is already added from a different \
source; remove it before adding this source\" >&2; exit 1; fi ;;\n \
*) exit 0 ;;\nesac",
removed = root.path().join("mkt-removed.sentinel").display()
),
),
root.path(),
);
let run = manager.register(InstallGoal::Install, false);
assert_eq!(
run,
ManagerRun::Steps {
marketplace: MarketplaceOutcome::Replaced {
marketplace_name: "acme".to_owned()
},
install: InstallOutcome::Installed,
}
);
assert_eq!(
shim_log(root.path()),
vec![
add_marketplace(root.path()),
"plugin marketplace remove acme".to_owned(),
add_marketplace(root.path()),
"plugin add acme-codex@acme".to_owned(),
],
"collision order must be add, remove, re-add, plugin add"
);
}
#[cfg(unix)]
#[test]
fn a_collision_whose_removal_fails_is_reported_with_both_reasons() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
"case \"$*\" in\n \
*'plugin marketplace remove'*) echo 'permission denied' >&2; exit 2 ;;\n \
*'plugin marketplace add'*) echo \"Error: marketplace 'acme' is already added \
from a different source; remove it before adding this source\" >&2; exit 1 ;;\n \
*) exit 0 ;;\nesac",
),
root.path(),
);
let run = manager.register(InstallGoal::Install, false);
let ManagerRun::Steps {
marketplace,
install,
} = run
else {
panic!("expected steps");
};
let MarketplaceOutcome::Failed { detail } = marketplace else {
panic!("expected a failure, got {marketplace:?}");
};
assert!(detail.contains("different source"), "{detail}");
assert!(detail.contains("permission denied"), "{detail}");
assert_eq!(install, InstallOutcome::Skipped);
}
#[cfg(unix)]
#[test]
fn a_disabled_plugin_leaves_someone_elses_marketplace_alone() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
&format!(
"case \"$*\" in\n \
*'plugin marketplace remove'*) touch \"{removed}\"; exit 0 ;;\n \
*'plugin marketplace add'*) if [ -f \"{removed}\" ]; then exit 0; \
else echo \"Error: marketplace 'acme' is already added from a different \
source; remove it before adding this source\" >&2; exit 1; fi ;;\n \
*) exit 0 ;;\nesac",
removed = root.path().join("mkt-removed.sentinel").display()
),
),
root.path(),
);
let run = manager.register(InstallGoal::Install, true);
assert_eq!(run, ManagerRun::SkippedDisabled);
assert!(
shim_log(root.path()).is_empty(),
"a disabled plugin must run no codex command at all, got {:?}",
shim_log(root.path())
);
}
#[cfg(unix)]
#[test]
fn a_printed_command_survives_shell_word_splitting() {
let awkward = std::path::PathBuf::from("/tmp/two words/it's here/$HOME`x`;rm -rf/plugin");
let manager = PluginManager::new("codex", &awkward, "acme", "acme-codex");
let words = shell_words(&manager.manual_commands()[0]);
assert_eq!(
words,
vec![
"codex".to_owned(),
"plugin".to_owned(),
"marketplace".to_owned(),
"add".to_owned(),
awkward.display().to_string(),
],
"the marketplace path did not survive as one word"
);
}
#[cfg(unix)]
fn shell_words(command: &str) -> Vec<String> {
let output = std::process::Command::new("/bin/sh")
.arg("-c")
.arg(format!("set -- {command}\nprintf '%s\\n' \"$@\""))
.output()
.unwrap();
assert!(
output.status.success(),
"the printed command is not even parseable by /bin/sh: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.unwrap()
.lines()
.map(str::to_owned)
.collect()
}
#[cfg(unix)]
#[test]
fn a_silent_failure_still_carries_a_detail() {
let root = tempfile::tempdir().unwrap();
let manager = manager(write_codex_shim(root.path(), "exit 3"), root.path());
let run = manager.register(InstallGoal::Install, false);
let ManagerRun::Steps { marketplace, .. } = run else {
panic!("expected steps");
};
let MarketplaceOutcome::Failed { detail } = marketplace else {
panic!("expected a failure, got {marketplace:?}");
};
assert!(detail.contains("exited with"), "{detail}");
}
#[cfg(unix)]
#[test]
fn a_long_failure_detail_is_bounded() {
let root = tempfile::tempdir().unwrap();
let manager = manager(
write_codex_shim(
root.path(),
"yes x | head -c 5000 | tr -d '\\n' >&2; exit 1",
),
root.path(),
);
let run = manager.register(InstallGoal::Install, false);
let ManagerRun::Steps { marketplace, .. } = run else {
panic!("expected steps");
};
let MarketplaceOutcome::Failed { detail } = marketplace else {
panic!("expected a failure, got {marketplace:?}");
};
assert_eq!(detail.chars().count(), MAX_DETAIL_CHARS + 1, "{detail}");
assert!(detail.ends_with('…'), "{detail}");
}
#[test]
fn the_manual_commands_are_the_commands_a_run_would_have_issued() {
let root = tempfile::tempdir().unwrap();
let manager = manager(missing_codex(root.path()), root.path());
assert_eq!(
manager.manual_commands(),
[
format!(
"codex plugin marketplace add {}",
manager.marketplace_root().display()
),
"codex plugin add acme-codex@acme".to_owned(),
]
);
assert_eq!(manager.install_command(), manager.manual_commands()[1]);
}
#[test]
fn only_an_explicit_false_reads_as_disabled() {
let spec = plugin_spec("acme-codex", "acme");
assert!(plugin_disabled_in_config(
&format!("[plugins.\"{spec}\"]\nenabled = false\n"),
&spec
));
assert!(!plugin_disabled_in_config(
&format!("[plugins.\"{spec}\"]\nenabled = true\n"),
&spec
));
assert!(!plugin_disabled_in_config("", &spec));
assert!(!plugin_disabled_in_config(
&format!("[plugins.\"{spec}\"]\n"),
&spec
));
assert!(!plugin_disabled_in_config(
"[plugins.\"other@acme\"]\nenabled = false\n",
&spec
));
assert!(!plugin_disabled_in_config("not = [valid\n", &spec));
}
#[test]
fn describes_cover_every_outcome_without_leaking_a_debug_shape() {
for outcome in [
MarketplaceOutcome::Added,
MarketplaceOutcome::AlreadyAdded,
MarketplaceOutcome::Replaced {
marketplace_name: "acme".to_owned(),
},
MarketplaceOutcome::Failed {
detail: "boom".to_owned(),
},
] {
let described = outcome.describe();
assert!(!described.is_empty());
assert!(!described.contains('{'), "{described}");
}
for outcome in [
InstallOutcome::Installed,
InstallOutcome::AlreadyInstalled,
InstallOutcome::Refreshed,
InstallOutcome::Failed {
detail: "boom".to_owned(),
},
InstallOutcome::RemovedNotReinstalled {
detail: "boom".to_owned(),
},
InstallOutcome::Skipped,
] {
let described = outcome.describe();
assert!(!described.is_empty());
assert!(!described.contains('{'), "{described}");
}
}
#[test]
fn only_a_proven_copy_counts_as_delivered() {
assert!(InstallOutcome::Installed.confirmed_delivery());
assert!(InstallOutcome::Refreshed.confirmed_delivery());
for outcome in [
InstallOutcome::AlreadyInstalled,
InstallOutcome::Failed {
detail: String::new(),
},
InstallOutcome::RemovedNotReinstalled {
detail: String::new(),
},
InstallOutcome::Skipped,
] {
assert!(!outcome.confirmed_delivery(), "{outcome:?}");
}
}
}