osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
#[cfg(unix)]
#[test]
fn conflicting_providers_are_visible_in_plugin_commands_contract() {
    let dir = make_temp_dir("osp-cli-plugin-conflicts");
    let _alpha = write_provider_plugin(&dir, "alpha-provider", "hello", "alpha");
    let _beta = write_provider_plugin(&dir, "beta-provider", "hello", "beta");
    let home = make_temp_dir("osp-cli-plugin-conflicts-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", "commands"])
        .assert()
        .success()
        .get_output()
        .clone();

    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "plugins commands conflict view");
    assert_eq!(row["name"], "hello");
    assert_eq!(row["conflicted"], true);
    assert_eq!(row["requires_selection"], true);
    assert_eq!(row["provider"], Value::Null);
    assert_eq!(row["source"], Value::Null);
    let providers = row["providers"]
        .as_array()
        .expect("providers should render as an array");
    assert!(
        providers.contains(&Value::String("alpha-provider (env)".to_string()))
    );
    assert!(
        providers.contains(&Value::String("beta-provider (env)".to_string()))
    );
    assert!(
        output.stderr.is_empty(),
        "stderr should stay empty: {}",
        String::from_utf8_lossy(&output.stderr)
    );

}

#[cfg(unix)]
#[test]
fn provider_selection_can_be_persisted_or_overridden_per_invocation_contract() {
    let dir = make_temp_dir("osp-cli-plugin-select-provider");
    let _alpha = write_provider_plugin(&dir, "alpha-provider", "shared", "alpha");
    let _beta = write_provider_plugin(&dir, "beta-provider", "shared", "beta");
    let home = make_temp_dir("osp-cli-plugin-select-provider-home");

    let mut before = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    before
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["shared"]);
    let before_output = before.assert().failure().get_output().clone();
    assert!(
        before_output.stdout.is_empty(),
        "stdout should stay empty: {}",
        String::from_utf8_lossy(&before_output.stdout)
    );
    assert_snapshot_text!(
        "provider_selection_before_selection_stderr",
        String::from_utf8(before_output.stderr).expect("stderr should be utf-8"),
    );

    let mut oneshot = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    oneshot
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--plugin-provider", "beta-provider", "shared"]);
    oneshot
        .assert()
        .success()
        .stdout(predicate::str::contains("beta-from-plugin"));

    let mut select = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    select
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["plugins", "select-provider", "shared", "beta-provider"]);
    select.assert().success().stderr(predicate::str::contains(
        "selected provider for command `shared`: beta-provider",
    ));

    let mut commands = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    commands
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--json", "plugins", "commands"]);
    let commands_output = commands.assert().success().get_output().clone();
    let commands_stdout =
        String::from_utf8(commands_output.stdout).expect("stdout should be utf-8");
    let commands_payload = parse_json_stdout(commands_stdout.as_bytes());
    let row = first_json_row(&commands_payload, "plugins commands selected provider view");
    assert_eq!(row["name"], "shared");
    assert_eq!(row["provider"], "beta-provider");
    assert_eq!(row["conflicted"], true);
    assert_eq!(row["requires_selection"], false);
    assert_eq!(row["selected_explicitly"], true);
    assert_eq!(row["source"], "env");
    assert_snapshot_text!("provider_selection_selected_commands_json", commands_stdout);

    let mut after_select = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    after_select
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["shared"]);
    after_select
        .assert()
        .success()
        .stdout(predicate::str::contains("beta-from-plugin"))
        .stderr(predicate::str::contains("multiple plugins").not())
        .stderr(predicate::str::contains("--plugin-provider").not());

    let mut clear = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    clear
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["plugins", "clear-provider", "shared"]);
    clear.assert().success().stderr(predicate::str::contains(
        "cleared provider selection for command `shared`",
    ));

    let mut after_clear = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    after_clear
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["shared"]);
    let after_clear_output = after_clear.assert().failure().get_output().clone();
    assert!(
        after_clear_output.stdout.is_empty(),
        "stdout should stay empty: {}",
        String::from_utf8_lossy(&after_clear_output.stdout)
    );
    assert_snapshot_text!(
        "provider_selection_after_clear_stderr",
        String::from_utf8(after_clear_output.stderr).expect("stderr should be utf-8"),
    );

}

#[cfg(unix)]
#[test]
fn plugin_provider_override_works_across_discovery_sources_contract() {
    let explicit_dir = make_temp_dir("osp-cli-plugin-precedence-explicit");
    let env_dir = make_temp_dir("osp-cli-plugin-precedence-env");
    let bundled_dir = make_temp_dir("osp-cli-plugin-precedence-bundled");
    let path_dir = make_temp_dir("osp-cli-plugin-precedence-path");
    let home = make_temp_dir("osp-cli-plugin-precedence-home");
    let user_dir = home.join(".config").join("osp").join("plugins");
    std::fs::create_dir_all(&user_dir).expect("user plugin dir should be created");
    write_config(
        &home,
        r#"
[default]
extensions.plugins.discovery.path = true
"#,
    );

    let _explicit = write_provider_plugin(&explicit_dir, "explicit-provider", "ranked", "explicit");
    let _env = write_provider_plugin(&env_dir, "env-provider", "ranked", "env");
    let _bundled = write_provider_plugin(&bundled_dir, "bundled-provider", "ranked", "bundled");
    let _user = write_provider_plugin(&user_dir, "user-provider", "ranked", "user");
    let _path = write_provider_plugin(&path_dir, "path-provider", "ranked", "path");
    write_manifest(
        &bundled_dir,
        r#"
protocol_version = 1

[[plugin]]
id = "bundled-provider"
exe = "osp-bundled-provider"
version = "0.1.0"
enabled_by_default = true
commands = ["ranked"]
"#,
    );
    let path_env = format!("{}:/usr/bin:/bin", path_dir.display());

    let mut commands = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    commands
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &env_dir)
        .env("OSP_BUNDLED_PLUGIN_DIR", &bundled_dir)
        .env("PATH", &path_env)
        .args([
            "--plugin-dir",
            explicit_dir
                .to_str()
                .expect("explicit path should be utf-8"),
            "--json",
            "plugins",
            "commands",
        ]);
    let commands_output = commands.assert().success().get_output().clone();
    let commands_stdout =
        String::from_utf8(commands_output.stdout).expect("stdout should be utf-8");
    let commands_payload = parse_json_stdout(commands_stdout.as_bytes());
    let row = first_json_row(&commands_payload, "plugins commands conflicted precedence view");
    assert_eq!(row["name"], "ranked");
    assert_eq!(row["conflicted"], true);
    assert_eq!(row["requires_selection"], true);
    assert_eq!(row["provider"], Value::Null);
    assert_eq!(row["source"], Value::Null);
    assert_snapshot_text!(
        "provider_override_conflicted_commands_json",
        commands_stdout,
    );

    let mut explicit = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    explicit
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &env_dir)
        .env("OSP_BUNDLED_PLUGIN_DIR", &bundled_dir)
        .env("PATH", &path_env)
        .args([
            "--plugin-dir",
            explicit_dir
                .to_str()
                .expect("explicit path should be utf-8"),
            "--plugin-provider",
            "explicit-provider",
            "ranked",
        ]);
    explicit
        .assert()
        .success()
        .stdout(predicate::str::contains("explicit-from-plugin"));

    let mut env = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    env.envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &env_dir)
        .env("OSP_BUNDLED_PLUGIN_DIR", &bundled_dir)
        .env("PATH", &path_env)
        .args(["--plugin-provider", "env-provider", "ranked"]);
    env.assert()
        .success()
        .stdout(predicate::str::contains("env-from-plugin"));

    let mut bundled = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    bundled
        .envs(crate::test_env::isolated_env(&home))
        .env_remove("OSP_PLUGIN_PATH")
        .env("OSP_BUNDLED_PLUGIN_DIR", &bundled_dir)
        .env("PATH", &path_env)
        .args(["--plugin-provider", "bundled-provider", "ranked"]);
    bundled
        .assert()
        .success()
        .stdout(predicate::str::contains("bundled-from-plugin"));

    let mut user = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    user.envs(crate::test_env::isolated_env(&home))
        .env_remove("OSP_PLUGIN_PATH")
        .env_remove("OSP_BUNDLED_PLUGIN_DIR")
        .env("PATH", &path_env)
        .args(["--plugin-provider", "user-provider", "ranked"]);
    user.assert()
        .success()
        .stdout(predicate::str::contains("user-from-plugin"));

    std::fs::remove_file(user_dir.join("osp-user-provider"))
        .expect("user plugin should be removable");

    let mut path = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    path.envs(crate::test_env::isolated_env(&home))
        .env_remove("OSP_PLUGIN_PATH")
        .env_remove("OSP_BUNDLED_PLUGIN_DIR")
        .env("PATH", &path_env)
        .args(["--plugin-provider", "path-provider", "ranked"]);
    path.assert()
        .success()
        .stdout(predicate::str::contains("path-from-plugin"));

}