use predicates::prelude::PredicateBooleanExt;
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 describe_json_includes_typed_flag_schema() {
fez()
.args(["describe", "services.logs", "--json"])
.assert()
.success()
.stdout(contains("\"flag_schema\""))
.stdout(contains("\"name\":\"--lines\""))
.stdout(contains("\"type\":\"integer\""))
.stdout(contains("\"name\":\"--follow\""))
.stdout(contains("\"type\":\"boolean\""));
}
#[test]
fn describe_json_includes_output_schema_for_object_payloads() {
fez()
.args(["describe", "services.status", "--json"])
.assert()
.success()
.stdout(contains("\"output\""))
.stdout(contains("\"kind\":\"ServiceStatus\""))
.stdout(contains("\"schema\""))
.stdout(contains("\"id\":{\"type\":\"string\"}"))
.stdout(contains("\"active_state\":{\"type\":\"string\"}"))
.stdout(contains(
"\"required\":[\"id\",\"load_state\",\"active_state\",\"sub_state\"]",
))
.stdout(contains("\"error\""))
.stdout(contains("\"apiVersion\":{\"type\":\"string\"}"))
.stdout(contains(
"\"status\":{\"const\":\"error\",\"type\":\"string\"}",
))
.stdout(contains("\"code\":{\"type\":\"string\"}"));
}
#[test]
fn describe_json_includes_output_schema_for_table_payloads() {
fez()
.args(["describe", "packages.list", "--json"])
.assert()
.success()
.stdout(contains("\"output\""))
.stdout(contains("\"kind\":\"PackageList\""))
.stdout(contains(
"\"const\":[\"name\",\"evr\",\"arch\",\"repo_id\",\"install_size\",\"summary\"]",
))
.stdout(contains("\"prefixItems\":[{\"type\":\"string\"}"))
.stdout(contains("{\"type\":\"integer\"}"))
.stdout(contains("\"count\":{\"type\":\"integer\"}"));
}
#[test]
fn describe_json_documents_dry_run_alternate_outputs() {
fez()
.args(["describe", "services.start", "--json"])
.assert()
.success()
.stdout(contains("\"alternates\""))
.stdout(contains("\"kind\":\"DryRun\""))
.stdout(contains("\"command\":{\"type\":\"string\"}"));
fez()
.args(["describe", "packages.install", "--json"])
.assert()
.success()
.stdout(contains("\"alternates\""))
.stdout(contains("\"kind\":\"PackagePlan\""))
.stdout(contains("\"dry_run\":{\"type\":\"boolean\"}"));
}
#[test]
fn describe_json_marks_repeatable_and_conflicting_flags() {
fez()
.args(["describe", "packages.list", "--json"])
.assert()
.success()
.stdout(contains("\"name\":\"--repo\""))
.stdout(contains("\"repeatable\":true"))
.stdout(contains("\"name\":\"--installed\""))
.stdout(contains("\"conflicts_with\":[\"--available\"]"));
fez()
.args(["describe", "packages.repolist", "--json"])
.assert()
.success()
.stdout(contains("\"name\":\"--enabled\""))
.stdout(contains("\"conflicts_with\":[\"--disabled\",\"--all\"]"));
}
#[test]
fn describe_json_includes_input_choices() {
fez()
.args(["describe", "firewall.panic", "--json"])
.assert()
.success()
.stdout(contains("\"name\":\"state\""))
.stdout(contains("\"choices\":[\"on\",\"off\"]"));
}
#[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 mcp_help_lists_expanded_tools_flag() {
fez()
.args(["mcp", "--help"])
.assert()
.success()
.stdout(contains("--expanded-tools"));
}
#[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_text_includes_privileged_output_inputs_flags() {
fez()
.args(["describe", "services.start"])
.assert()
.success()
.stdout(contains("privileged: true"))
.stdout(contains("output: ServiceMutation"))
.stdout(contains("inputs:"))
.stdout(contains("unit: string required"))
.stdout(contains("flags:"))
.stdout(contains("--force"))
.stdout(contains("--dry-run"));
}
#[test]
fn describe_text_marks_readonly_not_privileged() {
fez()
.args(["describe", "services.list"])
.assert()
.success()
.stdout(contains("privileged: false"))
.stdout(contains("output: ServiceList"));
}
#[test]
fn packages_list_help_and_descriptor_document_pagination_filters() {
fez()
.args(["packages", "list", "--help"])
.assert()
.success()
.stdout(contains("--limit"))
.stdout(contains("--offset"))
.stdout(contains("--name"));
fez()
.args(["describe", "packages.list", "--json"])
.assert()
.success()
.stdout(contains("--limit"))
.stdout(contains("--offset"))
.stdout(contains("--name"));
}
#[test]
fn services_start_help_shows_examples_and_long() {
fez()
.args(["services", "start", "--help"])
.assert()
.success()
.stdout(contains("Examples:"))
.stdout(contains("--force"));
}
#[test]
fn global_force_help_not_systemd_specific() {
fez()
.arg("--help")
.assert()
.success()
.stdout(contains("--force"))
.stdout(contains("Override command-specific safety guardrails"))
.stdout(contains("protected-unit policy").not());
}
#[test]
fn services_start_help_keeps_protected_unit_wording() {
fez()
.args(["services", "start", "--help"])
.assert()
.success()
.stdout(contains("Protected units"));
}
#[test]
fn readonly_command_help_hides_force_and_dry_run() {
for path in [
["network", "list"],
["packages", "info"],
["firewall", "status"],
["services", "status"],
] {
fez()
.args([path[0], path[1], "--help"])
.assert()
.success()
.stdout(contains("--force").not())
.stdout(contains("--dry-run").not());
}
}
#[test]
fn mutating_command_help_keeps_force_and_dry_run() {
fez()
.args(["services", "start", "--help"])
.assert()
.success()
.stdout(contains("--force"))
.stdout(contains("--dry-run"));
fez()
.args(["packages", "install", "--help"])
.assert()
.success()
.stdout(contains("--force"))
.stdout(contains("--dry-run"));
}
#[test]
fn hidden_global_flag_still_parses_on_readonly() {
let out = fez()
.args(["network", "list", "--force", "--json"])
.output()
.expect("run");
assert_ne!(
out.status.code(),
Some(2),
"read-only command rejected a hidden global flag as a usage error: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn help_flags_match_descriptor_flags() {
let cmd = fez::cli::command();
for d in fez::capability::registry() {
let parts: Vec<&str> = d.id.split('.').collect();
let mut node = &cmd;
for p in &parts {
node = node
.get_subcommands()
.find(|c| c.get_name() == *p)
.unwrap_or_else(|| panic!("no subcommand for {}", d.id));
}
for arg in node.get_arguments() {
let long = match arg.get_long() {
Some(l) => format!("--{l}"),
None => continue,
};
if long != "--force" && long != "--dry-run" {
continue;
}
let advertised = d.flags.iter().any(|f| f == &long);
let hidden = arg.is_hide_set();
assert_eq!(
hidden, !advertised,
"{}: {long} hidden={hidden} but descriptor advertised={advertised}",
d.id
);
}
}
}
#[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() {
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() {
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]));
}
#[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");
{
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"
);
}
fn tokenize_example(s: &str) -> Vec<String> {
let mut out = Vec::new();
let mut cur = String::new();
let mut in_quote = false;
let mut has_token = false;
for c in s.chars() {
match c {
'\'' => {
in_quote = !in_quote;
has_token = true;
}
ch if ch.is_whitespace() && !in_quote => {
if has_token {
out.push(std::mem::take(&mut cur));
has_token = false;
}
}
ch => {
cur.push(ch);
has_token = true;
}
}
}
if has_token {
out.push(cur);
}
out
}
#[test]
fn every_descriptor_example_parses() {
let cmd = fez::cli::command();
for d in fez::capability::registry() {
for example in &d.examples {
let argv = tokenize_example(example);
assert_eq!(
argv.first().map(String::as_str),
Some("fez"),
"example for {} does not start with `fez`: {example}",
d.id
);
let res = cmd.clone().try_get_matches_from(&argv);
assert!(
res.is_ok(),
"example for {} fails to parse: `{example}`\n {}",
d.id,
res.unwrap_err()
);
}
}
}
#[test]
fn json_missing_required_arg_emits_envelope() {
fez()
.args(["--json", "services", "status"])
.assert()
.code(2)
.stdout(contains("\"apiVersion\":\"fez/v1\""))
.stdout(contains("\"kind\":\"Error\""))
.stdout(contains("\"status\":\"error\""))
.stdout(contains("\"code\":\"usage\""))
.stderr(contains("required arguments").not());
}
#[test]
fn json_after_subcommand_still_emits_envelope() {
fez()
.args(["services", "status", "--json"])
.assert()
.code(2)
.stdout(contains("\"apiVersion\":\"fez/v1\""))
.stdout(contains("\"kind\":\"Error\""))
.stdout(contains("\"status\":\"error\""))
.stdout(contains("\"code\":\"usage\""));
}
#[test]
fn json_unknown_flag_emits_envelope() {
fez()
.args(["services", "list", "--json", "--bogus"])
.assert()
.code(2)
.stdout(contains("\"kind\":\"Error\""))
.stdout(contains("\"code\":\"usage\""));
}
#[test]
fn json_unknown_capability_emits_envelope() {
fez()
.args(["describe", "nope.nope", "--json"])
.assert()
.code(4)
.stdout(contains("\"kind\":\"Error\""))
.stdout(contains("\"code\":\"not-found\""))
.stdout(contains("nope.nope"));
}
#[test]
fn plain_missing_arg_keeps_clap_stderr() {
fez()
.args(["services", "status"])
.assert()
.code(2)
.stderr(contains("required arguments"));
}
#[test]
fn plain_unknown_capability_keeps_stderr() {
fez()
.args(["describe", "nope.nope"])
.assert()
.code(4)
.stderr(contains("unknown capability"));
}
#[test]
fn json_help_and_version_still_succeed() {
fez().args(["--json", "--help"]).assert().success();
fez().args(["--json", "--version"]).assert().success();
}