xtask-todo-lib 0.1.32

Todo workspace library and cargo devshell subcommand
Documentation
use std::cell::RefCell;
use std::io::Cursor;
use std::path::PathBuf;
use std::rc::Rc;

use crate::test_support::{cwd_mutex, devshell_workspace_env_mutex, vm_env_mutex};

use super::super::vfs::Vfs;
use super::super::vm::{
    SessionHolder, VmConfig, ENV_DEVSHELL_VM, ENV_DEVSHELL_VM_BACKEND, ENV_DEVSHELL_VM_SOCKET,
    ENV_DEVSHELL_VM_WORKSPACE_MODE,
};
use super::{process_line, run, should_add_history_entry, StepResult};

fn vm_test() -> Rc<RefCell<SessionHolder>> {
    Rc::new(RefCell::new(SessionHolder::new_host()))
}

#[test]
fn process_line_empty_returns_continue() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, "  \n", &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Continue);
}

#[test]
fn process_line_exit_returns_exit() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, "exit", &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Exit);
}

#[test]
fn process_line_quit_returns_exit() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, "quit", &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Exit);
}

#[test]
fn process_line_parse_error_returns_continue() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, "echo >", &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Continue);
    let err = String::from_utf8(stderr).unwrap();
    assert!(err.contains("parse error"));
}

#[test]
fn process_line_pwd_continues_and_writes() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, "pwd", &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Continue);
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains('/'));
}

#[test]
fn process_line_unknown_command_continues_and_stderr() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(
        &vfs,
        &vm,
        "unknowncmd",
        &mut stdin,
        &mut stdout,
        &mut stderr,
    );
    assert_eq!(r, StepResult::Continue);
    let err = String::from_utf8(stderr).unwrap();
    assert!(err.contains("unknown command"));
}

#[test]
fn process_line_source_runs_script_from_host() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let dir = std::env::temp_dir().join(format!("devshell_repl_source_{}", std::process::id()));
    let _ = std::fs::create_dir_all(&dir);
    let script_path = dir.join("repl_sourced.dsh");
    std::fs::write(&script_path, "echo repl_sourced\n").unwrap();
    let line = format!("source {}", script_path.display());
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, &line, &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Continue);
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("repl_sourced"), "stdout: {out}");
    let _ = std::fs::remove_file(&script_path);
    let _ = std::fs::remove_dir(&dir);
}

#[test]
fn process_line_dot_path_runs_script() {
    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let vm = vm_test();
    let dir = std::env::temp_dir().join(format!("devshell_repl_dot_{}", std::process::id()));
    let _ = std::fs::create_dir_all(&dir);
    let script_path = dir.join("dot_sourced.dsh");
    std::fs::write(&script_path, "echo dot_ok\n").unwrap();
    let line = format!(". {}", script_path.display());
    let mut stdin = Cursor::new(b"");
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    let r = process_line(&vfs, &vm, &line, &mut stdin, &mut stdout, &mut stderr);
    assert_eq!(r, StepResult::Continue);
    let out = String::from_utf8(stdout).unwrap();
    assert!(out.contains("dot_ok"), "stdout: {out}");
    let _ = std::fs::remove_file(&script_path);
    let _ = std::fs::remove_dir(&dir);
}

#[test]
fn should_add_history_entry_skips_empty_and_duplicate() {
    assert!(!should_add_history_entry("", None));
    assert!(!should_add_history_entry("   ", None));
    assert!(should_add_history_entry("pwd", None));
    assert!(!should_add_history_entry("pwd", Some("pwd")));
    assert!(should_add_history_entry("ls", Some("pwd")));
}

fn tmp_dir(name: &str) -> PathBuf {
    std::env::temp_dir().join(format!(
        "xtask_devshell_repl_{name}_{}_{}",
        std::process::id(),
        std::thread::current().name().unwrap_or("test")
    ))
}

fn restore_var(key: &str, val: Option<std::ffi::OsString>) {
    match val {
        Some(v) => std::env::set_var(key, v),
        None => std::env::remove_var(key),
    }
}

#[cfg(feature = "beta-vm")]
#[test]
fn run_readline_guest_primary_eof_writes_session_json_not_legacy_bin() {
    let _cwd = cwd_mutex();
    let _vm = vm_env_mutex();
    let _ws = devshell_workspace_env_mutex();

    let dir = tmp_dir("guest_primary_eof");
    let _ = std::fs::remove_dir_all(&dir);
    std::fs::create_dir_all(&dir).unwrap();
    let dir = dir.canonicalize().expect("canonicalize temp dir");
    let old_cwd = std::env::current_dir().expect("current_dir");
    std::env::set_current_dir(&dir).expect("chdir");

    let old_vm = std::env::var_os(ENV_DEVSHELL_VM);
    let old_backend = std::env::var_os(ENV_DEVSHELL_VM_BACKEND);
    let old_mode = std::env::var_os(ENV_DEVSHELL_VM_WORKSPACE_MODE);
    let old_sock = std::env::var_os(ENV_DEVSHELL_VM_SOCKET);
    let old_wsroot = std::env::var_os(super::session_store::ENV_DEVSHELL_WORKSPACE_ROOT);

    std::env::set_var(ENV_DEVSHELL_VM, "1");
    std::env::set_var(ENV_DEVSHELL_VM_BACKEND, "beta");
    std::env::set_var(ENV_DEVSHELL_VM_WORKSPACE_MODE, "guest");
    std::env::set_var(ENV_DEVSHELL_VM_SOCKET, "tcp:127.0.0.1:9");
    std::env::remove_var(super::session_store::ENV_DEVSHELL_WORKSPACE_ROOT);

    let cfg = VmConfig::from_env();
    let session = SessionHolder::try_from_config(&cfg).expect("beta session from config");
    assert!(
        session.is_guest_primary(),
        "mode=guest + backend=beta should be guest-primary"
    );
    let vm_session = Rc::new(RefCell::new(session));

    let vfs = Rc::new(RefCell::new(Vfs::new()));
    let bin_path = dir.join(".dev_shell.bin");
    let mut stdin = Cursor::new(Vec::<u8>::new());
    let mut stdout = Vec::new();
    let mut stderr = Vec::new();
    run(
        &vfs,
        &vm_session,
        false,
        &bin_path,
        &mut stdin,
        &mut stdout,
        &mut stderr,
    )
    .expect("run readline eof");

    let session_json = dir.join(".cargo-devshell").join("session.json");
    assert!(
        !bin_path.exists(),
        "guest-primary exit should skip legacy bin save"
    );
    assert!(
        session_json.is_file(),
        "expected guest-primary session JSON"
    );
    let json = std::fs::read_to_string(&session_json).expect("read session json");
    assert!(
        json.contains("\"format\": \"devshell_session_v1\""),
        "session format should be v1: {json}"
    );

    restore_var(ENV_DEVSHELL_VM, old_vm);
    restore_var(ENV_DEVSHELL_VM_BACKEND, old_backend);
    restore_var(ENV_DEVSHELL_VM_WORKSPACE_MODE, old_mode);
    restore_var(ENV_DEVSHELL_VM_SOCKET, old_sock);
    restore_var(
        super::session_store::ENV_DEVSHELL_WORKSPACE_ROOT,
        old_wsroot,
    );
    let _ = std::env::set_current_dir(old_cwd);
    let _ = std::fs::remove_dir_all(&dir);
}