agent-first-psql 0.4.2

Persistent PostgreSQL client for AI agents — SQL-native JSONL in, JSONL out
Documentation
use super::*;

#[test]
fn parse_params_order_and_types() {
    let p_res = parse_params(&["2=active".to_string(), "1=42".to_string()]);
    assert!(p_res.is_ok());
    if let Ok(p) = p_res {
        assert_eq!(p[0], Value::Number(42.into()));
        assert_eq!(p[1], Value::String("active".to_string()));
    }
}

#[test]
fn parse_params_missing_index_errors() {
    let err_res = parse_params(&["2=active".to_string()]);
    assert!(err_res.is_err());
    if let Err(err) = err_res {
        assert!(err.contains("missing parameter index 1"));
    }
}

#[test]
fn parse_params_index_starts_from_one() {
    let err_res = parse_params(&["0=x".to_string()]);
    assert!(err_res.is_err());
    if let Err(err) = err_res {
        assert!(err.contains("start at 1"));
    }
}

#[test]
fn parse_params_invalid_shape() {
    let err_res = parse_params(&["abc".to_string()]);
    assert!(err_res.is_err());
    if let Err(err) = err_res {
        assert!(err.contains("expected N=value"));
    }
}

#[test]
fn parse_param_value_primitives() {
    assert_eq!(parse_param_value("null"), Value::Null);
    assert_eq!(parse_param_value("true"), Value::Bool(true));
    assert_eq!(parse_param_value("false"), Value::Bool(false));
    assert_eq!(parse_param_value("42"), Value::Number(42.into()));
    assert_eq!(parse_param_value("1.5"), serde_json::json!(1.5));
    assert_eq!(parse_param_value("NaN"), Value::String("NaN".to_string()));
    assert_eq!(parse_param_value("abc"), Value::String("abc".to_string()));
}

#[test]
fn parse_output_formats() {
    assert!(matches!(parse_output("json"), Ok(OutputFormat::Json)));
    assert!(matches!(parse_output("yaml"), Ok(OutputFormat::Yaml)));
    assert!(matches!(parse_output("plain"), Ok(OutputFormat::Plain)));
    assert!(parse_output("bad").is_err());
}

#[test]
fn parse_log_categories_normalizes_and_dedups() {
    let logs = parse_log_categories(&[
        " Query.Result ".to_string(),
        "query.result".to_string(),
        "".to_string(),
        "ALL".to_string(),
    ]);
    assert_eq!(logs, vec!["query.result".to_string(), "all".to_string()]);
}

#[test]
fn clap_log_flag_accepts_startup() {
    let cli_res =
        AfdCli::try_parse_from(["afpsql", "--mode", "pipe", "--log", "startup,query.error"]);
    assert!(cli_res.is_ok());
    if let Ok(cli) = cli_res {
        assert_eq!(
            parse_log_categories(&cli.log),
            vec!["startup".to_string(), "query.error".to_string()]
        );
    }
}

#[test]
fn startup_requested_detects_raw_log_entries() {
    assert!(startup_requested_from_raw(&[
        "afpsql".to_string(),
        "--log".to_string(),
        "startup".to_string(),
    ]));
    assert!(startup_requested_from_raw(&[
        "afpsql".to_string(),
        "--log=all".to_string(),
    ]));
    assert!(!startup_requested_from_raw(&[
        "afpsql".to_string(),
        "--log".to_string(),
        "query.error".to_string(),
    ]));
}

#[test]
fn load_sql_validation() {
    assert!(load_sql(Some("select 1".to_string()), None).is_ok());
    assert!(load_sql(Some("x".to_string()), Some("y".to_string())).is_err());
    assert!(load_sql(None, None).is_err());
}

#[test]
fn parse_psql_mode_all_flags_and_sql_file() {
    let dir = std::env::temp_dir();
    let path = dir.join(format!("afpsql_sql_{}.sql", std::process::id()));
    assert!(std::fs::write(&path, "select $1::int").is_ok());
    let raw = vec![
        "afpsql".to_string(),
        "--mode".to_string(),
        "psql".to_string(),
        "-f".to_string(),
        path.to_string_lossy().to_string(),
        "-h".to_string(),
        "localhost".to_string(),
        "-p".to_string(),
        "5432".to_string(),
        "-U".to_string(),
        "roger".to_string(),
        "-d".to_string(),
        "postgres".to_string(),
        "--conninfo-secret".to_string(),
        "host=localhost user=roger dbname=postgres".to_string(),
        "-v".to_string(),
        "1=7".to_string(),
        "--output".to_string(),
        "plain".to_string(),
    ];
    let mode_res = parse_psql_mode(&raw);
    assert!(mode_res.is_ok());
    if let Ok(mode) = mode_res {
        assert!(matches!(mode, Mode::Cli(_)));
        if let Mode::Cli(req) = mode {
            assert_eq!(req.sql.trim(), "select $1::int");
            assert_eq!(req.params.len(), 1);
            assert!(matches!(req.output, OutputFormat::Plain));
            assert_eq!(req.session.host.as_deref(), Some("localhost"));
            assert_eq!(req.session.user.as_deref(), Some("roger"));
            assert_eq!(req.session.dbname.as_deref(), Some("postgres"));
            assert!(req.session.conninfo_secret.is_some());
        }
    }
    let _ = std::fs::remove_file(path);
}

#[test]
fn parse_psql_mode_dsn_and_errors() {
    let raw = vec![
        "afpsql".to_string(),
        "--mode".to_string(),
        "psql".to_string(),
        "-c".to_string(),
        "select 1".to_string(),
        "--dsn-secret".to_string(),
        "postgresql://localhost/postgres".to_string(),
    ];
    let mode_res = parse_psql_mode(&raw);
    assert!(mode_res.is_ok());
    if let Ok(mode) = mode_res {
        assert!(matches!(mode, Mode::Cli(_)));
        if let Mode::Cli(req) = mode {
            assert_eq!(
                req.session.dsn_secret.as_deref(),
                Some("postgresql://localhost/postgres")
            );
        }
    }

    let bad = vec![
        "afpsql".to_string(),
        "--mode".to_string(),
        "psql".to_string(),
        "--bad".to_string(),
    ];
    let err_res = parse_psql_mode(&bad);
    assert!(err_res.is_err());
    if let Err(err) = err_res {
        assert!(err.contains("unsupported psql-mode argument"));
    }
}

#[test]
fn parse_psql_mode_positional_dsn_does_not_short_circuit() {
    let raw = vec![
        "afpsql".to_string(),
        "--mode".to_string(),
        "psql".to_string(),
        "postgresql://localhost/postgres".to_string(),
        "-c".to_string(),
        "select $1::int as n".to_string(),
        "-v".to_string(),
        "1=7".to_string(),
    ];
    let mode_res = parse_psql_mode(&raw);
    assert!(mode_res.is_ok());
    if let Ok(mode) = mode_res {
        assert!(matches!(mode, Mode::Cli(_)));
        if let Mode::Cli(req) = mode {
            assert_eq!(
                req.session.dsn_secret.as_deref(),
                Some("postgresql://localhost/postgres")
            );
            assert_eq!(req.sql, "select $1::int as n");
            assert_eq!(req.params, vec![serde_json::json!(7)]);
        }
    }
}

#[test]
fn parse_psql_mode_port_and_v_errors() {
    let bad_port = vec![
        "afpsql".to_string(),
        "--mode".to_string(),
        "psql".to_string(),
        "-p".to_string(),
        "abc".to_string(),
        "-c".to_string(),
        "select 1".to_string(),
    ];
    let err_res = parse_psql_mode(&bad_port);
    assert!(err_res.is_err());
    if let Err(err) = err_res {
        assert!(err.contains("invalid -p port"));
    }

    let bad_v = vec![
        "afpsql".to_string(),
        "--mode".to_string(),
        "psql".to_string(),
        "-c".to_string(),
        "select $1".to_string(),
        "-v".to_string(),
        "bad".to_string(),
    ];
    let err_res = parse_psql_mode(&bad_v);
    assert!(err_res.is_err());
    if let Err(err) = err_res {
        assert!(err.contains("expected N=value") || err.contains("invalid"));
    }
}