#[test]
#[cfg(unix)]
fn unknown_domain_command_shows_plugin_hint_contract() {
let home = make_temp_dir("osp-cli-no-plugin-home");
let empty_plugins = make_temp_dir("osp-cli-empty-plugins");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.envs(crate::test_env::isolated_env(&home))
.env("PATH", "/usr/bin:/bin")
.env("OSP_PLUGIN_PATH", &empty_plugins)
.env("OSP_BUNDLED_PLUGIN_DIR", &empty_plugins)
.args(["ldap", "user", "oistes"]);
cmd.assert()
.failure()
.stderr(predicate::str::contains("no plugin provides command: ldap"))
.stderr(predicate::str::contains(
"Hint: run osp plugins list and set --plugin-dir or OSP_PLUGIN_PATH",
));
}
#[test]
#[cfg(unix)]
fn errors_remain_visible_at_double_quiet_contract() {
let home = make_temp_dir("osp-cli-no-plugin-home-quiet");
let empty_plugins = make_temp_dir("osp-cli-empty-plugins-quiet");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.envs(crate::test_env::isolated_env(&home))
.env("PATH", "/usr/bin:/bin")
.env("OSP_PLUGIN_PATH", &empty_plugins)
.env("OSP_BUNDLED_PLUGIN_DIR", &empty_plugins)
.args(["-qq", "ldap", "user", "oistes"]);
cmd.assert()
.failure()
.stderr(predicate::str::contains("no plugin provides command: ldap"));
}
#[cfg(unix)]
#[test]
fn external_plugin_help_is_passed_through_contract() {
let dir = make_temp_dir("osp-cli-plugin-help");
let _plugin_path = write_hello_plugin(&dir);
let home = make_temp_dir("osp-cli-plugin-help-home");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["hello", "--help"]);
let help_flag = cmd.assert().success().get_output().clone();
let help_flag_stdout =
String::from_utf8(help_flag.stdout).expect("help stdout should be utf-8");
assert_snapshot_text!("external_plugin_help_stdout", help_flag_stdout.clone());
let mut cmd_help_subcommand = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd_help_subcommand
.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["hello", "help"]);
let help_subcommand = cmd_help_subcommand.assert().success().get_output().clone();
let help_subcommand_stdout =
String::from_utf8(help_subcommand.stdout).expect("help stdout should be utf-8");
assert_eq!(help_subcommand_stdout, help_flag_stdout);
}
#[cfg(unix)]
#[test]
fn external_plugin_help_keeps_raw_stderr_contract() {
let dir = make_temp_dir("osp-cli-plugin-help-stderr");
let _plugin_path = write_help_stderr_plugin(&dir);
let home = make_temp_dir("osp-cli-plugin-help-stderr-home");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["hello", "--help"]);
let output = cmd.assert().success().get_output().clone();
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf-8");
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf-8");
assert_snapshot_text!("external_plugin_help_stderr_stdout", stdout);
assert_snapshot_text!("external_plugin_help_stderr_stderr", stderr);
}
#[cfg(unix)]
#[test]
fn ignores_non_plugin_extension_files_contract() {
use std::os::unix::fs::PermissionsExt;
let dir = make_temp_dir("osp-cli-ignore-script");
let script_path = dir.join("osp-ignore.sh");
std::fs::write(&script_path, "#!/bin/sh\necho should-not-run\n")
.expect("script should be written");
let mut perms = std::fs::metadata(&script_path)
.expect("metadata should be readable")
.permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&script_path, perms).expect("script should be executable");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.env("PATH", "/usr/bin:/bin")
.env("OSP_PLUGIN_PATH", &dir)
.args(["plugins", "list"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("osp-ignore.sh").not());
}
#[cfg(unix)]
#[test]
fn bundled_plugin_requires_manifest_contract() {
let dir = make_temp_dir("osp-cli-plugin-bundled-missing-manifest");
let _plugin_path = write_hello_plugin(&dir);
let home = make_temp_dir("osp-cli-plugin-home");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.envs(crate::test_env::isolated_env(&home))
.env("OSP_BUNDLED_PLUGIN_DIR", &dir)
.args(["plugins", "list"]);
cmd.assert()
.success()
.stdout(predicate::str::contains("bundled manifest.toml not found"))
.stdout(predicate::str::contains("healthy:"))
.stdout(predicate::str::contains("source:"))
.stdout(predicate::str::contains("bundled"));
}
#[cfg(unix)]
#[test]
fn bundled_manifest_controls_default_enable_contract() {
let dir = make_temp_dir("osp-cli-plugin-bundled-manifest");
let _plugin_path = write_hello_plugin(&dir);
write_manifest(
&dir,
r#"
protocol_version = 1
[[plugin]]
id = "hello"
exe = "osp-hello"
version = "0.1.0"
enabled_by_default = false
commands = ["hello"]
"#,
);
let home = make_temp_dir("osp-cli-plugin-home");
let mut first = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
first
.envs(crate::test_env::isolated_env(&home))
.env("OSP_BUNDLED_PLUGIN_DIR", &dir)
.args(["hello"]);
first.assert().failure().stderr(predicate::str::contains(
"no plugin provides command: hello",
));
let mut enable = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
enable
.envs(crate::test_env::isolated_env(&home))
.env("OSP_BUNDLED_PLUGIN_DIR", &dir)
.args(["plugins", "enable", "hello"]);
enable
.assert()
.success()
.stderr(predicate::str::contains("enabled command: hello"));
let mut second = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
second
.envs(crate::test_env::isolated_env(&home))
.env("OSP_BUNDLED_PLUGIN_DIR", &dir)
.args(["hello"]);
second
.assert()
.success()
.stdout(predicate::str::contains("hello-from-plugin"));
}
#[cfg(unix)]
#[test]
fn bundled_manifest_mismatch_marks_plugin_unhealthy_contract() {
let dir = make_temp_dir("osp-cli-plugin-bundled-manifest-mismatch");
let _plugin_path = write_hello_plugin(&dir);
write_manifest(
&dir,
r#"
protocol_version = 1
[[plugin]]
id = "hello"
exe = "osp-hello"
version = "0.1.0"
enabled_by_default = true
commands = ["ldap"]
"#,
);
let home = make_temp_dir("osp-cli-plugin-home");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
cmd.envs(crate::test_env::isolated_env(&home))
.env("OSP_BUNDLED_PLUGIN_DIR", &dir)
.args(["plugins", "list"]);
cmd.assert().success().stdout(predicate::str::contains(
"manifest commands mismatch for hello",
));
}
#[cfg(unix)]
#[test]
fn bundled_manifest_id_stays_visible_when_describe_id_mismatches_contract() {
let dir = make_temp_dir("osp-cli-plugin-bundled-id-mismatch");
let _plugin_path = write_describe_mismatch_plugin(&dir);
write_manifest(
&dir,
r#"
protocol_version = 1
[[plugin]]
id = "hello"
exe = "osp-hello"
version = "0.1.0"
enabled_by_default = true
commands = ["hello"]
"#,
);
let home = make_temp_dir("osp-cli-plugin-bundled-id-mismatch-home");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
let output = cmd
.envs(crate::test_env::isolated_env(&home))
.env("OSP_BUNDLED_PLUGIN_DIR", &dir)
.args(["--json", "plugins", "list"])
.assert()
.success()
.get_output()
.clone();
let payload = parse_json_stdout(&output.stdout);
assert!(
payload
.as_array()
.expect("plugins list should render a JSON array")
.iter()
.any(|row| {
row["plugin_id"] == "hello"
&& row["issue"]
.as_str()
.is_some_and(|issue| issue.contains("manifest id mismatch: expected hello, got wrong"))
}),
"expected manifest id mismatch row in payload: {payload}"
);
}
#[cfg(unix)]
#[test]
fn duplicate_plugin_ids_keep_one_winner_and_shadow_later_copies_contract() {
let dir = make_temp_dir("osp-cli-plugin-duplicate-ids");
write_mismatched_id_plugin(&dir, "alpha-bin", "shared-id", "alpha");
write_mismatched_id_plugin(&dir, "beta-bin", "shared-id", "beta");
let home = make_temp_dir("osp-cli-plugin-duplicate-ids-home");
let mut list = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
let list_output = list
.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["--json", "plugins", "list"])
.assert()
.success()
.get_output()
.clone();
let list_payload = parse_json_stdout(&list_output.stdout);
let rows = list_payload
.as_array()
.expect("plugins list should render a JSON array");
assert_eq!(rows.len(), 2);
assert!(rows.iter().all(|row| row["plugin_id"] == "shared-id"));
assert_eq!(rows.iter().filter(|row| row["healthy"] == true).count(), 1);
assert_eq!(rows.iter().filter(|row| row["healthy"] == false).count(), 1);
assert!(rows.iter().any(|row| {
row["healthy"] == false
&& row["issue"].as_str().is_some_and(|issue| {
issue.contains("duplicate plugin id `shared-id` shadowed by")
})
}));
assert!(rows.iter().any(|row| {
row["healthy"] == true
&& row["commands"]
.as_array()
.is_some_and(|commands| commands.contains(&serde_json::Value::String("alpha".to_string())))
}));
let mut commands = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
let commands_output = commands
.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["--json", "plugins", "commands"])
.assert()
.success()
.get_output()
.clone();
let commands_payload = parse_json_stdout(&commands_output.stdout);
let row = first_json_row(&commands_payload, "plugins commands duplicate winner view");
assert_eq!(row["name"], "alpha");
assert_eq!(row["provider"], "shared-id");
assert_eq!(row["source"], "env");
assert_eq!(row["conflicted"], false);
assert_eq!(row["requires_selection"], false);
let mut winner = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
winner
.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["alpha"]);
winner
.assert()
.success()
.stdout(predicate::str::contains("ignored"));
let mut shadowed = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
shadowed
.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["beta"]);
shadowed.assert().failure().stderr(predicate::str::contains(
"no plugin provides command: beta",
));
}
#[cfg(unix)]
#[test]
fn plugin_min_osp_version_mismatch_marks_plugin_unhealthy_contract() {
let dir = make_temp_dir("osp-cli-plugin-min-osp-version");
let _plugin_path = write_plugin_with_min_version(&dir, "future", "9.9.9");
let home = make_temp_dir("osp-cli-plugin-min-osp-version-home");
let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
let output = cmd
.envs(crate::test_env::isolated_env(&home))
.env("OSP_PLUGIN_PATH", &dir)
.args(["--json", "plugins", "list"])
.assert()
.success()
.get_output()
.clone();
let payload = parse_json_stdout(&output.stdout);
assert!(
payload
.as_array()
.expect("plugins list should render a JSON array")
.iter()
.any(|row| {
row["plugin_id"] == "future"
&& row["healthy"] == false
&& row["issue"]
.as_str()
.is_some_and(|issue| issue.contains("requires osp >="))
}),
"expected min-version failure row in payload: {payload}"
);
}