osp-cli 1.5.1

CLI and REPL for querying and managing OSP infrastructure data
Documentation
use super::{
    ParsedReplDispatch, parse_repl_invocation, render_repl_command_output,
    repl_cache_key_for_command,
};
use crate::app::sink::BufferedUiSink;
use crate::app::{
    AppState, AppStateInit, CliCommandResult, LaunchContext, ReplCommandOutput, RuntimeContext,
    TerminalKind,
};
use crate::cli::{Commands, IntroArgs};
use crate::config::{ConfigLayer, ConfigResolver, ResolveOptions};
use crate::core::output::OutputFormat;
use crate::repl::input::ReplParsedLine;
use crate::ui::RenderSettings;
use crate::ui::messages::MessageLevel;

fn base_invocation(state: &AppState) -> crate::app::ResolvedInvocation {
    crate::app::resolve_invocation_ui(
        state.runtime.config.resolved(),
        &state.runtime.ui,
        &crate::cli::invocation::InvocationOptions::default(),
    )
}

fn make_state() -> AppState {
    let mut defaults = ConfigLayer::default();
    defaults.set("profile.default", "default");
    let mut resolver = ConfigResolver::default();
    resolver.set_defaults(defaults);
    let config = resolver
        .resolve(ResolveOptions::default().with_terminal("repl"))
        .expect("test config should resolve");

    AppState::new(AppStateInit {
        context: RuntimeContext::new(None, TerminalKind::Repl, None),
        config,
        render_settings: RenderSettings::test_plain(OutputFormat::Json),
        message_verbosity: MessageLevel::Success,
        debug_verbosity: 0,
        plugins: crate::plugin::PluginManager::new(Vec::new()),
        native_commands: crate::native::NativeCommandRegistry::default(),
        themes: crate::ui::theme_catalog::ThemeCatalog::default(),
        launch: LaunchContext::default(),
    })
}

#[test]
fn parse_repl_invocation_invalid_help_alias_paths_cover_help_and_error_unit() {
    let mut state = make_state();
    let parsed = ReplParsedLine {
        command_tokens: vec!["help".to_string()],
        dispatch_tokens: vec!["help".to_string()],
        stages: Vec::new(),
    };

    let ParsedReplDispatch::Help {
        result,
        effective,
        stages,
    } = parse_repl_invocation(&state.runtime, &state.session, &parsed)
        .expect("invalid help alias should render guide help")
    else {
        panic!("expected help dispatch");
    };

    let mut sink = BufferedUiSink::default();
    let rendered = render_repl_command_output(
        &state.runtime,
        &mut state.session,
        "help",
        &stages,
        *result,
        &effective,
        &mut sink,
    )
    .expect("invalid help guide should render");
    assert!(rendered.contains("help expects a command target"));

    let staged = ReplParsedLine {
        command_tokens: vec!["help".to_string()],
        dispatch_tokens: vec!["help".to_string()],
        stages: vec!["config".to_string()],
    };
    let err = match parse_repl_invocation(&state.runtime, &state.session, &staged) {
        Ok(_) => panic!("staged invalid help alias should fail"),
        Err(err) => err,
    };
    assert!(err.to_string().contains("help expects a command target"));
}

#[test]
fn parse_repl_invocation_covers_missing_command_and_inline_help_errors_unit() {
    let mut state = make_state();
    let empty = ReplParsedLine {
        command_tokens: Vec::new(),
        dispatch_tokens: Vec::new(),
        stages: Vec::new(),
    };
    let err = match parse_repl_invocation(&state.runtime, &state.session, &empty) {
        Ok(_) => panic!("empty parsed line should fail here"),
        Err(err) => err,
    };
    assert!(err.to_string().contains("missing command"));

    let parsed = ReplParsedLine::parse("config show --wat", state.runtime.config.resolved())
        .expect("line should parse");
    let ParsedReplDispatch::Help {
        result, effective, ..
    } = parse_repl_invocation(&state.runtime, &state.session, &parsed)
        .expect("unknown argument should turn into inline help")
    else {
        panic!("expected help dispatch");
    };
    let mut sink = BufferedUiSink::default();
    let rendered = render_repl_command_output(
        &state.runtime,
        &mut state.session,
        "config show --wat",
        &[],
        *result,
        &effective,
        &mut sink,
    )
    .expect("inline help should render");
    assert!(rendered.contains("Usage"));

    let staged = ReplParsedLine::parse(
        "config show --wat | config",
        state.runtime.config.resolved(),
    )
    .expect("line should parse");
    let err = match parse_repl_invocation(&state.runtime, &state.session, &staged) {
        Ok(_) => panic!("staged inline help parse errors should fail"),
        Err(err) => err,
    };
    assert!(err.to_string().contains("--wat"));
}

#[test]
fn repl_cache_key_covers_disabled_builtin_and_external_paths_unit() {
    let state = make_state();
    let mut invocation = crate::cli::invocation::InvocationOptions::default();
    assert!(
        repl_cache_key_for_command(
            &state.runtime,
            &Commands::Intro(IntroArgs::default()),
            &invocation
        )
        .is_none()
    );

    invocation.cache = true;
    assert!(
        repl_cache_key_for_command(
            &state.runtime,
            &Commands::Intro(IntroArgs::default()),
            &invocation
        )
        .is_none()
    );

    invocation.plugin_provider = Some("plugin-a".to_string());
    let key = repl_cache_key_for_command(
        &state.runtime,
        &Commands::External(vec!["theme".to_string(), "show".to_string()]),
        &invocation,
    )
    .expect("external cache key should exist");
    assert!(key.contains("provider:plugin-a"));
    assert!(key.contains("tokens:5:theme|4:show|"));
}

#[test]
fn render_repl_command_output_covers_json_and_text_pipeline_paths_unit() {
    let mut state = make_state();
    let invocation = base_invocation(&state);
    let mut sink = BufferedUiSink::default();

    let json_result = CliCommandResult {
        exit_code: 0,
        messages: Default::default(),
        output: Some(ReplCommandOutput::Json(serde_json::json!([
            "alpha", "beta"
        ]))),
        stderr_text: None,
        failure_report: None,
    };
    let json_rendered = render_repl_command_output(
        &state.runtime,
        &mut state.session,
        "intro | beta",
        &["beta".to_string()],
        json_result,
        &invocation,
        &mut sink,
    )
    .expect("json pipeline should render");
    assert_eq!(json_rendered.trim(), "beta");

    let text_result = CliCommandResult {
        exit_code: 0,
        messages: Default::default(),
        output: Some(ReplCommandOutput::Text("alpha\nbeta\n".to_string())),
        stderr_text: None,
        failure_report: None,
    };
    let text_rendered = render_repl_command_output(
        &state.runtime,
        &mut state.session,
        "intro | beta",
        &["beta".to_string()],
        text_result,
        &invocation,
        &mut sink,
    )
    .expect("text pipeline should render");
    assert_eq!(text_rendered.trim(), "beta");
}