tuible 0.0.2-alpha.1

A keyboard-driven database client for your terminal, built for both humans and AI agents.
#![allow(clippy::expect_used, clippy::unwrap_used)]

use std::path::Path;
use std::process::{Command, Output};

fn tuible(args: &[&str]) -> Output {
    let state_home = tempfile::tempdir().unwrap();
    tuible_with_state(args, state_home.path())
}

fn tuible_with_state(args: &[&str], state_home: &Path) -> Output {
    Command::new(env!("CARGO_BIN_EXE_tuible"))
        .args(args)
        .env("XDG_STATE_HOME", state_home)
        .env("XDG_DATA_HOME", state_home)
        .output()
        .unwrap()
}

#[test]
fn top_level_help_explains_safety_and_workflows() {
    let output = tuible(&["--help"]);
    let stdout = String::from_utf8(output.stdout).unwrap();

    assert!(output.status.success());
    assert!(stdout.contains("read-only"));
    assert!(stdout.contains("tuible query"));
    assert!(stdout.contains("execute"));
    assert!(stdout.contains("dynamodb"));
}

#[test]
fn query_emits_bounded_typed_json() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("cli.sqlite");
    std::fs::File::create(&path).unwrap();
    let path = path.to_str().unwrap();

    assert!(
        tuible(&[
            "execute",
            path,
            "CREATE TABLE values_table (id INTEGER, name TEXT)",
            "--yes",
        ])
        .status
        .success()
    );
    assert!(
        tuible(&[
            "execute",
            path,
            "INSERT INTO values_table VALUES (1, 'one'), (2, 'two')",
            "--yes",
        ])
        .status
        .success()
    );

    let output = tuible(&[
        "query",
        path,
        "SELECT id, name FROM values_table ORDER BY id",
        "--max-rows",
        "1",
    ]);
    let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();

    assert!(output.status.success());
    assert_eq!(json["columns"], serde_json::json!(["id", "name"]));
    assert_eq!(json["rows"], serde_json::json!([[1, "one"]]));
    assert_eq!(json["truncated"], true);
}

#[test]
fn writes_require_execute_and_confirmation() {
    let dir = tempfile::tempdir().unwrap();
    let path = dir.path().join("readonly.sqlite");
    std::fs::File::create(&path).unwrap();
    let path = path.to_str().unwrap();
    assert!(
        tuible(&["execute", path, "CREATE TABLE items (id INTEGER)", "--yes"])
            .status
            .success()
    );

    let query_write = tuible(&["query", path, "INSERT INTO items VALUES (1)"]);
    let unconfirmed = tuible(&["execute", path, "INSERT INTO items VALUES (1)"]);

    assert!(!query_write.status.success());
    assert!(String::from_utf8_lossy(&query_write.stderr).contains("readonly"));
    assert!(!unconfirmed.status.success());
    assert!(String::from_utf8_lossy(&unconfirmed.stderr).contains("--yes"));
}

#[test]
fn cli_statements_never_create_interactive_query_history() {
    let dir = tempfile::tempdir().unwrap();
    let state_home = dir.path().join("state");
    let database = dir.path().join("cli-history.sqlite");
    std::fs::File::create(&database).unwrap();
    let database = database.to_str().unwrap();

    assert!(
        tuible_with_state(
            &[
                "execute",
                database,
                "CREATE TABLE private_values (value TEXT)",
                "--yes",
            ],
            &state_home,
        )
        .status
        .success()
    );
    assert!(
        tuible_with_state(
            &["query", database, "SELECT 'sensitive-literal'"],
            &state_home,
        )
        .status
        .success()
    );

    assert!(!state_home.join("tuible/query-history.json").exists());
}

#[test]
fn dynamodb_help_exposes_job_workflows_and_pagination() {
    let query = tuible(&["dynamodb", "query", "--help"]);
    let put = tuible(&["dynamodb", "put", "--help"]);
    let query_help = String::from_utf8(query.stdout).unwrap();
    let put_help = String::from_utf8(put.stdout).unwrap();

    assert!(query.status.success());
    assert!(query_help.contains("--profile"));
    assert!(query_help.contains("--index"));
    assert!(query_help.contains("--start-key"));
    assert!(query_help.contains("--typed-json"));
    assert!(put.status.success());
    assert!(put_help.contains("--condition"));
    assert!(put_help.contains("--yes"));
}

#[test]
fn dynamodb_writes_fail_before_network_without_confirmation() {
    let output = tuible(&["dynamodb", "put", "users", r#"{"pk":"USER#1"}"#, "--local"]);

    assert!(!output.status.success());
    assert!(String::from_utf8_lossy(&output.stderr).contains("--yes"));
}