lifeloop-cli 0.1.1

Provider-neutral lifecycle abstraction and normalizer for AI harnesses
Documentation
//! CLI integration tests for `lifeloop manifest list|show|inspect`.
//!
//! Issue #9: validate the host-lifecycle CLI surface end-to-end via
//! `std::process::Command` and `env!("CARGO_BIN_EXE_lifeloop")`,
//! mirroring the pattern in `tests/fake_client.rs`.

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

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

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 manifest_list_emits_every_registered_adapter() {
    let (code, stdout, stderr) = run(&["manifest", "list"]);
    assert_eq!(code, 0, "stderr=`{stderr}`");
    let v: serde_json::Value = serde_json::from_str(&stdout).expect("list output is JSON");
    let arr = v.as_array().expect("array");
    let ids: Vec<&str> = arr
        .iter()
        .map(|e| e.get("adapter_id").and_then(|s| s.as_str()).unwrap())
        .collect();
    for required in &[
        "codex", "claude", "hermes", "openclaw", "gemini", "opencode",
    ] {
        assert!(ids.contains(required), "missing `{required}` in {ids:?}");
    }
}

#[test]
fn manifest_show_returns_the_full_manifest_for_codex() {
    let (code, stdout, stderr) = run(&["manifest", "show", "codex"]);
    assert_eq!(code, 0, "stderr=`{stderr}`");
    let v: serde_json::Value = serde_json::from_str(&stdout).expect("manifest is JSON");
    assert_eq!(v.get("adapter_id").and_then(|s| s.as_str()), Some("codex"));
    assert!(v.get("integration_modes").is_some());
}

#[test]
fn manifest_show_unknown_id_is_validation_error() {
    let (code, _stdout, stderr) = run(&["manifest", "show", "nope"]);
    assert_eq!(code, 1, "expected validation exit (1), got {code}");
    assert!(stderr.contains("unknown adapter_id"));
}

#[test]
fn manifest_inspect_accepts_id_at_known_version() {
    // Find codex's actual registered version via `manifest show`, then
    // pin it for `inspect`. Avoids hard-coding the version string in
    // this test.
    let (_, show_stdout, _) = run(&["manifest", "show", "codex"]);
    let v: serde_json::Value = serde_json::from_str(&show_stdout).unwrap();
    let version = v.get("adapter_version").and_then(|s| s.as_str()).unwrap();
    let spec = format!("codex@{version}");
    let (code, stdout, stderr) = run(&["manifest", "inspect", &spec]);
    assert_eq!(code, 0, "stderr=`{stderr}`");
    let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
    assert_eq!(
        parsed.get("adapter_id").and_then(|s| s.as_str()),
        Some("codex")
    );
}

#[test]
fn manifest_inspect_rejects_wrong_version_with_validation_exit() {
    let (code, _stdout, stderr) = run(&["manifest", "inspect", "codex@99.99.99"]);
    assert_eq!(code, 1, "expected validation exit (1), got {code}");
    assert!(stderr.contains("not `99.99.99`") || stderr.contains("not `99.99.99"));
}

#[test]
fn manifest_inspect_missing_at_separator_is_usage_error() {
    let (code, _stdout, stderr) = run(&["manifest", "inspect", "codex"]);
    assert_eq!(code, 2, "expected usage exit (2), got {code}");
    assert!(stderr.contains("expected <adapter_id>@<version>"));
}

#[test]
fn manifest_unknown_subcommand_is_usage_error() {
    let (code, _stdout, stderr) = run(&["manifest", "bogus"]);
    assert_eq!(code, 2);
    assert!(stderr.contains("unknown subcommand"));
}