rustfs-cli 0.1.36

A Rust S3 CLI client for S3-compatible object storage
Documentation
#![cfg(not(windows))]

mod admin_support;

use std::process::{Command, Output};
use std::time::Duration;

use admin_support::{rc_binary, rc_host_alias, start_admin_test_server};
use serde_json::Value;

fn run_status(response: &'static str, json: bool, token: bool) -> Output {
    let config_dir = tempfile::tempdir().expect("create isolated config");
    let (endpoint, receiver, handle) = start_admin_test_server(response);
    let mut command = Command::new(rc_binary());
    command.arg("--no-color");
    if json {
        command.arg("--json");
    }
    command.args(["admin", "heal", "status", "myalias"]);
    if token {
        command.args(["--client-token", "25080000-0000-4000-8000-000000000001"]);
    }
    let output = command
        .env("RC_CONFIG_DIR", config_dir.path())
        .env("RC_HOST_myalias", rc_host_alias(&endpoint))
        .env("NO_PROXY", "127.0.0.1,localhost")
        .env("no_proxy", "127.0.0.1,localhost")
        .output()
        .expect("run heal status");
    assert_eq!(
        output.status.code(),
        Some(0),
        "status lookup must retain its exit contract: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let request = receiver
        .recv_timeout(Duration::from_secs(5))
        .expect("capture status request");
    assert_eq!(request.method, "POST");
    assert_eq!(
        request.target,
        if token {
            "/rustfs/admin/v3/heal/?clientToken=25080000-0000-4000-8000-000000000001"
        } else {
            "/rustfs/admin/v3/background-heal/status"
        }
    );
    handle.join().expect("status server finished");
    output
}

fn status_outputs(response: &'static str, token: bool) -> (Value, String, String) {
    let json_output = run_status(response, true, token);
    assert!(json_output.stderr.is_empty());
    let json: Value = serde_json::from_slice(&json_output.stdout).expect("decode status output");
    let schema: Value = serde_json::from_str(include_str!("../../../schemas/output_v2.json"))
        .expect("decode output schema");
    let validator = jsonschema::validator_for(&serde_json::json!({
        "$ref": "#/definitions/healStatus",
        "definitions": schema["definitions"],
    }))
    .expect("compile heal status schema");
    assert!(validator.is_valid(&json), "status violates schema: {json}");
    let text_output = run_status(response, false, token);
    (
        json,
        String::from_utf8(text_output.stdout).expect("status text"),
        String::from_utf8(text_output.stderr).expect("status warnings"),
    )
}

#[test]
fn degraded_status_preserves_partial_coverage_in_json_and_text() {
    let (json, text, warning) = status_outputs(
        r#"{"state":"degraded","clusterStatusComplete":false,"coverage":{"expected":4,"responded":3,"unknown":1,"reasons":["peer_status_unavailable"]}}"#,
        false,
    );
    assert_eq!(json["state"], "degraded");
    assert_eq!(json["healing"], false);
    assert_eq!(json["clusterStatusComplete"], false);
    assert_eq!(json["coverage"]["expected"], 4);
    assert_eq!(json["coverage"]["responded"], 3);
    assert_eq!(json["coverage"]["unknown"], 1);
    assert_eq!(json["coverage"]["reasons"][0], "peer_status_unavailable");
    assert!(text.contains("Heal Status: Degraded"));
    assert!(text.contains("3/4 responded"));
    assert!(text.contains("1 unknown"));
    assert!(text.contains("peer_status_unavailable"));
    assert!(warning.contains("incomplete"));
    assert!(!text.contains("Idle"));
    assert!(!text.contains("No active heal operation"));
}

#[test]
fn explicit_unknown_and_future_states_never_become_idle() {
    for response in [
        r#"{"state":"unknown"}"#,
        r#"{"state":"future_state"}"#,
        r#"{"state":"future_state","currentScanMode":2,"bitrotStartTime":"2026-09-13T13:00:00Z"}"#,
    ] {
        let (json, text, warning) = status_outputs(response, false);
        assert_eq!(json["state"], "unknown");
        assert_eq!(json["healing"], false);
        assert!(text.contains("Heal Status: Unknown"));
        assert!(warning.contains("unknown"));
        assert!(!text.contains("Idle"));
        assert!(!text.contains("Finished"));
        assert!(!text.contains("No active heal operation"));
        assert!(json.get("clusterStatusComplete").is_none());
        assert!(json.get("coverage").is_none());
    }
}

#[test]
fn unknown_runtime_retains_observed_tasks_without_claiming_a_known_state() {
    let (json, text, warning) = status_outputs(
        r#"{"state":"future_state","healOperations":{"queueLength":2,"activeTasks":1}}"#,
        false,
    );
    assert_eq!(json["state"], "unknown");
    assert_eq!(json["healing"], true);
    assert_eq!(json["healQueueLength"], 2);
    assert_eq!(json["healActiveTasks"], 1);
    assert!(text.contains("Heal Status: Unknown"));
    assert!(text.contains("2 queued, 1 active"));
    assert!(warning.contains("unknown"));
    assert!(!text.contains("No active heal operation"));
}

#[test]
fn active_partial_status_keeps_activity_and_incomplete_warning() {
    let (json, text, warning) = status_outputs(
        r#"{"state":"active","clusterStatusComplete":false,"coverage":{"expected":4,"responded":3,"unknown":1,"reasons":["future_reason"]}}"#,
        false,
    );
    assert_eq!(json["state"], "active");
    assert_eq!(json["healing"], true);
    assert_eq!(json["clusterStatusComplete"], false);
    assert_eq!(json["coverage"]["reasons"][0], "future_reason");
    assert!(text.contains("Heal Status: In Progress"));
    assert!(text.contains("3/4 responded"));
    assert!(text.contains("future_reason"));
    assert!(warning.contains("incomplete"));
}

#[test]
fn complete_idle_and_runtime_unavailability_remain_distinct() {
    for (response, state, label) in [
        (
            r#"{"state":"idle","clusterStatusComplete":true,"coverage":{"expected":4,"responded":4,"unknown":0,"reasons":[]}}"#,
            "idle",
            "Idle",
        ),
        (
            r#"{"state":"disabled","clusterStatusComplete":true,"coverage":{"expected":4,"responded":4,"unknown":0,"reasons":[]}}"#,
            "disabled",
            "Disabled",
        ),
        (
            r#"{"state":"uninitialized","clusterStatusComplete":true,"coverage":{"expected":4,"responded":4,"unknown":0,"reasons":[]}}"#,
            "uninitialized",
            "Uninitialized",
        ),
    ] {
        let (json, text, warning) = status_outputs(response, false);
        assert_eq!(json["state"], state);
        assert_eq!(json["healing"], false);
        assert_eq!(json["clusterStatusComplete"], true);
        assert_eq!(json["coverage"]["unknown"], 0);
        assert!(text.contains(&format!("Heal Status: {label}")));
        assert!(text.contains("4/4 responded"));
        assert!(warning.is_empty());
        if state != "idle" {
            assert!(!text.contains("No active heal operation"));
        }
    }
}

#[test]
fn missing_coverage_counts_stay_unknown_and_legacy_state_still_works() {
    let (json, text, warning) = status_outputs(r#"{"state":"degraded","coverage":{}}"#, false);
    assert!(json.get("clusterStatusComplete").is_none());
    for count in ["expected", "responded", "unknown"] {
        assert!(json["coverage"].get(count).is_none());
    }
    assert!(text.contains("?/? responded"));
    assert!(warning.contains("incomplete"));

    for (response, healing, label) in [
        (r#"{}"#, false, "Idle"),
        (r#"{"healQueueLength":1}"#, true, "In Progress"),
        (r#"{"currentScanMode":2}"#, true, "In Progress"),
    ] {
        let (json, text, _) = status_outputs(response, false);
        assert!(json.get("state").is_none());
        assert!(json.get("clusterStatusComplete").is_none());
        assert!(json.get("coverage").is_none());
        assert_eq!(json["healing"], healing);
        assert!(text.contains(&format!("Heal Status: {label}")));
        assert!(text.contains("Cluster status: Unknown"));
    }
}

#[test]
fn coverage_reasons_preserve_json_but_cannot_inject_terminal_lines() {
    let (json, text, warning) = status_outputs(
        r#"{"state":"degraded","coverage":{"reasons":["future\nforged_line\u001b[2J"]}}"#,
        false,
    );
    assert_eq!(
        json["coverage"]["reasons"][0],
        "future\nforged_line\u{1b}[2J"
    );
    assert!(!text.contains('\u{1b}'));
    assert!(!text.lines().any(|line| line.starts_with("forged_line")));
    assert!(warning.contains("incomplete"));
}

#[test]
fn explicit_incompleteness_cannot_fall_back_to_legacy_idle() {
    for response in [
        r#"{"clusterStatusComplete":false}"#,
        r#"{"state":"idle","clusterStatusComplete":false}"#,
        r#"{"state":"idle","coverage":{"expected":4,"responded":3}}"#,
        r#"{"state":"idle","coverage":{"unknown":1}}"#,
        r#"{"state":"idle","coverage":{"reasons":["peer_status_unavailable"]}}"#,
    ] {
        let (_, text, warning) = status_outputs(response, false);
        assert!(text.contains("Heal Status: Unknown"));
        assert!(warning.contains("incomplete"));
        assert!(!text.contains("No active heal operation"));
    }
}

#[test]
fn token_status_preserves_legacy_summaries_and_terminal_error_detail() {
    for (response, summary, label, healing) in [
        (r#"{"summary":"running"}"#, "running", "In Progress", true),
        (r#"{"summary":"finished"}"#, "finished", "Finished", false),
        (r#"{"summary":"stopped"}"#, "stopped", "Stopped", false),
        (r#"{"summary":"notFound"}"#, "notFound", "Not Found", false),
        (
            r#"{"summary":"stopped","detail":"heal traversal completed with errors: 1 failed objects","progress":{"objectsScanned":1,"objectsFailed":1},"outcome":{"execution":{"state":"completed_with_errors"}}}"#,
            "stopped",
            "Stopped",
            false,
        ),
    ] {
        let (json, text, warning) = status_outputs(response, true);
        assert_eq!(json["summary"], summary);
        assert_eq!(json["healing"], healing);
        assert!(json.get("state").is_none());
        assert!(json.get("clusterStatusComplete").is_none());
        assert!(json.get("coverage").is_none());
        assert!(text.contains(&format!("Heal Status: {label}")));
        assert!(!text.contains("Cluster status:"));
        assert!(warning.is_empty());
        if response.contains("completed_with_errors") {
            assert_eq!(json["itemsFailed"], 1);
            assert!(
                json["detail"]
                    .as_str()
                    .unwrap()
                    .contains("completed with errors")
            );
            assert!(text.contains("completed with errors"));
        }
    }
}