rusty-fez 0.3.0

Agent-native management CLI for Fedora/RHEL (drives cockpit-bridge)
Documentation
use predicates::str::contains;

mod common;
use common::fez_plain as fez;

#[test]
fn prints_version() {
    fez()
        .arg("--version")
        .assert()
        .success()
        .stdout(contains("fez"));
}

#[test]
fn help_lists_command_groups() {
    fez()
        .arg("--help")
        .assert()
        .success()
        .stdout(contains("services"))
        .stdout(contains("capabilities"))
        .stdout(contains("describe"));
}

#[test]
fn global_flags_present() {
    fez()
        .args(["services", "list", "--help"])
        .assert()
        .success()
        .stdout(contains("--host"))
        .stdout(contains("--json"));
}

#[test]
fn capabilities_lists_service_ids() {
    fez()
        .arg("capabilities")
        .assert()
        .success()
        .stdout(contains("services.list"))
        .stdout(contains("services.logs"));
}

#[test]
fn describe_emits_envelope_json() {
    fez()
        .args(["describe", "services.status", "--json"])
        .assert()
        .success()
        .stdout(contains("\"apiVersion\":\"fez/v1\""))
        .stdout(contains("ServiceStatus"));
}

#[test]
fn describe_unknown_exits_4() {
    fez().args(["describe", "nope"]).assert().code(4);
}

#[test]
fn capabilities_lists_mutation_ids() {
    fez()
        .arg("capabilities")
        .assert()
        .success()
        .stdout(contains("services.start"))
        .stdout(contains("services.stop"))
        .stdout(contains("services.restart"))
        .stdout(contains("services.reload"))
        .stdout(contains("services.enable"))
        .stdout(contains("services.disable"));
}

#[test]
fn describe_start_is_privileged() {
    fez()
        .args(["describe", "services.start", "--json"])
        .assert()
        .success()
        .stdout(contains("\"privileged\":true"))
        .stdout(contains("\"output_kind\":\"ServiceMutation\""))
        .stdout(contains("--dry-run"))
        .stdout(contains("--force"));
}

#[test]
fn describe_enable_lists_now_flag() {
    fez()
        .args(["describe", "services.enable", "--json"])
        .assert()
        .success()
        .stdout(contains("\"output_kind\":\"ServiceEnablement\""))
        .stdout(contains("--now"));
}

#[test]
fn services_help_lists_mutation_verbs() {
    fez()
        .args(["services", "--help"])
        .assert()
        .success()
        .stdout(contains("start"))
        .stdout(contains("enable"));
}

#[test]
fn help_lists_mcp_subcommand() {
    fez()
        .arg("--help")
        .assert()
        .success()
        .stdout(contains("mcp"));
}

#[test]
fn capabilities_json_emits_envelope() {
    fez()
        .args(["capabilities", "--json"])
        .assert()
        .success()
        .stdout(contains("\"apiVersion\":\"fez/v1\""))
        .stdout(contains("CapabilityList"))
        .stdout(contains("services.list"));
}

#[test]
fn describe_human_output_includes_example() {
    fez()
        .args(["describe", "services.status"])
        .assert()
        .success()
        .stdout(contains("services.status"))
        .stdout(contains("examples:"))
        .stdout(contains("fez services status"));
}

#[test]
fn describe_unknown_json_still_exits_4() {
    fez()
        .args(["describe", "nope", "--json"])
        .assert()
        .code(4)
        .stderr(contains("unknown capability"));
}

#[test]
fn services_start_help_shows_examples_and_long() {
    fez()
        .args(["services", "start", "--help"])
        .assert()
        .success()
        .stdout(contains("Examples:"))
        .stdout(contains("--force"));
}

#[test]
fn guide_text_mentions_discovery_loop_and_exit_codes() {
    fez()
        .arg("guide")
        .assert()
        .success()
        .stdout(contains("capabilities"))
        .stdout(contains("describe"))
        .stdout(contains("protected-unit"))
        .stdout(contains("fez/v1"));
}

#[test]
fn guide_json_emits_agent_guide_envelope() {
    fez()
        .args(["guide", "--json"])
        .assert()
        .success()
        .stdout(contains("\"apiVersion\":\"fez/v1\""))
        .stdout(contains("AgentGuide"))
        .stdout(contains("exitCodes"));
}

#[test]
fn describe_text_shows_long_and_all_examples() {
    fez()
        .args(["describe", "services.enable"])
        .assert()
        .success()
        .stdout(contains("--now"))
        .stdout(contains("boot"));
}

#[test]
fn completions_bash_emits_script() {
    fez()
        .args(["completions", "bash"])
        .assert()
        .success()
        .stdout(contains("_fez"));
}

#[test]
fn man_emits_roff() {
    fez()
        .arg("man")
        .assert()
        .success()
        .stdout(contains(".TH"))
        .stdout(contains("fez"));
}

#[test]
fn every_capability_id_has_a_clap_path() {
    // Each dotted id maps to a real subcommand path under the enriched command.
    let cmd = fez::cli::command();
    for d in fez::capability::registry() {
        let parts: Vec<&str> = d.id.split('.').collect();
        let mut node = &cmd;
        let mut found = true;
        for p in &parts {
            match node.get_subcommands().find(|c| c.get_name() == *p) {
                Some(c) => node = c,
                None => {
                    found = false;
                    break;
                }
            }
        }
        assert!(found, "capability {} has no clap path", d.id);
    }
}

#[test]
fn describe_example_matches_help_after_help() {
    // describe and --help both read the registry, so the first example must
    // appear in the rendered after_help for that command.
    let cmd = fez::cli::command();
    let d = fez::capability::find("services.start").unwrap();
    let services = cmd
        .get_subcommands()
        .find(|c| c.get_name() == "services")
        .unwrap();
    let start = services
        .get_subcommands()
        .find(|c| c.get_name() == "start")
        .unwrap();
    let after = start.get_after_help().unwrap().to_string();
    assert!(after.contains(&d.examples[0]));
}

// Regression for the BrokenPipe panic (issue #20): when a downstream reader
// closes the pipe early, fez must not panic (exit 101) or print a panic
// backtrace. With SIGPIPE reset to SIG_DFL it dies quietly via the signal, the
// conventional Unix behavior. We spawn the real binary, read a single byte from
// its stdout, then drop the read end so the rest of the write hits EPIPE.
#[cfg(unix)]
#[test]
fn broken_pipe_does_not_panic() {
    use std::io::Read;
    use std::process::{Command as StdCommand, Stdio};

    let exe = assert_cmd::cargo::cargo_bin("fez");
    let mut child = StdCommand::new(exe)
        .args(["completions", "zsh"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn fez");

    // Read one byte, then drop stdout to close the read end of the pipe.
    {
        let mut stdout = child.stdout.take().expect("child stdout");
        let mut one = [0u8; 1];
        let _ = stdout.read(&mut one);
    }

    let output = child.wait_with_output().expect("wait for fez");
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        !stderr.contains("panicked"),
        "fez panicked on broken pipe: {stderr}"
    );
    assert_ne!(
        output.status.code(),
        Some(101),
        "fez exited 101 (Rust panic) on broken pipe"
    );
}