osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
#[cfg(unix)]
#[test]
fn external_plugin_dispatch_contract() {
    let dir = make_temp_dir("osp-cli-plugin-exec");
    let _plugin_path = write_hello_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-exec-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"]);
    cmd.assert()
        .success()
        .stdout(predicate::str::contains("hello-from-plugin"));

}

#[test]
fn plugin_dispatch_propagates_runtime_hints_contract() {
    let dir = make_temp_dir("osp-cli-plugin-runtime-hints");
    let _plugin_path = write_hints_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-runtime-home");

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    let output = cmd
        .env("TERM", "xterm-256color")
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args([
            "--profile",
            "tsd",
            "-vv",
            "-ddd",
            "--json",
            "--color",
            "never",
            "--unicode",
            "always",
            "hints",
        ])
        .assert()
        .success()
        .get_output()
        .clone();

    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "plugin runtime hints");
    assert_eq!(row["ui_verbosity"], "trace");
    assert_eq!(row["debug_level"], "3");
    assert_eq!(row["format"], "json");
    assert_eq!(row["color"], "never");
    assert_eq!(row["unicode"], "always");
    assert_eq!(row["profile"], "tsd");
    assert_eq!(row["terminal_kind"], "cli");
    assert_eq!(row["terminal"], "xterm-256color");

}

#[cfg(unix)]
#[test]
fn plugin_dispatch_propagates_config_env_contract() {
    let dir = make_temp_dir("osp-cli-plugin-config-env");
    let _plugin_path = write_config_env_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-config-env-home");
    write_config(
        &home,
        r#"
[default]
profile.default = "uio"
extensions.plugins.env.shared.url = "https://common.example"
extensions.plugins.env.endpoint = "shared-endpoint"
extensions.plugins.cfg.env.endpoint = "plugin-endpoint"
extensions.plugins.cfg.env.api.token = "token-123"
extensions.plugins.cfg.env.enable_cache = true
extensions.plugins.cfg.env.retries = 3
"#,
    );

    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", "cfg"])
        .assert()
        .success()
        .get_output()
        .clone();

    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "plugin config env");
    assert_eq!(row["shared_url"], "https://common.example");
    assert_eq!(row["endpoint"], "plugin-endpoint");
    assert_eq!(row["api_token"], "token-123");
    assert_eq!(row["enable_cache"], "true");
    assert_eq!(row["retries"], "3");
    assert!(
        output.stderr.is_empty(),
        "stderr should stay empty: {}",
        String::from_utf8_lossy(&output.stderr)
    );

}

#[cfg(unix)]
#[test]
fn plugin_non_zero_exit_surfaces_stderr_contract() {
    let dir = make_temp_dir("osp-cli-plugin-non-zero-exit");
    let _plugin_path = write_non_zero_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-non-zero-exit-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(["boom"]);
    let output = cmd.assert().failure().get_output().clone();
    assert!(
        output.stdout.is_empty(),
        "stdout should stay empty: {}",
        String::from_utf8_lossy(&output.stdout)
    );
    assert_snapshot_text!(
        "plugin_non_zero_exit_stderr",
        String::from_utf8(output.stderr).expect("stderr should be utf-8"),
    );

}

#[cfg(unix)]
#[test]
fn plugin_invalid_json_response_surfaces_contract() {
    let dir = make_temp_dir("osp-cli-plugin-invalid-json");
    let _plugin_path = write_invalid_json_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-invalid-json-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(["broken"]);
    cmd.assert()
        .failure()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::contains(
            "invalid JSON response from plugin broken",
        ));

}

#[cfg(unix)]
#[test]
fn plugin_messages_stay_on_stderr_when_data_is_json_contract() {
    let dir = make_temp_dir("osp-cli-plugin-messages-json");
    let _plugin_path = write_message_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-messages-json-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(["--json", "messageful"]);
    let output = cmd.assert().success().get_output().clone();
    let stdout = String::from_utf8(output.stdout.clone()).expect("stdout should be utf-8");
    let stderr = String::from_utf8(output.stderr).expect("stderr should be utf-8");
    let payload = parse_json_stdout(stdout.as_bytes());
    let row = first_json_row(&payload, "plugin json message output");
    assert_eq!(row["message"], "json-from-plugin");
    assert!(!stdout.contains("plugin-warning-line"));
    assert_snapshot_text!("plugin_messages_json_stdout", stdout);
    assert_snapshot_text!("plugin_messages_json_stderr", stderr);

}

#[cfg(unix)]
#[test]
fn plugins_config_reports_projected_env_contract() {
    let home = make_temp_dir("osp-cli-plugin-config-view-home");
    write_config(
        &home,
        r#"
[default]
profile.default = "uio"
extensions.plugins.env.endpoint = "shared-endpoint"
extensions.plugins.env.shared.url = "https://common.example"
extensions.plugins.cfg.env.endpoint = "plugin-endpoint"
extensions.plugins.cfg.env.retries = 3
"#,
    );

    let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    let output = cmd
        .envs(crate::test_env::isolated_env(&home))
        .args(["--json", "plugins", "config", "cfg"])
        .assert()
        .success()
        .get_output()
        .clone();

    let payload = parse_json_stdout(&output.stdout);
    let rows = payload
        .as_array()
        .expect("plugins config should render a JSON array");
    assert!(
        rows.iter().any(|row| {
            row["plugin_id"] == "cfg"
                && row["env"] == "OSP_PLUGIN_CFG_ENDPOINT"
                && row["value"] == "plugin-endpoint"
                && row["config_key"] == "extensions.plugins.cfg.env.endpoint"
                && row["scope"] == "plugin"
        }),
        "expected plugin-scoped endpoint row in payload: {payload}"
    );
    assert!(
        rows.iter().any(|row| {
            row["plugin_id"] == "cfg"
                && row["env"] == "OSP_PLUGIN_CFG_SHARED_URL"
                && row["value"] == "https://common.example"
                && row["config_key"] == "extensions.plugins.env.shared.url"
                && row["scope"] == "shared"
        }),
        "expected shared env row in payload: {payload}"
    );
    assert!(
        output.stderr.is_empty(),
        "stderr should stay empty: {}",
        String::from_utf8_lossy(&output.stderr)
    );

}

#[cfg(unix)]
#[test]
fn multi_command_plugin_receives_selected_command_contract() {
    let dir = make_temp_dir("osp-cli-plugin-multi-command");
    let _plugin_path = write_multi_command_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-multi-command-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", "alpha", "run"])
        .assert()
        .success()
        .get_output()
        .clone();

    let payload = parse_json_stdout(&output.stdout);
    let row = first_json_row(&payload, "multi-command plugin dispatch");
    assert_eq!(row["selected_command"], "alpha");
    assert_eq!(row["arg0"], "alpha");
    assert_eq!(row["arg1"], "run");
    assert!(
        output.stderr.is_empty(),
        "stderr should stay empty: {}",
        String::from_utf8_lossy(&output.stderr)
    );

}

#[cfg(unix)]
#[test]
fn oneshot_dispatch_does_not_use_repl_session_cache_contract() {
    let dir = make_temp_dir("osp-cli-plugin-counter");
    let _plugin_path = write_counter_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-counter-home");

    let mut first = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    first
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--unicode", "never", "counter"]);
    first
        .assert()
        .success()
        .stdout(predicate::str::contains("| 1"));

    let mut second = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    second
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--unicode", "never", "counter"]);
    second
        .assert()
        .success()
        .stdout(predicate::str::contains("| 2"));

}

#[cfg(unix)]
#[test]
fn describe_cache_is_reused_and_invalidated_contract() {
    let dir = make_temp_dir("osp-cli-plugin-describe-cache");
    let plugin_path = write_describe_counter_plugin(&dir);
    let home = make_temp_dir("osp-cli-plugin-describe-cache-home");
    let describe_count_path = dir.join("describe-count.txt");

    let mut first = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    first
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--json", "plugins", "list"]);
    let first_output = first
        .assert()
        .success()
        .get_output()
        .clone();
    let first_payload = parse_json_stdout(&first_output.stdout);
    assert!(
        first_payload
            .as_array()
            .expect("plugins list should render a JSON array")
            .iter()
            .any(|row| row["plugin_version"] == "0.1.1"),
        "expected plugin_version 0.1.1 in payload: {first_payload}"
    );
    assert_eq!(
        std::fs::read_to_string(&describe_count_path)
            .expect("describe count should be written")
            .trim(),
        "1"
    );

    let mut second = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    second
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--json", "plugins", "list"]);
    let second_output = second
        .assert()
        .success()
        .get_output()
        .clone();
    let second_payload = parse_json_stdout(&second_output.stdout);
    assert!(
        second_payload
            .as_array()
            .expect("plugins list should render a JSON array")
            .iter()
            .any(|row| row["plugin_version"] == "0.1.1"),
        "expected cached plugin_version 0.1.1 in payload: {second_payload}"
    );
    assert_eq!(
        std::fs::read_to_string(&describe_count_path)
            .expect("describe count should still be readable")
            .trim(),
        "1"
    );

    let mut script =
        std::fs::read_to_string(&plugin_path).expect("plugin script should be readable");
    script.push_str("\n# cache invalidation\n");
    std::fs::write(&plugin_path, script).expect("plugin script should be updated");

    let mut third = Command::new(assert_cmd::cargo::cargo_bin!("osp"));
    third
        .envs(crate::test_env::isolated_env(&home))
        .env("OSP_PLUGIN_PATH", &dir)
        .args(["--json", "plugins", "list"]);
    let third_output = third
        .assert()
        .success()
        .get_output()
        .clone();
    let third_payload = parse_json_stdout(&third_output.stdout);
    assert!(
        third_payload
            .as_array()
            .expect("plugins list should render a JSON array")
            .iter()
            .any(|row| row["plugin_version"] == "0.1.2"),
        "expected invalidated plugin_version 0.1.2 in payload: {third_payload}"
    );
    assert_eq!(
        std::fs::read_to_string(&describe_count_path)
            .expect("describe count should reflect invalidation")
            .trim(),
        "2"
    );

}