texio-cli 0.1.1

Reliable Markdown operations for shell scripts and AI agents
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;

fn texio() -> Command {
    Command::cargo_bin("texio").unwrap()
}

#[test]
fn headings_reads_stdin_and_emits_json() {
    texio()
        .args(["headings", "-", "--json"])
        .write_stdin("# One\n## Two\n")
        .assert()
        .success()
        .stdout("[{\"level\":1,\"title\":\"One\"},{\"level\":2,\"title\":\"Two\"}]\n");
}

#[test]
fn missing_section_has_stable_json_error_and_exit_code() {
    texio()
        .args(["--error-format", "json", "section", "-", "Missing"])
        .write_stdin("# Present\n")
        .assert()
        .code(4)
        .stdout("")
        .stderr(predicate::str::contains(
            "{\"error\":{\"code\":\"section_not_found\",\"message\":\"section not found: Missing\"}}",
        ));
}

#[test]
fn ambiguous_section_has_stable_exit_code() {
    texio()
        .args(["section", "-", "Same"])
        .write_stdin("# Same\none\n# Same\ntwo\n")
        .assert()
        .code(5)
        .stderr(predicate::str::contains(
            "ambiguous section: Same (2 matches)",
        ));
}

#[test]
fn unreadable_input_has_stable_exit_code() {
    texio()
        .args(["headings", "file-that-does-not-exist.md"])
        .assert()
        .code(3)
        .stderr(predicate::str::starts_with("texio: "));
}

#[test]
fn replace_requires_an_explicit_mode() {
    texio()
        .args(["replace", "doc.md", "--section", "A", "--text", "new"])
        .assert()
        .code(2)
        .stderr(predicate::str::is_empty().not());
}

#[test]
fn stdout_mode_does_not_modify_the_file() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("doc.md");
    fs::write(&path, "# A\nold\n# B\nkeep\n").unwrap();

    texio()
        .args([
            "replace",
            path.to_str().unwrap(),
            "--section",
            "A",
            "--text",
            "new",
            "--stdout",
        ])
        .assert()
        .success()
        .stdout("# A\nnew\n# B\nkeep\n");

    assert_eq!(fs::read_to_string(path).unwrap(), "# A\nold\n# B\nkeep\n");
}

#[test]
fn dry_run_is_a_unified_diff_and_does_not_modify_the_file() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("doc.md");
    fs::write(&path, "# A\nold\n").unwrap();

    texio()
        .args([
            "replace",
            path.to_str().unwrap(),
            "--section",
            "A",
            "--text",
            "new",
            "--dry-run",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("-old\n+new\n"));

    assert_eq!(fs::read_to_string(path).unwrap(), "# A\nold\n");
}

#[test]
fn write_mode_updates_the_file() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("doc.md");
    fs::write(&path, "# A\nold\n").unwrap();

    texio()
        .args([
            "replace",
            path.to_str().unwrap(),
            "--section",
            "A",
            "--text",
            "new",
            "--write",
        ])
        .assert()
        .success()
        .stdout("");

    assert_eq!(fs::read_to_string(path).unwrap(), "# A\nnew\n");
}

#[test]
fn replace_can_transform_a_document_from_stdin_to_stdout() {
    texio()
        .args([
            "replace",
            "-",
            "--section",
            "A",
            "--text",
            "new",
            "--stdout",
        ])
        .write_stdin("# A\nold\n")
        .assert()
        .success()
        .stdout("# A\nnew\n");
}

#[test]
fn replace_rejects_using_stdin_twice() {
    texio()
        .args(["replace", "-", "--section", "A", "--from", "-", "--stdout"])
        .assert()
        .code(2)
        .stderr(predicate::str::contains(
            "document and replacement cannot both read from stdin",
        ));
}