osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
#[test]
fn repl_builtin_overrides_do_not_mutate_runtime_ui_state_unit() {
    let mut state = make_test_state(Vec::new());
    let history = make_test_history(&mut state);
    let original_message = state.runtime.ui.message_verbosity;
    let original_debug = state.runtime.ui.debug_verbosity;

    repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "-q config get missing.key",
    )
    .expect("quiet config get should complete");
    assert_eq!(state.runtime.ui.message_verbosity, original_message);
    assert_eq!(state.runtime.ui.debug_verbosity, original_debug);

    repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "-d doctor last",
    )
    .expect("doctor last with debug override should complete");
    assert_eq!(state.runtime.ui.message_verbosity, original_message);
    assert_eq!(state.runtime.ui.debug_verbosity, original_debug);
}

#[test]
fn repl_exit_is_host_owned_at_root_but_leaves_shell_in_scope_unit() {
    let mut state = make_test_state(Vec::new());
    let history = make_test_history(&mut state);

    let root_exit = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "exit",
    )
    .expect("root exit should be handled by host dispatch");
    assert_eq!(root_exit, crate::repl::ReplLineResult::Exit(0));

    state.session.scope.enter("orch");
    let shell_exit = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "exit",
    )
    .expect("shell exit should leave the current shell");
    match shell_exit {
        crate::repl::ReplLineResult::Continue(text) => {
            assert!(text.contains("Leaving orch shell"));
        }
        other => panic!("unexpected repl result: {other:?}"),
    }
    assert!(state.session.scope.is_root());
}

#[test]
fn repl_failure_is_cached_for_doctor_last_unit() {
    let mut state = make_test_state(Vec::new());
    let history = make_test_history(&mut state);

    let err = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "missing",
    )
    .expect_err("unknown command should fail");
    let err_text = err.to_string();
    assert!(err_text.contains("plugin command failed"));

    let last = state
        .last_repl_failure()
        .expect("last failure should be recorded");
    assert_eq!(last.command_line, "missing");
    assert!(last.summary.contains("plugin command failed"));

    let rendered = doctor_cmd::run_doctor_command(
        doctor_cmd::DoctorCommandContext {
            config: crate::cli::commands::config::ConfigReadContext {
                context: &state.runtime.context,
                config: state.runtime.config.resolved(),
                ui: &state.runtime.ui,
                themes: &state.runtime.themes,
                config_overrides: &state.session.config_overrides,
                product_defaults: state.runtime.product_defaults(),
                runtime_load: state.runtime.launch.runtime_load,
            },
            plugins: crate::cli::commands::plugins::PluginsCommandContext {
                context: &state.runtime.context,
                config: state.runtime.config.resolved(),
                config_state: Some(&state.runtime.config),
                auth: &state.runtime.auth,
                clients: Some(&state.clients),
                plugin_manager: state.clients.plugins(),
                product_defaults: state.runtime.product_defaults(),
                runtime_load: state.runtime.launch.runtime_load,
            },
            ui: &state.runtime.ui,
            auth: &state.runtime.auth,
            themes: &state.runtime.themes,
            last_failure: state.session.last_failure.as_ref(),
        },
        crate::cli::DoctorArgs {
            command: Some(crate::cli::DoctorCommands::Last),
        },
    )
    .expect("doctor last should render");
    match rendered.output {
        Some(ReplCommandOutput::Json(json)) => {
            assert_eq!(json["status"], "error");
            assert_eq!(json["command"], "missing");
        }
        Some(ReplCommandOutput::Output(_)) => panic!("unexpected doctor output variant"),
        Some(ReplCommandOutput::Text(_)) => panic!("unexpected doctor output variant"),
        None => panic!("expected doctor output"),
    }
}

#[test]
fn rebuild_repl_state_preserves_last_failure_unit() {
    let mut state = make_test_state(Vec::new());
    state.record_repl_failure("ldap user nope", "boom", "boom detail");

    let next = super::super::rebuild_repl_state(&state).expect("rebuild should succeed");
    let last = next
        .last_repl_failure()
        .expect("last failure should survive rebuild");

    assert_eq!(last.command_line, "ldap user nope");
    assert_eq!(last.summary, "boom");
    assert_eq!(last.detail, "boom detail");
}

#[test]
fn repl_bang_expands_last_visible_command_unit() {
    use std::os::unix::fs::PermissionsExt;

    let dir = make_temp_dir("osp-cli-repl-bang-plugin");
    let plugin_path = dir.join("osp-cache");
    std::fs::write(
        &plugin_path,
        r#"#!/bin/sh
PATH=/usr/bin:/bin:$PATH
if [ "$1" = "--describe" ]; then
  cat <<'JSON'
{"protocol_version":1,"plugin_id":"cache","plugin_version":"0.1.0","min_osp_version":"0.1.0","commands":[{"name":"cache","about":"cache plugin","subcommands":[],"args":[],"flags":{}}]}
JSON
  exit 0
fi
printf '{"protocol_version":1,"ok":true,"data":{"message":"ok","arg":"%s"},"error":null,"meta":{"format_hint":"table","columns":["message","arg"]}}\n' "$2"
"#,
    )
    .expect("plugin script should be written");
    let mut perms = std::fs::metadata(&plugin_path)
        .expect("metadata should be readable")
        .permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&plugin_path, perms).expect("script should be executable");

    let mut state = make_test_state(vec![dir.to_path_buf()]);
    let history = make_test_history(&mut state);

    repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "cache first",
    )
    .expect("seed command should succeed");
    history
        .save_command_line("cache first")
        .expect("history seed should save");
    let cache_size_before = state.repl_cache_size();
    let expanded = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "!!",
    )
    .expect("bang expansion should succeed");
    match expanded {
        crate::repl::ReplLineResult::ReplaceInput(text) => {
            assert_eq!(text, "cache first");
        }
        other => panic!("unexpected repl result: {other:?}"),
    }
    assert_eq!(state.repl_cache_size(), cache_size_before);

}

#[test]
fn repl_bang_contains_search_expands_matching_command_unit() {
    use std::os::unix::fs::PermissionsExt;

    let dir = make_temp_dir("osp-cli-repl-bang-contains-plugin");
    let plugin_path = dir.join("osp-cache");
    std::fs::write(
        &plugin_path,
        r#"#!/bin/sh
PATH=/usr/bin:/bin:$PATH
if [ "$1" = "--describe" ]; then
  cat <<'JSON'
{"protocol_version":1,"plugin_id":"cache","plugin_version":"0.1.0","min_osp_version":"0.1.0","commands":[{"name":"cache","about":"cache plugin","subcommands":[],"args":[],"flags":{}}]}
JSON
  exit 0
fi
printf '{"protocol_version":1,"ok":true,"data":{"message":"ok","arg":"%s"},"error":null,"meta":{"format_hint":"table","columns":["message","arg"]}}\n' "$2"
"#,
    )
    .expect("plugin script should be written");
    let mut perms = std::fs::metadata(&plugin_path)
        .expect("metadata should be readable")
        .permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&plugin_path, perms).expect("script should be executable");

    let mut state = make_test_state(vec![dir.to_path_buf()]);
    let history = make_test_history(&mut state);

    repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "cache alpha",
    )
    .expect("first seed command should succeed");
    history
        .save_command_line("cache alpha")
        .expect("history seed should save");
    repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "cache beta",
    )
    .expect("second seed command should succeed");
    history
        .save_command_line("cache beta")
        .expect("history seed should save");
    let cache_size_before = state.repl_cache_size();
    let expanded = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "!?alpha",
    )
    .expect("contains bang expansion should succeed");
    match expanded {
        crate::repl::ReplLineResult::ReplaceInput(text) => {
            assert_eq!(text, "cache alpha");
        }
        other => panic!("unexpected repl result: {other:?}"),
    }
    assert_eq!(state.repl_cache_size(), cache_size_before);

}

#[test]
fn repl_bang_contains_search_respects_shell_scope_unit() {
    let mut state = make_test_state(Vec::new());
    let history = make_test_history(&mut state);

    history
        .save_command_line("config show")
        .expect("root history seed should save");

    state.session.scope.enter("orch");
    state.sync_history_shell_context();
    history
        .save_command_line("status")
        .expect("scoped history seed should save");

    let scoped_hit = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "!?status",
    )
    .expect("scoped bang search should succeed");
    match scoped_hit {
        crate::repl::ReplLineResult::ReplaceInput(text) => assert_eq!(text, "status"),
        other => panic!("unexpected repl result: {other:?}"),
    }

    let scoped_miss = repl_dispatch::execute_repl_plugin_line(
        &mut state.runtime,
        &mut state.session,
        &state.clients,
        &history,
        "!?config",
    )
    .expect("cross-shell bang miss should render feedback");
    match scoped_miss {
        crate::repl::ReplLineResult::Continue(text) => {
            assert_eq!(text, "No history match for: !?config\n");
        }
        other => panic!("unexpected repl result: {other:?}"),
    }
}