bijux-cli 0.3.6

Command-line runtime for automation, plugin-driven tools, and interactive workflows with structured output.
Documentation
#![forbid(unsafe_code)]
//! Expanded transcript parity and resiliency cases for REPL runtime.

use libc as _;
use std::fs;
use std::time::{Duration, Instant};

use bijux_cli as _;
use bijux_cli::api::repl::{
    completion_candidates, configure_history, execute_repl_input, execute_repl_line,
    inspect_last_error, load_history, register_completion_registry,
    register_plugin_completion_hook, repl_argv_from_line, startup_repl,
    startup_repl_with_diagnostics, ReplEvent, ReplInput,
};
use bijux_cli::api::routing::parser::parse_intent;
use bijux_cli::api::runtime::run_app;
use serde_json as _;
use shlex as _;
use thiserror as _;

fn temp_history_path(name: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!("bijux-repl-case-{name}.txt"))
}

#[test]
fn transcript_case_help_command() {
    let (mut session, _) = startup_repl("default", None);
    let event = execute_repl_input(&mut session, ReplInput::Line(":help status".to_string()))
        .expect("help should execute");
    match event {
        ReplEvent::Continue(Some(frame)) => assert!(frame.content.contains("Usage:")),
        _ => panic!("unexpected help event"),
    }
}

#[test]
fn transcript_case_plugin_command() {
    let (mut session, _) = startup_repl("default", None);
    let frame = execute_repl_line(&mut session, "community inspect").expect("plugin route");
    let content = frame.expect("frame").content;
    assert!(!content.trim().is_empty());
}

#[test]
fn transcript_case_error_command() {
    let (mut session, _) = startup_repl("default", None);
    let err = execute_repl_input(&mut session, ReplInput::Line(":invalid".to_string()))
        .expect_err("invalid meta command should fail");
    assert!(err.to_string().contains("invalid repl command"));
    assert!(inspect_last_error(&session).is_some());
}

#[test]
fn transcript_case_quiet_mode() {
    let (mut session, _) = startup_repl("default", None);
    let _ = execute_repl_input(&mut session, ReplInput::Line(":set quiet on".to_string()))
        .expect("quiet on");
    let frame = execute_repl_line(&mut session, "status").expect("status line");
    assert!(frame.is_none());
}

#[test]
fn transcript_case_json_mode() {
    let (mut session, _) = startup_repl("default", None);
    let _ = execute_repl_input(&mut session, ReplInput::Line(":set format json".to_string()))
        .expect("json mode");
    let frame = execute_repl_line(&mut session, "status").expect("json line");
    assert!(frame.expect("frame").content.trim_start().starts_with('{'));
}

#[test]
fn transcript_case_yaml_mode() {
    let (mut session, _) = startup_repl("default", None);
    let _ = execute_repl_input(&mut session, ReplInput::Line(":set format yaml".to_string()))
        .expect("yaml mode");
    let frame = execute_repl_line(&mut session, "status").expect("yaml line");
    assert!(frame.expect("frame").content.contains("status:"));
}

#[test]
fn transcript_case_interrupt() {
    let (mut session, _) = startup_repl("default", None);
    let interrupted = execute_repl_input(&mut session, ReplInput::Interrupt).expect("interrupt");
    assert!(matches!(interrupted, ReplEvent::Interrupted(_)));
}

#[test]
fn transcript_case_eof_exit() {
    let (mut session, _) = startup_repl("default", None);
    let eof = execute_repl_input(&mut session, ReplInput::Eof).expect("eof");
    assert!(matches!(eof, ReplEvent::Exit(_)));
}

#[test]
fn history_file_supports_python_prompt_toolkit_layout() {
    let path = temp_history_path("python-layout");
    fs::write(&path, "status\ndoctor\ncommunity inspect\n").expect("write history");

    let (mut session, _) = startup_repl("default", None);
    configure_history(&mut session, Some(path.clone()), true, 10);
    load_history(&mut session).expect("load history");

    assert_eq!(session.history, vec!["status", "doctor", "community inspect"]);
    let _ = fs::remove_file(path);
}

#[test]
fn history_file_supports_cli_json_layout_for_repl_interop() {
    let path = temp_history_path("cli-json-layout");
    fs::write(
        &path,
        "[{\"command\":\"status\",\"timestamp\":1.0},{\"command\":\"plugins list\",\"timestamp\":2.0}]",
    )
    .expect("write history");

    let (mut session, _) = startup_repl("default", None);
    configure_history(&mut session, Some(path.clone()), true, 10);
    load_history(&mut session).expect("load history");

    assert_eq!(session.history, vec!["status", "plugins list"]);
    let _ = fs::remove_file(path);
}

#[test]
fn repl_line_tokenization_matches_cli_parser_expectations() {
    let argv = repl_argv_from_line("status --format json --no-pretty");
    let parsed = parse_intent(&argv).expect("repl argv should parse");

    let expected = parse_intent(&[
        "bijux".to_string(),
        "status".to_string(),
        "--format".to_string(),
        "json".to_string(),
        "--no-pretty".to_string(),
    ])
    .expect("expected argv should parse");

    assert_eq!(parsed.normalized_path, expected.normalized_path);
    assert_eq!(parsed.global_flags.output_format, expected.global_flags.output_format);
}

#[test]
fn completion_includes_runtime_registry_candidates() {
    let (mut session, _) = startup_repl("default", None);
    register_completion_registry(
        &mut session,
        "runtime-catalog",
        vec!["history".to_string(), "history clear".to_string(), "memory".to_string()],
    );
    let history = completion_candidates(&session, "hist");
    let cli = completion_candidates(&session, "cli");

    assert!(history.iter().any(|s| s == "history"));
    assert!(cli.iter().any(|s| s == "cli"));
}

#[test]
fn completion_includes_plugin_namespace_candidates() {
    let (mut session, _) = startup_repl("default", None);
    register_plugin_completion_hook(
        &mut session,
        "community",
        vec!["community inspect".to_string(), "community status".to_string()],
    );
    let values = completion_candidates(&session, "community");
    assert!(values.iter().any(|s| s == "community"));
    assert!(values.iter().any(|s| s == "community inspect"));
}

#[test]
fn malformed_history_recovers_without_crashing() {
    let path = temp_history_path("malformed");
    fs::write(&path, "{not-json\u{0}").expect("write malformed history");

    let (mut session, _) = startup_repl("default", None);
    configure_history(&mut session, Some(path.clone()), true, 50);
    load_history(&mut session).expect("load should tolerate corruption");

    assert!(session.history.is_empty());
    assert!(inspect_last_error(&session).is_some());
    let _ = fs::remove_file(path);
}

#[test]
fn large_history_load_stays_within_sanity_budget() {
    let path = temp_history_path("perf");
    let lines =
        (0..20_000).map(|idx| format!("status --item {idx}")).collect::<Vec<_>>().join("\n");
    fs::write(&path, format!("{lines}\n")).expect("write history");

    let (mut session, _) = startup_repl("default", None);
    configure_history(&mut session, Some(path.clone()), true, 20_000);

    let started = Instant::now();
    load_history(&mut session).expect("load history");
    let elapsed = started.elapsed();

    assert_eq!(session.history.len(), 20_000);
    assert!(elapsed < Duration::from_secs(2));
    let _ = fs::remove_file(path);
}

#[test]
fn startup_works_without_config_or_plugin_registry() {
    let (_session, _startup) = startup_repl("default", None);
    let (_session2, _startup2, diagnostics) =
        startup_repl_with_diagnostics("default", None, &["community"]);
    assert_eq!(diagnostics.len(), 1);
}

#[test]
fn transcript_cases_cover_status_doctor_plugins_config_get_and_history() {
    let (mut session, _) = startup_repl("default", None);
    let commands =
        ["status", "doctor", "plugins list", "cli config get repl_missing_key", "history"];

    for command in commands {
        let frame = execute_repl_line(&mut session, command).expect("command should execute");
        assert!(frame.is_some(), "expected frame for command: {command}");
    }
}

#[test]
fn transcript_case_command_failure_recovery_and_syntax_errors() {
    let (mut session, _) = startup_repl("default", None);
    let failed = execute_repl_line(&mut session, "inspect unexpected")
        .expect("execution should return frame");
    let failed_frame = failed.expect("failure should emit frame");
    assert_eq!(failed_frame.stream, bijux_cli::api::repl::ReplStream::Stderr);
    assert!(failed_frame.content.contains("Usage: bijux"));

    let recovered = execute_repl_line(&mut session, "status").expect("session should recover");
    assert!(recovered.expect("status frame").content.contains("\"status\""));
}

#[test]
fn transcript_case_nested_help_inside_repl() {
    let (mut session, _) = startup_repl("default", None);
    let event = execute_repl_input(&mut session, ReplInput::Line(":help cli status".to_string()))
        .expect("nested help should execute");
    match event {
        ReplEvent::Continue(Some(frame)) => {
            assert!(frame.content.contains("Usage:"));
            assert!(frame.content.contains("status"));
        }
        _ => panic!("unexpected nested help event"),
    }
}

#[test]
fn transcript_case_switching_output_formats_in_session() {
    let (mut session, _) = startup_repl("default", None);

    let _ = execute_repl_input(&mut session, ReplInput::Line(":set format json".to_string()))
        .expect("json mode");
    let json_frame = execute_repl_line(&mut session, "status").expect("json status");
    assert!(json_frame.expect("json frame").content.trim_start().starts_with('{'));

    let _ = execute_repl_input(&mut session, ReplInput::Line(":set format yaml".to_string()))
        .expect("yaml mode");
    let yaml_frame = execute_repl_line(&mut session, "status").expect("yaml status");
    assert!(yaml_frame.expect("yaml frame").content.contains("status:"));

    let _ = execute_repl_input(&mut session, ReplInput::Line(":set format text".to_string()))
        .expect("text mode");
    let text_frame = execute_repl_line(&mut session, "status").expect("text status");
    assert!(text_frame.expect("text frame").content.contains("status:"));
}

#[test]
fn transcript_case_trace_mode_in_session() {
    let (mut session, _) = startup_repl("default", None);
    let _ = execute_repl_input(&mut session, ReplInput::Line(":set trace on".to_string()))
        .expect("trace mode on");
    assert!(session.trace_mode);

    let traced = execute_repl_line(&mut session, "status").expect("status line");
    assert!(traced.expect("frame").content.contains("\"status\""));

    let _ = execute_repl_input(&mut session, ReplInput::Line(":set trace off".to_string()))
        .expect("trace mode off");
    assert!(!session.trace_mode);
}

#[test]
fn completion_covers_grouped_cli_and_partial_tokens() {
    let (mut session, _) = startup_repl("default", None);
    register_completion_registry(
        &mut session,
        "runtime-catalog",
        vec!["history".to_string(), "history clear".to_string(), "memory".to_string()],
    );
    let cli = completion_candidates(&session, "cli");
    let history = completion_candidates(&session, "hist");
    let partial = completion_candidates(&session, "mem");

    assert!(cli.iter().any(|value| value.starts_with("cli")));
    assert!(history.iter().any(|value| value == "history"));
    assert!(partial.iter().any(|value| value == "memory"));
}

#[test]
fn reserved_namespace_collision_diagnostics_are_reported_in_session() {
    let (mut session, _) = startup_repl("default", None);
    let temp = std::env::temp_dir().join("bijux-repl-reserved-namespace");
    let frame = execute_repl_line(
        &mut session,
        &format!("cli plugins scaffold python cli --path {}", temp.display()),
    )
    .expect("command should execute");
    let failure = frame.expect("expected diagnostics frame");
    assert_eq!(failure.stream, bijux_cli::api::repl::ReplStream::Stderr);
    assert!(failure.content.contains("reserved"));
}

#[test]
fn repl_does_not_define_separate_semantics_for_common_commands() {
    let (mut session, _) = startup_repl("default", None);
    let commands = ["status", "doctor", "history"];

    for command in commands {
        let repl = execute_repl_line(&mut session, command).expect("repl command").expect("frame");
        let repl_json: serde_json::Value = serde_json::from_str(&repl.content).expect("repl json");

        let cli = run_app(&[
            "bijux".to_string(),
            "--format".to_string(),
            "json".to_string(),
            "--pretty".to_string(),
            "--color".to_string(),
            "never".to_string(),
            "--log-level".to_string(),
            "info".to_string(),
            command.to_string(),
        ])
        .expect("cli run");
        let cli_json: serde_json::Value = serde_json::from_str(&cli.stdout).expect("cli json");
        assert_eq!(repl_json, cli_json, "semantic mismatch for command: {command}");
    }
}

#[test]
fn repl_startup_latency_stays_within_loaded_registry_budget() {
    let start = Instant::now();
    let (_session, _startup, diagnostics) = startup_repl_with_diagnostics(
        "default",
        None,
        &["community", "memory", "history", "plugins", "doctor"],
    );
    let elapsed = start.elapsed();
    assert_eq!(diagnostics.len(), 5);
    assert!(elapsed < Duration::from_millis(200));
}

#[test]
fn repl_output_parity_with_non_interactive_cli_for_status() {
    let (mut session, _) = startup_repl("default", None);
    let repl = execute_repl_line(&mut session, "status").expect("repl status").expect("repl frame");
    assert_eq!(repl.stream, bijux_cli::api::repl::ReplStream::Stdout);
    let repl_value: serde_json::Value = serde_json::from_str(&repl.content).expect("repl json");

    let cli = run_app(&[
        "bijux".to_string(),
        "--format".to_string(),
        "json".to_string(),
        "--pretty".to_string(),
        "--color".to_string(),
        "never".to_string(),
        "--log-level".to_string(),
        "info".to_string(),
        "status".to_string(),
    ])
    .expect("cli run");
    let cli_value: serde_json::Value = serde_json::from_str(&cli.stdout).expect("cli json");

    assert_eq!(repl_value, cli_value);
}