pxh 0.9.24

pxh is a fast, cross-shell history mining tool with interactive fuzzy search, secret scanning, and bidirectional sync across machines. It indexes bash and zsh history in SQLite with rich metadata for powerful recall.
Documentation
use std::{env, fs, process::Command};

use pxh::test_utils::pxh_path;
use tempfile::TempDir;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

// Helper to create a Command with coverage environment variables
fn pxh_command() -> Command {
    let mut cmd = Command::new(pxh_path());

    // Propagate coverage environment variables if they exist
    if let Ok(profile_file) = env::var("LLVM_PROFILE_FILE") {
        cmd.env("LLVM_PROFILE_FILE", profile_file);
    }
    if let Ok(llvm_cov) = env::var("CARGO_LLVM_COV") {
        cmd.env("CARGO_LLVM_COV", llvm_cov);
    }

    cmd
}

#[test]
fn test_install_creates_correct_rc_files() -> Result<()> {
    // Test that install command correctly modifies shell RC files
    let temp_dir = TempDir::new()?;
    let home_dir = temp_dir.path();

    // Test bash installation
    let bashrc_path = home_dir.join(".bashrc");
    fs::write(&bashrc_path, "# existing bashrc content\n")?;

    let output = pxh_command().env("HOME", home_dir).args(["install", "bash"]).output()?;

    assert!(
        output.status.success(),
        "Bash install failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let bashrc_content = fs::read_to_string(&bashrc_path)?;
    assert!(
        bashrc_content.contains("# existing bashrc content"),
        "Original content should be preserved"
    );
    assert!(bashrc_content.contains("pxh shell-config bash"), "pxh shell-config should be added");
    assert!(
        !bashrc_content.contains("pxh completions"),
        "Completions are now included in shell-config, no separate line needed"
    );
    assert!(bashrc_content.contains("command -v pxh"), "Command check should be present");

    // Test zsh installation
    let zshrc_path = home_dir.join(".zshrc");
    fs::write(&zshrc_path, "# existing zshrc content\n")?;

    let output = pxh_command().env("HOME", home_dir).args(["install", "zsh"]).output()?;

    assert!(
        output.status.success(),
        "Zsh install failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let zshrc_content = fs::read_to_string(&zshrc_path)?;
    assert!(
        zshrc_content.contains("# existing zshrc content"),
        "Original content should be preserved"
    );
    assert!(zshrc_content.contains("pxh shell-config zsh"), "pxh shell-config should be added");

    Ok(())
}

#[test]
fn test_install_is_idempotent() -> Result<()> {
    // Test that running install multiple times doesn't duplicate the configuration
    let temp_dir = TempDir::new()?;
    let home_dir = temp_dir.path();
    let bashrc_path = home_dir.join(".bashrc");

    fs::write(&bashrc_path, "")?;

    // First install
    let output1 = pxh_command().env("HOME", home_dir).args(["install", "bash"]).output()?;

    assert!(output1.status.success());
    let bashrc_after_first = fs::read_to_string(&bashrc_path)?;

    // Second install
    let output2 = pxh_command().env("HOME", home_dir).args(["install", "bash"]).output()?;

    assert!(output2.status.success());
    let stdout2 = String::from_utf8_lossy(&output2.stdout);
    assert!(stdout2.contains("already present"), "Should report shell config already present");

    let bashrc_after_second = fs::read_to_string(&bashrc_path)?;
    assert_eq!(
        bashrc_after_first, bashrc_after_second,
        "RC file should not change on second install"
    );

    // Count occurrences of pxh shell-config
    let config_count = bashrc_after_second.matches("pxh shell-config").count();
    assert_eq!(config_count, 1, "Should have exactly one pxh shell-config entry");

    Ok(())
}

#[test]
fn test_install_upgrades_existing_with_separate_completions() -> Result<()> {
    // Simulate an old installation that has shell-config + completions as separate lines
    let temp_dir = TempDir::new()?;
    let home_dir = temp_dir.path();
    let bashrc_path = home_dir.join(".bashrc");

    // Write an rc file with the old-style pxh block (separate completions line)
    fs::write(
        &bashrc_path,
        r#"# existing content
if command -v pxh &> /dev/null; then
    eval "$(pxh shell-config bash)"
    eval "$(pxh completions bash)"
fi
"#,
    )?;

    let output = pxh_command().env("HOME", home_dir).args(["install", "bash"]).output()?;

    assert!(
        output.status.success(),
        "Install upgrade failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let bashrc_content = fs::read_to_string(&bashrc_path)?;
    assert!(
        !bashrc_content.contains("pxh completions"),
        "Separate completions line should be removed"
    );
    assert_eq!(bashrc_content.matches("pxh shell-config").count(), 1, "Should keep shell-config");

    // Running again should be a no-op
    let output2 = pxh_command().env("HOME", home_dir).args(["install", "bash"]).output()?;
    assert!(output2.status.success());
    let stdout2 = String::from_utf8_lossy(&output2.stdout);
    assert!(stdout2.contains("already present"), "Should report already installed");

    Ok(())
}

#[test]
fn test_shell_config_output() -> Result<()> {
    // Test that shell-config command produces valid shell code

    // Test bash config
    let bash_output = pxh_command().args(["shell-config", "bash"]).output()?;

    assert!(bash_output.status.success(), "shell-config bash failed");
    let bash_config = String::from_utf8_lossy(&bash_output.stdout);

    // Check for essential bash functions
    assert!(bash_config.contains("preexec()"), "Should define preexec function");
    assert!(bash_config.contains("precmd()"), "Should define precmd function");
    assert!(bash_config.contains("_pxh_init"), "Should have init function");
    assert!(bash_config.contains("PXH_SESSION_ID"), "Should set session ID");
    assert!(bash_config.contains("PXH_DB_PATH"), "Should set database path");

    // Test zsh config
    let zsh_output = pxh_command().args(["shell-config", "zsh"]).output()?;

    assert!(zsh_output.status.success(), "shell-config zsh failed");
    let zsh_config = String::from_utf8_lossy(&zsh_output.stdout);

    // Check for essential zsh functions
    assert!(zsh_config.contains("_pxh_addhistory"), "Should define _pxh_addhistory function");
    assert!(
        zsh_config.contains("_pxh_update_last_status"),
        "Should define _pxh_update_last_status function"
    );
    assert!(zsh_config.contains("_pxh_init"), "Should have init function");
    assert!(zsh_config.contains("add-zsh-hook"), "Should use zsh hooks");

    // zshaddhistory hook must return 0 so pxh failures don't suppress native history
    assert!(
        zsh_config.contains("return 0\n}"),
        "_pxh_addhistory must end with 'return 0' to avoid leaking pxh exit codes to zsh"
    );

    Ok(())
}

#[test]
fn test_manual_command_recording() -> Result<()> {
    // Test that we can manually record commands using insert and seal
    let temp_dir = TempDir::new()?;
    let db_path = temp_dir.path().join("test.db");

    // Insert a command
    let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();

    let insert_output = pxh_command()
        .args([
            "--db",
            db_path.to_str().unwrap(),
            "insert",
            "--shellname",
            "bash",
            "--hostname",
            "testhost",
            "--username",
            "testuser",
            "--session-id",
            "12345",
            "--start-unix-timestamp",
            &now.to_string(),
            "--working-directory",
            "/tmp",
            "echo hello world",
        ])
        .output()?;

    assert!(
        insert_output.status.success(),
        "Insert failed: {}",
        String::from_utf8_lossy(&insert_output.stderr)
    );

    // Seal the command
    let seal_output = pxh_command()
        .args([
            "--db",
            db_path.to_str().unwrap(),
            "seal",
            "--session-id",
            "12345",
            "--exit-status",
            "0",
            "--end-unix-timestamp",
            &(now + 1).to_string(),
        ])
        .output()?;

    assert!(
        seal_output.status.success(),
        "Seal failed: {}",
        String::from_utf8_lossy(&seal_output.stderr)
    );

    // Verify the command was recorded
    let show_output = pxh_command()
        .args(["--db", db_path.to_str().unwrap(), "show", "--limit", "10"])
        .output()?;

    assert!(show_output.status.success(), "Show failed");
    let history = String::from_utf8_lossy(&show_output.stdout);
    assert!(history.contains("echo hello world"), "Command should be in history");
    // Note: Working directory might not be shown in default output format
    // Just verify the command was recorded
    assert!(
        history.contains("testhost") || history.contains("echo hello world"),
        "History should contain our test data: {}",
        history
    );

    Ok(())
}

#[test]
fn test_install_creates_rc_file_if_missing() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let home_dir = temp_dir.path();

    // Do NOT create .bashrc -- it shouldn't exist yet
    let bashrc_path = home_dir.join(".bashrc");
    assert!(!bashrc_path.exists());

    let output = pxh_command().env("HOME", home_dir).args(["install", "bash"]).output()?;

    assert!(
        output.status.success(),
        "Install should succeed with missing rc file, got: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let bashrc_content = fs::read_to_string(&bashrc_path)?;
    assert!(bashrc_content.contains("pxh shell-config bash"), "pxh config should be written");

    // Same for zsh
    let zshrc_path = home_dir.join(".zshrc");
    assert!(!zshrc_path.exists());

    let output = pxh_command().env("HOME", home_dir).args(["install", "zsh"]).output()?;

    assert!(
        output.status.success(),
        "Install should succeed with missing rc file, got: {}",
        String::from_utf8_lossy(&output.stderr)
    );

    let zshrc_content = fs::read_to_string(&zshrc_path)?;
    assert!(zshrc_content.contains("pxh shell-config zsh"), "pxh config should be written");

    Ok(())
}

#[test]
fn test_install_rejects_invalid_shell() -> Result<()> {
    let temp_dir = TempDir::new()?;
    let home_dir = temp_dir.path();

    let output = pxh_command().env("HOME", home_dir).args(["install", "fish"]).output()?;

    assert!(!output.status.success(), "Should reject unsupported shell");
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stderr.contains("Unsupported shell: fish"), "Should mention unsupported shell");

    Ok(())
}