kibble 0.1.0

chew through any source into clean datasets — a fast ingestion, RAG & fine-tuning toolkit
Documentation
//! CLI-level error-handling contract (issue #60): a known-bad configuration must
//! produce a tidy `kibble: <msg>` on stderr and exit code 1 — NOT a Rust panic
//! (exit 101 + "thread 'main' panicked …"), which external callers can't parse.

use std::process::Command;

/// Run the built kibble binary with `args` in a fresh empty dir (no kibble.toml →
/// config defaults → `[ask].base_url` empty), returning (exit_code, stderr).
fn run_in_empty_dir(tag: &str, args: &[&str]) -> (Option<i32>, String) {
    let dir = std::env::temp_dir().join(format!("kibble_cli_{tag}_{}", std::process::id()));
    std::fs::create_dir_all(&dir).unwrap();
    let out = Command::new(env!("CARGO_BIN_EXE_kibble"))
        .args(args)
        .current_dir(&dir)
        .env("RUST_BACKTRACE", "0")
        .output()
        .expect("spawn kibble");
    std::fs::remove_dir_all(&dir).ok();
    (out.status.code(), String::from_utf8_lossy(&out.stderr).into_owned())
}

#[test]
fn ask_without_base_url_exits_clean_not_panic() {
    let (code, stderr) = run_in_empty_dir("ask", &["ask", "what is a stack canary?"]);
    assert_eq!(code, Some(1), "expected clean exit 1, got {code:?}\nstderr:\n{stderr}");
    assert!(stderr.contains("kibble:"), "want 'kibble:' prefix, stderr:\n{stderr}");
    assert!(!stderr.to_lowercase().contains("panicked"), "must not panic, stderr:\n{stderr}");
}

#[test]
fn search_without_index_exits_clean_not_panic() {
    let (code, stderr) = run_in_empty_dir("search", &["search", "stack canary"]);
    assert_eq!(code, Some(1), "expected clean exit 1, got {code:?}\nstderr:\n{stderr}");
    assert!(stderr.contains("kibble:"), "want 'kibble:' prefix, stderr:\n{stderr}");
    assert!(!stderr.to_lowercase().contains("panicked"), "must not panic, stderr:\n{stderr}");
}