larpshell 0.2.3

Ctrl+C then Ctrl+V is simply too much work. Just let an LLM rule your terminal!!
use super::*;

fn clean_home(suffix: &str) -> std::path::PathBuf {
    let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("target/tests")
        .join(format!("larpshell_test_clean_{suffix}"));
    if dir.exists() {
        fs::remove_dir_all(&dir).unwrap();
    }
    fs::create_dir_all(&dir).unwrap();
    dir
}

fn run_clean_home(home: &std::path::Path, args: &[&str]) -> std::process::Output {
    ensure_binary_built();
    let config_dir = home.join(".config");
    std::process::Command::new(binary())
        .args(args)
        .env("HOME", home)
        .env("XDG_CONFIG_HOME", &config_dir)
        .env("NO_COLOR", "1")
        .output()
        .expect("failed to run larpshell")
}

fn clean_home_config_path(home: &std::path::Path) -> std::path::PathBuf {
    home.join(".config").join("larpshell").join("config.toml")
}

fn stderr_text(out: &std::process::Output) -> String {
    String::from_utf8_lossy(&out.stderr).into_owned()
}

fn stdout_text(out: &std::process::Output) -> String {
    String::from_utf8_lossy(&out.stdout).into_owned()
}

fn assert_no_shell_bootstrap(out: &std::process::Output) {
    let stderr = stderr_text(out);
    assert!(
        !stderr.contains("restart shell or run 'source ~/.bashrc'"),
        "subcommand should not trigger shell bootstrap; stderr: {stderr}"
    );
}

fn assert_agent_mode_written(home: &std::path::Path, expected: &str) {
    let contents = fs::read_to_string(clean_home_config_path(home)).unwrap();
    assert!(
        contents.contains(expected),
        "config contents were: {contents}"
    );
}

fn assert_success(out: &std::process::Output) {
    assert!(
        out.status.success(),
        "expected success, stderr: {}",
        stderr_text(out)
    );
}

fn assert_file_contains(path: &std::path::Path, expected: &str) {
    let contents = fs::read_to_string(path).unwrap();
    assert!(
        contents.contains(expected),
        "file contents were: {contents}"
    );
}

fn assert_clean_home_agent_bootstrap(suffix: &str, args: &[&str], expected: &str) {
    let home = clean_home(suffix);
    let out = run_clean_home(&home, args);
    assert_success(&out);
    assert_agent_mode_written(&home, expected);
}

fn assert_prompt_show_uses_default(suffix: &str, args: &[&str], placeholder: &str) {
    let home = clean_home(suffix);
    let out = run_clean_home(&home, args);
    assert_success(&out);
    assert!(stdout_text(&out).contains(placeholder));
}

fn run_clean_home_with_editor(
    home: &std::path::Path,
    args: &[&str],
    editor: &std::path::Path,
) -> std::process::Output {
    ensure_binary_built();
    let config_dir = home.join(".config");
    std::process::Command::new(binary())
        .args(args)
        .env("HOME", home)
        .env("XDG_CONFIG_HOME", &config_dir)
        .env("EDITOR", editor)
        .env("NO_COLOR", "1")
        .output()
        .expect("failed to run larpshell")
}

fn agent_safe_prompt_path(home: &std::path::Path) -> std::path::PathBuf {
    home.join(".config")
        .join("larpshell")
        .join("agent-safe-prompt.md")
}

fn make_noop_editor(home: &std::path::Path) -> std::path::PathBuf {
    let editor_script = home.join("fake-editor.sh");
    fs::write(&editor_script, "#!/bin/sh\nexit 0\n").unwrap();
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mut perms = fs::metadata(&editor_script).unwrap().permissions();
        perms.set_mode(0o755);
        fs::set_permissions(&editor_script, perms).unwrap();
    }
    editor_script
}

const SAFE_BOOTSTRAPPED: &str = "agent = \"safe\"";
const ON_BOOTSTRAPPED: &str = "agent = \"on\"";
const OFF_BOOTSTRAPPED: &str = "agent = \"off\"";

#[test]
fn agent_on_safe_off_subcommand_updates_config() {
    let home = temp_home("agent_toggle");
    let port = mock_ollama(&[]);
    write_ollama_config(&home, port);

    let config_path = home.join("config").join("larpshell").join("config.toml");

    let out = run(&home, &["agent", "on"]);
    assert!(out.status.success());
    assert_file_contains(&config_path, "agent = \"on\"");

    let out = run(&home, &["agent", "safe"]);
    assert!(out.status.success());
    assert_file_contains(&config_path, "agent = \"safe\"");

    let out = run(&home, &["agent", "off"]);
    assert!(out.status.success());
    assert_file_contains(&config_path, "agent = \"off\"");
}

#[test]
fn agent_safe_subcommand_bootstraps_missing_config() {
    let home = temp_home("agent_safe_no_config");
    let config_path = home.join("config").join("larpshell").join("config.toml");

    let out = run(&home, &["agent", "safe"]);
    assert_success(&out);
    assert_file_contains(&config_path, "agent = \"safe\"");
}

#[test]
fn agent_safe_subcommand_bootstraps_missing_config_on_clean_home() {
    assert_clean_home_agent_bootstrap(
        "agent_safe_clean_home",
        &["agent", "safe"],
        SAFE_BOOTSTRAPPED,
    );
}

#[test]
fn agent_status_subcommand_does_not_require_shell_bootstrap() {
    let home = clean_home("agent_status_clean_home");

    let out = run_clean_home(&home, &["agent"]);
    assert_success(&out);
    assert_no_shell_bootstrap(&out);
}

#[test]
fn prompt_agent_safe_show_uses_default_on_clean_home() {
    let home = clean_home("prompt_agent_safe_clean_home");

    let out = run_clean_home(&home, &["prompt", "agent-safe", "show"]);
    assert_success(&out);
    assert!(!stdout_text(&out).contains("{request}"));
    assert_no_shell_bootstrap(&out);
}

#[test]
fn prompt_agent_safe_edit_creates_prompt_file_on_clean_home() {
    let home = clean_home("prompt_agent_safe_edit_clean_home");
    let editor_script = make_noop_editor(&home);

    let out = run_clean_home_with_editor(&home, &["prompt", "agent-safe", "edit"], &editor_script);
    assert_success(&out);

    let contents = fs::read_to_string(agent_safe_prompt_path(&home)).unwrap();
    assert!(!contents.contains("{request}"));
    assert!(contents.contains("safe, read-only tools"));
}

#[test]
fn agent_on_subcommand_bootstraps_missing_config_on_clean_home() {
    assert_clean_home_agent_bootstrap("agent_on_clean_home", &["agent", "on"], ON_BOOTSTRAPPED);
}

#[test]
fn agent_off_subcommand_bootstraps_missing_config_on_clean_home() {
    assert_clean_home_agent_bootstrap("agent_off_clean_home", &["agent", "off"], OFF_BOOTSTRAPPED);
}

#[test]
fn prompt_agent_show_uses_default_on_clean_home() {
    assert_prompt_show_uses_default(
        "prompt_agent_clean_home",
        &["prompt", "agent", "show"],
        "interacting with the user's machine",
    );
}

#[test]
fn prompt_system_show_uses_default_on_clean_home() {
    assert_prompt_show_uses_default(
        "prompt_system_clean_home",
        &["prompt", "system", "show"],
        "{request}",
    );
}

#[test]
fn prompt_explain_show_uses_default_on_clean_home() {
    assert_prompt_show_uses_default(
        "prompt_explain_clean_home",
        &["prompt", "explain", "show"],
        "{command}",
    );
}

#[test]
fn history_status_commands_still_work_with_shell_bootstrap_home() {
    let home = temp_home("history_control");

    let out = run(&home, &["history", "on"]);
    assert!(out.status.success());

    let out = run(&home, &["history", "off"]);
    assert!(out.status.success());
}

#[test]
fn tool_output_subcommand_updates_config() {
    let home = temp_home("tool_output_control");
    let port = mock_ollama(&[]);
    write_ollama_config(&home, port);

    let config_path = home.join("config").join("larpshell").join("config.toml");

    let out = run(&home, &["verbose", "off"]);
    assert_success(&out);
    assert_file_contains(&config_path, "verbose_tool_output = false");

    let out = run(&home, &["verbose", "on"]);
    assert_success(&out);
    assert_file_contains(&config_path, "verbose_tool_output = true");
}

#[test]
fn verbose_slash_command_takes_effect_immediately() {
    let home = temp_home("verbose_slash");
    let port = mock_ollama(&[
        r#"CHAT_JSON:{"message":{"content":"","tool_calls":[{"function":{"name":"search_files","arguments":{"pattern":"Cargo","directory_path":"."}}}]}}"#,
        "MESSAGE: done",
    ]);
    write_ollama_config(&home, port);

    let out = run_with_stdin_interactive(
        &home,
        &[],
        b"/agent safe\n/verbose off\nfind Cargo\n/quit\n",
    );
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("verbose tool output: off"),
        "expected verbose off message; stderr: {stderr}"
    );
    assert!(
        stderr.contains("result ("),
        "expected tool summary; stderr: {stderr}"
    );
    assert!(
        !stderr.contains("Cargo.toml"),
        "verbose output should not show result lines after /verbose off; stderr: {stderr}"
    );

    let config_path = home.join("config").join("larpshell").join("config.toml");
    assert_file_contains(&config_path, "verbose_tool_output = false");
}

#[test]
fn agent_slash_command_parsed_in_interactive() {
    let home = temp_home("agent_slash");
    let port = mock_ollama(&[]);
    write_ollama_config(&home, port);

    let out = run_with_stdin(&home, &[], b"/agent safe\n/quit\n");
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(
        stderr.contains("agent mode: safe"),
        "expected agent safe message; stderr: {stderr}"
    );

    let config_path = home.join("config").join("larpshell").join("config.toml");
    assert_file_contains(&config_path, "agent = \"safe\"");
}

#[test]
fn agent_multiline_message_does_not_indent_continuation_lines() {
    let home = temp_home("agent_multiline_message");
    let port = mock_ollama(&["MESSAGE: package needed by:
larpshell
COMMAND: echo done"]);
    let config_path = home.join("config").join("larpshell").join("config.toml");
    fs::create_dir_all(config_path.parent().unwrap()).unwrap();
    fs::write(
        &config_path,
        format!(
            "provider = \"ollama\"\nagent = \"safe\"\n\n[providers.ollama]\nbase_url = \"http://127.0.0.1:{port}\"\nmodel = \"test\"\n"
        ),
    )
    .unwrap();

    let out = run_with_stdin(&home, &[], b"install package\n");
    let stderr = String::from_utf8_lossy(&out.stderr);
    assert!(out.status.success(), "stderr: {stderr}");
    assert!(
        stderr.contains("● package needed by:\nlarpshell"),
        "expected multiline message without indent; stderr: {stderr}"
    );
    assert!(
        !stderr.contains("● package needed by:\n  larpshell"),
        "message continuation line should not be indented; stderr: {stderr}"
    );
}

#[test]
fn agent_off_by_default_in_config() {
    let home = temp_home("agent_default");
    let port = mock_ollama(&[]);
    write_ollama_config(&home, port);

    let config_path = home.join("config").join("larpshell").join("config.toml");
    let contents = fs::read_to_string(&config_path).unwrap();
    assert!(
        !contents.contains("agent = \"on\"") && !contents.contains("agent = \"safe\""),
        "fresh config should not have agent enabled"
    );
}