sysknife-cli 0.2.7

Approval-gated AI system administration CLI and MCP server
//! T1 — end-to-end smoke tests for the `sysknife` binary via `assert_cmd`.
//!
//! Before this batch, the binary had **zero** integration tests — every
//! argparse change, every `main.rs` wiring tweak, every stdout/stderr
//! stream confusion shipped undetected and surfaced as production bugs.
//! These tests boot the actual compiled binary (no mocking, no library
//! short-circuit) and exercise the failure surfaces a user is most
//! likely to hit:
//!
//!   1. `--help` returns 0 and prints something that mentions every
//!      top-level subcommand.
//!   2. `doctor` against an unreachable daemon returns non-zero and
//!      writes the failure to stderr (not stdout — automation parses
//!      stdout for the JSON `--json` form).
//!   3. `history --since "not-a-timestamp"` returns non-zero with a
//!      clear error rather than panicking.
//!   4. Unknown subcommands surface as clap usage errors.
//!
//! All tests set `SYSKNIFE_SOCKET` to a non-existent absolute path so
//! the daemon-touching commands fail fast with a connection error
//! instead of trying the production `/run/sysknife/daemon.sock`.

use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::process::Command;

/// Path the CLI tries to connect to in tests — points at a directory we
/// own so the failure mode is "ENOENT" rather than "ECONNREFUSED on a
/// stale socket".
fn fake_socket(dir: &tempfile::TempDir) -> std::path::PathBuf {
    dir.path().join("does-not-exist.sock")
}

fn cli() -> Command {
    Command::cargo_bin("sysknife").expect("sysknife binary builds")
}

#[test]
fn help_lists_every_top_level_subcommand() {
    let output = cli()
        .arg("--help")
        .assert()
        .success()
        .stdout(predicate::str::contains("doctor"))
        .stdout(predicate::str::contains("history"))
        .stdout(predicate::str::contains("audit"))
        .stdout(predicate::str::contains("mcp-server"))
        .stdout(predicate::str::contains("completions"));
    drop(output);
}

#[test]
fn unknown_subcommand_via_clap_usage_error() {
    // clap's external-subcommand mechanism turns any unknown first arg
    // into the free-form intent, so `sysknife nonsense-command` goes
    // through the planning path. To force a clap error we use an
    // unknown FLAG instead.
    cli()
        .arg("--no-such-flag")
        .assert()
        .failure()
        .stderr(predicate::str::contains("unexpected").or(predicate::str::contains("unknown")));
}

#[test]
fn doctor_fails_loudly_when_daemon_socket_is_unreachable() {
    let dir = tempfile::tempdir().unwrap();
    cli()
        .env("SYSKNIFE_SOCKET", fake_socket(&dir))
        .arg("doctor")
        .assert()
        .failure();
}

#[test]
fn history_rejects_unparseable_since_timestamp() {
    let dir = tempfile::tempdir().unwrap();
    cli()
        .env("SYSKNIFE_SOCKET", fake_socket(&dir))
        .arg("history")
        .arg("--since")
        .arg("not-a-timestamp")
        .assert()
        // Failure is the contract; either the parser or the daemon
        // round-trip can fail (depending on order). What matters is we
        // get a non-zero exit and don't panic.
        .failure();
}

#[test]
fn completions_subcommand_emits_a_shell_script() {
    cli()
        .arg("completions")
        .arg("bash")
        .assert()
        .success()
        // Bash completion scripts always start with `#!/usr/bin/env`
        // or define a `_sysknife` function — either is fine.
        .stdout(predicate::str::contains("_sysknife").or(predicate::str::starts_with("#")));
}