kittynode-cli 0.36.0

Control center for world computer operators.
use assert_cmd::Command;
use std::net::TcpListener;
use std::path::{Path, PathBuf};
use tempfile::tempdir;

#[test]
fn get_packages_outputs_known_package() {
    let mut cmd = Command::cargo_bin("kittynode").unwrap();
    let output = cmd
        .args(["package", "list"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let stdout = String::from_utf8_lossy(&output);
    assert!(
        stdout.contains("Ethereum"),
        "expected CLI output to list Ethereum package, got {stdout}"
    );
}

#[test]
fn get_config_outputs_readable_text() {
    let mut cmd = Command::cargo_bin("kittynode").unwrap();
    let output = cmd
        .args(["config", "show"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let stdout = String::from_utf8_lossy(&output);
    assert!(
        stdout.contains("Server URL:"),
        "expected config output to include Server URL, got {stdout}"
    );
}

#[test]
fn web_start_and_stop_roundtrip() {
    let temp_home = tempdir().expect("failed to create temp home directory");
    let sandbox = WebServiceSandbox::new(temp_home.path().to_path_buf());
    let port = find_free_port();

    let start_output = Command::cargo_bin("kittynode")
        .unwrap()
        .env("HOME", sandbox.home())
        .args(["web", "start", "--port", &port.to_string()])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let start_stdout = String::from_utf8_lossy(&start_output);
    assert!(
        start_stdout.contains("started"),
        "expected start output to mention service starting, got {start_stdout}"
    );
    assert!(
        sandbox.state_path().exists(),
        "expected web state file to exist after start"
    );

    let status_running = Command::cargo_bin("kittynode")
        .unwrap()
        .env("HOME", sandbox.home())
        .args(["web", "status"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let status_running_stdout = String::from_utf8_lossy(&status_running);
    assert!(
        status_running_stdout.to_lowercase().contains("running"),
        "expected status output to mention running state, got {status_running_stdout}"
    );

    let stop_output = Command::cargo_bin("kittynode")
        .unwrap()
        .env("HOME", sandbox.home())
        .args(["web", "stop"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let stop_stdout = String::from_utf8_lossy(&stop_output);
    assert!(
        stop_stdout.contains("stopped"),
        "expected stop output to mention service stopping, got {stop_stdout}"
    );
    assert!(
        !sandbox.state_path().exists(),
        "expected web state file to be removed after stop"
    );

    let status_stopped = Command::cargo_bin("kittynode")
        .unwrap()
        .env("HOME", sandbox.home())
        .args(["web", "status"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let status_stopped_stdout = String::from_utf8_lossy(&status_stopped);
    assert!(
        status_stopped_stdout.to_lowercase().contains("not running"),
        "expected status output to mention not running state, got {status_stopped_stdout}"
    );
}

fn find_free_port() -> u16 {
    TcpListener::bind("127.0.0.1:0")
        .expect("failed to bind to probe port")
        .local_addr()
        .expect("failed to read local address")
        .port()
}

struct WebServiceSandbox {
    home: PathBuf,
}

impl WebServiceSandbox {
    fn new(home: PathBuf) -> Self {
        Self { home }
    }

    fn home(&self) -> &Path {
        &self.home
    }

    fn state_path(&self) -> PathBuf {
        self.home
            .join(".config")
            .join("kittynode")
            .join("runtime")
            .join("kittynode-web.json")
    }

    fn stop(&self) {
        if let Ok(mut cmd) = Command::cargo_bin("kittynode") {
            let _ = cmd.env("HOME", self.home()).args(["web", "stop"]).output();
        }
    }
}

impl Drop for WebServiceSandbox {
    fn drop(&mut self) {
        self.stop();
    }
}