taba-cli 0.1.0

The Taba client command line.
Documentation
use super::*;

const STATUS: &str = r#"{"collectedAt":"2026-10-15T12:00:00.000Z","partial":true,
 "missingNodes":[{"nodeId":"n3","reason":"timeout"}],"worstCondition":"member-unreachable",
 "conditions":[{"condition":"member-unreachable","severity":"warning"},{"condition":"quorum-lost","severity":"critical"}],
 "summary":{"members":{"active":2,"syncing":1},"membersVersion":7,"quorumAvailable":true,"syncingBlocks":4},
 "nodes":[
  {"nodeId":"n1","reachable":true,"status":{"version":"1.2.3","mode":"production","state":"active","drainState":"active",
    "startedAt":"2026-10-15T10:00:00.000Z",
    "storage":{"memoryUsedBytes":512,"memoryLimitBytes":1024,"memoryMessages":10,"diskMessages":20,"diskFreeBytes":9999},
    "signals":{"membersVersion":7,"quorumAvailable":true,"channels":2,"syncingBlocks":1,"blocksBelowReplication":3,"blocksBelowQuorum":0,"frozenNodes":0},
    "runtime":{"activeRequests":2.0,"rateLimitRejected":5.0,"channelRttP95Seconds":0.012}}},
  {"nodeId":"n3","reachable":false,"status":null}]}"#;

/// TUI: TUI-CLU-01
#[test]
fn cluster_status_is_read_with_conditions_nodes_and_the_partial_envelope() {
    let s = parse_cluster_status(STATUS).unwrap();
    assert!(s.partial);
    assert_eq!(
        s.missing,
        vec![Missing {
            node_id: "n3".into(),
            reason: "timeout".into()
        }]
    );
    assert_eq!(s.conditions.len(), 2);
    assert_eq!(s.conditions[1].severity, Severity::Critical);
    assert_eq!(s.members, vec![("active".into(), 2), ("syncing".into(), 1)]);
    assert_eq!(
        (s.members_version, s.quorum, s.syncing_blocks),
        (7, true, 4)
    );
    assert_eq!(s.nodes.len(), 2);
}

/// TUI: TUI-CLU-03
#[test]
fn a_node_status_carries_storage_signals_and_runtime() {
    let n = &parse_cluster_status(STATUS).unwrap().nodes[0];
    assert_eq!((n.memory_used, n.memory_limit), (512, 1024));
    assert_eq!((n.memory_messages, n.disk_messages), (10, 20));
    assert_eq!(n.below_replication, 3);
    assert_eq!((n.active_requests, n.rate_limited), (2.0, 5.0));
    assert_eq!(n.rtt_p95, Some(0.012));
    assert!((n.memory_ratio() - 0.5).abs() < 1e-9);
}

/// TUI: TUI-CLU-03
#[test]
fn an_unreachable_node_has_no_status() {
    let n = &parse_cluster_status(STATUS).unwrap().nodes[1];
    assert!(!n.reachable);
    assert_eq!(n.memory_limit, 0);
    assert_eq!(n.memory_ratio(), 0.0);
}

/// TUI: TUI-CLU-07
#[test]
fn the_node_list_is_read_without_addresses() {
    let nodes = parse_nodes(
        r#"{"partial":false,"missingNodes":[],"items":[
        {"nodeId":"a","self":true,"reachable":true,"state":"active","drainState":"active","version":"1"},
        {"nodeId":"b","self":false,"reachable":true,"state":"drained","drainState":"drained","version":"1"}]}"#,
    )
    .unwrap();
    assert_eq!(nodes.items.len(), 2);
    assert!(nodes.items[0].is_self);
    assert_eq!(nodes.items[1].state, "drained");
    assert!(!nodes.partial);
}

/// TUI: TUI-FRM-10
#[test]
fn me_says_what_the_token_can_do() {
    let me = parse_me(
        r#"{"authEnabled":true,"root":false,"name":"app","permissions":["publish","consume"]}"#,
    )
    .unwrap();
    assert!(!me.is_root());
    assert!(me.can("publish") && !me.can("admin") && !me.can("manage") && !me.can("create"));
    assert!(!me.is_admin());
    let manager =
        parse_me(r#"{"authEnabled":true,"root":false,"permissions":["manage"]}"#).unwrap();
    assert!(
        manager.can("manage")
            && manager.can("create")
            && manager.can("publish")
            && manager.can("consume")
            && !manager.can("admin")
            && !manager.is_admin()
    );
    let admin = parse_me(r#"{"authEnabled":true,"root":false,"permissions":["admin"]}"#).unwrap();
    assert!(admin.is_admin() && !admin.is_root());
    assert!(
        admin.can("admin")
            && admin.can("manage")
            && admin.can("create")
            && admin.can("publish")
            && admin.can("consume")
    );
    let open = parse_me(r#"{"authEnabled":false,"root":false,"permissions":[]}"#).unwrap();
    assert!(open.is_root() && open.can("admin"));
}

/// TUI: TUI-NOD-03
#[test]
fn drain_status_is_read() {
    let d = parse_drain(
        r#"{"drainState":"draining","trigger":"api","startedAt":null,"pendingMessages":13}"#,
    )
    .unwrap();
    assert_eq!((d.state.as_str(), d.pending), ("draining", 13));
    assert!(!d.is_active() && !d.is_done());
}

/// TUI: TUI-NOD-04
#[test]
fn a_cluster_flush_lists_each_node_and_a_node_flush_is_one_entry() {
    let r = parse_flush(
        r#"{"complete":false,"nodes":[{"nodeId":"a","complete":true,"outboxPending":0},
        {"nodeId":"b","complete":false,"pending":true,"error":"timeout"}]}"#,
    )
    .unwrap();
    assert!(!r.complete);
    assert_eq!(r.nodes.len(), 2);
    assert_eq!(r.nodes[1].error.as_deref(), Some("timeout"));
    let one = parse_flush(r#"{"complete":true,"memoryRemaining":0,"timedOut":false,"nodeId":"a"}"#)
        .unwrap();
    assert_eq!(one.nodes.len(), 1);
    assert!(one.complete && one.nodes[0].complete);
}

/// TUI: TUI-NOD-05
#[test]
fn a_stop_report_lists_nodes_and_warnings() {
    let r = parse_stop(
        r#"{"stopped":["a","b"],"stopList":false,"warnings":["S3 down"],"nodes":[{"nodeId":"a","drained":true,"outboxPending":0}]}"#,
    )
    .unwrap();
    assert_eq!(r.stopped, vec!["a", "b"]);
    assert!(!r.stop_list);
    assert_eq!(r.warnings, vec!["S3 down"]);
    assert_eq!(r.nodes, vec![("a".to_owned(), true)]);
}

/// TUI: TUI-CLU-05
#[test]
fn each_call_maps_to_its_route_and_node_ids_are_encoded() {
    let c = ApiCall::DrainStart("n/1".into()).to_call();
    assert_eq!((c.method, c.path.as_str()), ("POST", "/nodes/n%2F1/drain"));
    let c = ApiCall::DrainCancel("a".into()).to_call();
    assert_eq!((c.method, c.path.as_str()), ("DELETE", "/nodes/a/drain"));
    assert_eq!(
        ApiCall::FlushCluster.to_call().path,
        "/cluster/backup/flush"
    );
    assert_eq!(ApiCall::StopCluster.to_call().path, "/cluster/stop");
    assert!(ApiCall::StopCluster.is_action() && !ApiCall::ClusterStatus.is_action());
}

/// TUI: TUI-NOD-02
#[test]
fn a_backup_is_read_leniently() {
    let b = parse_backup(
        r#"{"s3Reachable":true,"outboxPending":4,"lastSuccessAt":"2026-10-15T10:00:00Z"}"#,
    )
    .unwrap();
    assert_eq!((b.reachable, b.pending), (Some(true), Some(4)));
    assert!(b.last_success.is_some());
}

#[test]
fn bad_json_is_an_error_not_a_panic() {
    assert!(parse_cluster_status("<html>").is_err());
    assert!(parse_nodes("").is_err());
}

/// TUI: TUI-NOD-01
#[test]
fn what_a_real_development_node_answers_is_read() {
    // Copied from a node in development mode: no memory limit, channels as a map, a null round trip.
    let text = r#"{"collectedAt":"2026-10-15T20:21:14.532Z","conditions":[],"missingNodes":[],"nodes":[{"nodeId":"01a14139","reachable":true,"status":{"collectedAt":"x","drainState":"active","mode":"development","nodeId":"01a14139","runtime":{"activeRequests":1.0,"channelRttP95Seconds":null,"rateLimitRejected":0.0},"signals":{"blocksBelowQuorum":0,"blocksBelowReplication":0,"channels":{"a":1,"b":2},"frozenNodes":0,"membersVersion":0,"quorumAvailable":true,"syncingBlocks":0},"startedAt":"2026-10-15T20:21:08.585Z","state":"active","storage":{"diskFreeBytes":41568894976,"diskMessages":0,"memoryLimitBytes":null,"memoryMessages":0,"memoryUsedBytes":0},"version":"0.1.0"}}],"partial":false,"scope":"cluster","summary":{"members":{"active":1},"membersVersion":0,"quorumAvailable":true,"syncingBlocks":0},"worstCondition":"ok"}"#;
    let status = parse_cluster_status(text).unwrap();
    let node = &status.nodes[0];
    assert_eq!(node.channels, 2);
    assert_eq!((node.memory_limit, node.memory_ratio()), (0, 0.0));
    assert_eq!(node.rtt_p95, None);
    assert_eq!(node.disk_free, 41_568_894_976);
    assert!(node.quorum);
    let stop = parse_stop(r#"{"nodes":[{"drained":true,"nodeId":"a"}],"stopList":false,"stopped":["a"],"warnings":["S3 backup not configured: there is no stop list"]}"#).unwrap();
    assert_eq!(stop.stopped, vec!["a"]);
    assert_eq!(stop.warnings.len(), 1);
}

/// TUI: TUI-TOK-01
#[test]
fn a_page_of_tokens_is_read_with_its_cursor_and_without_a_secret() {
    let page = parse_tokens(
        r#"{"items":[{"id":"t1","name":"app","permissions":["publish","consume"],"topicPrefixes":["orders."],"createdAt":"2026-10-01T10:00:00Z","expiresAt":null},{"id":"t2","name":"ops","permissions":["admin"],"topicPrefixes":[],"createdAt":"2026-10-02T10:00:00Z","expiresAt":"2027-01-01T00:00:00Z"}],"nextCursor":"abc"}"#,
    )
    .unwrap();
    assert_eq!(page.items.len(), 2);
    assert_eq!(page.items[0].permissions, vec!["publish", "consume"]);
    assert_eq!(page.items[0].prefixes, vec!["orders."]);
    assert_eq!(page.items[0].expires_at, None);
    assert_eq!(
        page.items[1].expires_at.as_deref(),
        Some("2027-01-01T00:00:00Z")
    );
    assert_eq!(page.next_cursor.as_deref(), Some("abc"));
}

/// TUI: TUI-TOK-03
#[test]
fn a_created_token_brings_the_secret_once() {
    let (token, secret) = parse_token_created(
        r#"{"id":"t1","name":"app","permissions":["publish"],"topicPrefixes":[],"createdAt":"2026-10-01T10:00:00Z","expiresAt":null,"token":"tb_t1.s3cret"}"#,
    )
    .unwrap();
    assert_eq!((token.id.as_str(), secret.as_str()), ("t1", "tb_t1.s3cret"));
    assert!(parse_token_created(r#"{"id":"t1","name":"app"}"#).is_err());
}

/// TUI: TUI-TOK-02
#[test]
fn the_token_calls_map_to_their_routes() {
    let c = ApiCall::Tokens(Some("c1".into())).to_call();
    assert_eq!((c.method, c.path.as_str()), ("GET", "/tokens"));
    assert!(c.query.contains(&("cursor".to_owned(), "c1".to_owned())));
    let c = ApiCall::TokenCreate("{}".into()).to_call();
    assert_eq!(
        (c.method, c.path.as_str(), c.body.as_slice()),
        ("POST", "/tokens", b"{}".as_slice())
    );
    let c = ApiCall::TokenUpdate("a/b".into(), "{}".into()).to_call();
    assert_eq!((c.method, c.path.as_str()), ("PATCH", "/tokens/a%2Fb"));
    let c = ApiCall::TokenRevoke("t1".into()).to_call();
    assert_eq!((c.method, c.path.as_str()), ("DELETE", "/tokens/t1"));
    assert!(ApiCall::TokenRevoke("t1".into()).is_action() && !ApiCall::Tokens(None).is_action());
}

/// TUI: TUI-TOP-01
#[test]
fn a_page_of_topics_is_read_with_its_definition_and_the_values_that_apply() {
    let page = parse_topics(
        r#"{"items":[{"id":"1","name":"orders","type":"queue","fifo":true,"volatile":false,"tags":{"team":"shop","env":"prod"},"ttl":60,"maxDeliveries":null,"maxMessageBytes":null,"effective":{"maxDeliveries":10,"maxMessageBytes":5242880,"ttl":60}},{"id":"2","name":"orders-dead-letter","deadLetter":true,"source":"orders"},{"id":"3","name":"fan","type":"distributor","prefixes":["a","b"]},{"nope":1}],"nextCursor":"abc"}"#,
    )
    .expect("a page");
    assert_eq!(page.next_cursor.as_deref(), Some("abc"));
    // The item without a name is skipped.
    assert_eq!(page.items.len(), 3);
    let orders = &page.items[0];
    assert!(orders.fifo && !orders.volatile && !orders.is_distributor());
    assert_eq!(
        orders.tags,
        [
            ("team".into(), "shop".into()),
            ("env".into(), "prod".into())
        ]
    );
    assert_eq!((orders.ttl, orders.max_deliveries), (Some(60), None));
    assert_eq!(orders.effective_max_deliveries, Some(10));
    assert_eq!(orders.effective_max_message_bytes, Some(5_242_880));
    let dead = &page.items[1];
    assert!(dead.dead_letter);
    assert_eq!(dead.source.as_deref(), Some("orders"));
    assert_eq!(dead.kind, "queue");
    assert!(page.items[2].is_distributor());
    assert_eq!(page.items[2].prefixes, ["a", "b"]);
}

/// TUI: TUI-TOP-01
#[test]
fn the_numbers_of_a_topic_come_with_the_severity_of_its_worst_condition() {
    let page = parse_topic_stats(
        r#"{"items":[{"name":"orders","depth":9,"publishRate":1.5,"consumeRate":0,"oldestAge":61.2,"worstCondition":"stalled","conditions":[{"condition":"stalled","severity":"warning"},{"condition":"idle","severity":"info"}]},{"name":"fan"}],"partial":true,"missingNodes":[{"nodeId":"n2","reason":"timeout"}]}"#,
    )
    .expect("a page");
    assert!(page.partial);
    assert_eq!(page.missing.len(), 1);
    let (name, orders) = &page.items[0];
    assert_eq!(name, "orders");
    assert_eq!(orders.depth, Some(9));
    assert_eq!(orders.oldest_age, Some(61.2));
    assert_eq!(
        (orders.worst.as_str(), orders.severity),
        ("stalled", Severity::Warning)
    );
    // A distributor has no numbers, and no condition other than `ok`.
    let (_, fan) = &page.items[1];
    assert_eq!((fan.depth, fan.worst.as_str()), (None, "ok"));
}

/// TUI: TUI-TPF-01
#[test]
fn the_topic_calls_use_the_routes_of_the_api() {
    let route = |call: ApiCall| {
        let c = call.to_call();
        (c.method, c.path)
    };
    assert_eq!(
        route(ApiCall::TopicUpdate("a b".into(), "{}".into())),
        ("PATCH", "/topics/a%20b".to_owned())
    );
    assert_eq!(
        route(ApiCall::TopicDelete("orders".into())),
        ("DELETE", "/topics/orders".to_owned())
    );
    assert_eq!(
        route(ApiCall::TopicCreate("{}".into())),
        ("POST", "/topics".to_owned())
    );
    assert!(ApiCall::TopicDelete("x".into()).is_action());
    assert!(!ApiCall::Topics(None).is_action());
    let stats = ApiCall::TopicStats(Some("c".into())).to_call();
    assert!(stats.query.contains(&("cursor".to_owned(), "c".to_owned())));
    assert!(stats
        .query
        .iter()
        .any(|(k, v)| k == "fields" && v.contains("worstCondition")));
}