cellos-ctl 0.5.6

cellctl — kubectl-style CLI for CellOS execution cells and formations. Thin HTTP client over cellos-server with apply/get/describe/logs/events/webui.
Documentation
//! CTL-002 (E2E report) — cellctl detects UUID-vs-name and routes to
//! the right server-side path.
//!
//! Bug shape: `cellctl describe formation <name>` previously assembled
//! `/v1/formations/<name>` and got a 400 from the server's UUID
//! extractor. The fix is a client-side shape probe:
//!
//!   * UUID-parseable input → `/v1/formations/{uuid}` (existing route)
//!   * everything else      → `/v1/formations/by-name/{name}` (new route)
//!
//! This is the public-API pin for that shape probe. The hot path the
//! E2E report exercises is `describe formation <name>` and
//! `delete formation <name>`; both share the same routing helper
//! `cellos_ctl::client::formation_path` so we test it once at the
//! boundary.

use cellos_ctl::client::formation_path;

/// CTL-002 happy path: a kubectl-style name routes to `by-name`. This
/// is the regression the E2E report opened — `describe formation demo`
/// used to assemble `/v1/formations/demo` and hit the 400.
#[test]
fn name_routes_to_by_name_path() {
    assert_eq!(formation_path("demo"), "/v1/formations/by-name/demo");
}

/// CTL-002 compatibility pin: a real UUID still goes to the existing
/// `/v1/formations/{uuid}` route. Existing operator workflows
/// (`cellctl describe formation <uuid>`) MUST NOT regress. This is the
/// "no client breaking changes" property the directive calls out.
#[test]
fn uuid_routes_to_uuid_path() {
    // Canonical hyphenated v4 form.
    let id = "550e8400-e29b-41d4-a716-446655440000";
    assert_eq!(formation_path(id), format!("/v1/formations/{id}"));
}

/// Case-tolerant probe: `Uuid::parse_str` accepts uppercase per
/// RFC 4122 §3 and so does `axum`'s extractor. Pin that an operator
/// pasting an uppercase UUID still hits the UUID route, not by-name.
#[test]
fn uppercase_uuid_routes_to_uuid_path() {
    let id = "550E8400-E29B-41D4-A716-446655440000";
    assert_eq!(formation_path(id), format!("/v1/formations/{id}"));
}

/// Names with reserved URL characters (`/`, ` `, etc.) MUST be encoded
/// before they become path segments. Without encoding, a name like
/// `ns/team` would silently truncate to `ns` at the server's router.
#[test]
fn name_with_reserved_chars_does_not_escape_segment() {
    let got = formation_path("ns/team");
    assert!(
        got.starts_with("/v1/formations/by-name/"),
        "must hit by-name route, got {got}",
    );
    assert!(
        !got.contains("ns/team"),
        "reserved `/` must be encoded so it cannot escape the path segment; got {got}",
    );
}

/// Garbage that *looks* like a UUID but isn't (wrong length, wrong
/// hyphenation) MUST fall through to the by-name route — the by-name
/// route at the server returns a clean 404, which is the right
/// operator-UX answer ("no such name"). The bug was the 400
/// "UUID parsing failed" leaking through; this pins that it cannot
/// regress.
#[test]
fn near_uuid_but_invalid_routes_to_by_name() {
    let almost = "550e8400-e29b-41d4-a716-44665544000"; // 35 chars
    assert!(uuid::Uuid::parse_str(almost).is_err(), "precondition");
    assert!(
        formation_path(almost).starts_with("/v1/formations/by-name/"),
        "invalid-UUID input MUST route to by-name, not the UUID extractor"
    );
}