lifeloop-cli 0.1.0

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
//! CLI integration tests for `lifeloop conformance run` (issue #9).

use std::path::PathBuf;
use std::process::{Command, Stdio};

fn lifeloop_bin() -> std::path::PathBuf {
    std::path::PathBuf::from(env!("CARGO_BIN_EXE_lifeloop"))
}

fn conformance_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("conformance")
}

fn run(args: &[&str]) -> (i32, String, String) {
    let out = Command::new(lifeloop_bin())
        .args(args)
        .stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .output()
        .expect("spawn lifeloop");
    (
        out.status.code().unwrap_or(-1),
        String::from_utf8_lossy(&out.stdout).into_owned(),
        String::from_utf8_lossy(&out.stderr).into_owned(),
    )
}

#[test]
fn conformance_run_walks_full_corpus_and_emits_jsonl() {
    let root = conformance_root();
    let (code, stdout, stderr) = run(&["conformance", "run", "--root", root.to_str().unwrap()]);
    assert_eq!(code, 0, "stderr=`{stderr}`");
    // JSONL: every line is a valid JSON object with the expected keys.
    let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
    assert!(
        lines.len() >= 30,
        "expected many fixtures, got {}",
        lines.len()
    );
    for line in &lines {
        let v: serde_json::Value = serde_json::from_str(line).expect("each line is JSON");
        assert!(v.get("path").is_some(), "row missing path: {line}");
        assert!(v.get("kind").is_some(), "row missing kind: {line}");
        assert!(v.get("expect").is_some(), "row missing expect: {line}");
        assert!(v.get("outcome").is_some(), "row missing outcome: {line}");
    }
}

#[test]
fn conformance_run_summary_emits_one_object() {
    let root = conformance_root();
    let (code, stdout, stderr) = run(&[
        "conformance",
        "run",
        "--root",
        root.to_str().unwrap(),
        "--summary",
    ]);
    assert_eq!(code, 0, "stderr=`{stderr}`");
    let v: serde_json::Value = serde_json::from_str(&stdout).expect("summary is JSON");
    let total = v.get("total").and_then(|n| n.as_u64()).unwrap();
    let passed = v.get("passed").and_then(|n| n.as_u64()).unwrap();
    assert!(total > 0);
    assert_eq!(total, passed, "every fixture should have passed");
}

#[test]
fn conformance_run_missing_root_is_input_error() {
    let (code, _stdout, stderr) = run(&["conformance", "run", "--root", "/nonexistent/path/xyz"]);
    assert_eq!(code, 3, "expected input exit (3), got {code}");
    assert!(stderr.contains("not a directory"));
}

#[test]
fn conformance_run_unknown_flag_is_usage_error() {
    let (code, _stdout, stderr) = run(&["conformance", "run", "--bogus"]);
    assert_eq!(code, 2);
    assert!(stderr.contains("unknown flag"));
}