insmaller 0.6.2

Config-driven installer: describe install steps in TOML and run them from one binary
//! End-to-end coverage for the `input`/`prompt` task step. The harness has
//! no TTY (cargo test redirects stdin), so these exercise the non-TTY paths:
//! env-provided values pass, missing required values fail fast, and a
//! `confirm = "X"` mismatch aborts the task — proving the engine reaches the
//! same answer as the interactive case without needing a real terminal.

use std::fs;
use std::process::Command;

fn write_marker_step(marker: &str) -> String {
    if cfg!(windows) {
        format!(r#"type = "shell"
script = "cmd /C echo ok > {marker}""#)
    } else {
        format!(r#"type = "shell"
script = "echo ok > {marker}""#)
    }
}

#[test]
fn input_confirm_match_passes_via_env() {
    let bin = env!("CARGO_BIN_EXE_insmaller");
    let dir = tempfile::tempdir().unwrap();
    let marker = dir.path().join("ran").display().to_string().replace('\\', "/");
    let marker_step = write_marker_step(&marker);
    fs::write(
        dir.path().join("installer.toml"),
        format!(
            r#"[task.reset]
[[task.reset.steps]]
type    = "input"
name    = "CONFIRM"
required = true
confirm = "RESET"

[[task.reset.steps]]
{marker_step}
"#
        ),
    )
    .unwrap();

    let out = Command::new(bin)
        .args(["task", "reset"])
        .env("CONFIRM", "RESET")
        .current_dir(dir.path())
        .output()
        .expect("run");
    assert!(
        out.status.success(),
        "env-provided CONFIRM=RESET should pass\nstderr: {}",
        String::from_utf8_lossy(&out.stderr),
    );
    assert!(
        std::path::Path::new(&marker).exists(),
        "follow-up step did not run after passing confirm",
    );
}

#[test]
fn input_confirm_mismatch_aborts() {
    let bin = env!("CARGO_BIN_EXE_insmaller");
    let dir = tempfile::tempdir().unwrap();
    let marker = dir.path().join("ran").display().to_string().replace('\\', "/");
    let marker_step = write_marker_step(&marker);
    fs::write(
        dir.path().join("installer.toml"),
        format!(
            r#"[task.reset]
[[task.reset.steps]]
type    = "input"
name    = "CONFIRM"
required = true
confirm = "RESET"

[[task.reset.steps]]
{marker_step}
"#
        ),
    )
    .unwrap();

    let out = Command::new(bin)
        .args(["task", "reset"])
        .env("CONFIRM", "NOPE")
        .current_dir(dir.path())
        .output()
        .expect("run");
    assert!(
        !out.status.success(),
        "confirm mismatch must abort\nstderr: {}",
        String::from_utf8_lossy(&out.stderr),
    );
    assert!(
        !std::path::Path::new(&marker).exists(),
        "follow-up step must not run after confirm mismatch",
    );
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("confirm"),
        "expected a 'confirm' error message; got: {}",
        String::from_utf8_lossy(&out.stderr),
    );
}

#[test]
fn input_required_unset_and_no_tty_fails_fast() {
    // No TTY (cargo test) + required + env unset → EnvResolver returns Fail
    // immediately, never blocks. Sanity-checks the structurally-non-blocking
    // contract under the new InteractiveResolver wrapper.
    let bin = env!("CARGO_BIN_EXE_insmaller");
    let dir = tempfile::tempdir().unwrap();
    fs::write(
        dir.path().join("installer.toml"),
        r#"[task.need]
[[task.need.steps]]
type    = "input"
name    = "MUST_BE_SET"
required = true
"#,
    )
    .unwrap();
    let out = Command::new(bin)
        .args(["task", "need"])
        .env_remove("MUST_BE_SET")
        .current_dir(dir.path())
        .output()
        .expect("run");
    assert!(!out.status.success(), "required+unset must fail");
    assert!(
        String::from_utf8_lossy(&out.stderr).to_lowercase().contains("required"),
        "expected 'required' in stderr; got: {}",
        String::from_utf8_lossy(&out.stderr),
    );
}

#[test]
fn interactive_tasks_false_keeps_env_only_contract() {
    // `[settings] interactive_tasks = false` forces EnvResolver even if a TTY
    // were attached. Verifies the opt-out: env-provided value passes;
    // env-absent value fails fast. (We can't observe "no prompt" without a
    // TTY here, but the env path proves the resolver wiring honored the flag.)
    let bin = env!("CARGO_BIN_EXE_insmaller");
    let dir = tempfile::tempdir().unwrap();
    let marker = dir.path().join("ran").display().to_string().replace('\\', "/");
    let marker_step = write_marker_step(&marker);
    fs::write(
        dir.path().join("installer.toml"),
        format!(
            r#"[settings]
interactive_tasks = false

[task.t]
[[task.t.steps]]
type    = "prompt"
name    = "TOK"
required = true

[[task.t.steps]]
{marker_step}
"#
        ),
    )
    .unwrap();
    let out = Command::new(bin)
        .args(["task", "t"])
        .env("TOK", "ok")
        .current_dir(dir.path())
        .output()
        .expect("run");
    assert!(
        out.status.success(),
        "interactive_tasks=false + env-set value must pass\nstderr: {}",
        String::from_utf8_lossy(&out.stderr),
    );
    assert!(std::path::Path::new(&marker).exists());
}