use super::super::*;
#[allow(unused_imports)]
use super::support::*;
#[test]
fn sort_key_cycle_matches_ui_column_order() {
let order = [
SortKey::Name,
SortKey::App,
SortKey::Status,
SortKey::Health,
SortKey::Version,
SortKey::Age,
];
let mut cur = order[0];
for expected in order.iter().skip(1).chain(std::iter::once(&order[0])) {
cur = cur.next();
assert_eq!(cur, *expected);
}
}
#[test]
fn sort_key_parse_roundtrip() {
for k in [
SortKey::Name,
SortKey::App,
SortKey::Status,
SortKey::Health,
SortKey::Version,
SortKey::Age,
] {
assert_eq!(SortKey::parse(k.label()), Some(k));
}
assert_eq!(SortKey::parse("bogus"), None);
}
#[test]
fn parse_ignore_keys_splits_and_lowercases() {
assert_eq!(crate::app::parse_ignore_keys(None), Vec::<String>::new());
assert_eq!(
crate::app::parse_ignore_keys(Some("")),
Vec::<String>::new()
);
assert_eq!(
crate::app::parse_ignore_keys(Some(" Version_Label , MinSize ,")),
vec!["version_label".to_string(), "minsize".to_string()]
);
}
#[test]
fn filter_config_diffs_supports_namespace_qualified_match() {
let diffs = vec![
crate::app::ConfigDiff {
namespace: "aws:autoscaling:asg".into(),
name: "MinSize".into(),
left: Some("2".into()),
right: Some("3".into()),
},
crate::app::ConfigDiff {
namespace: "aws:elasticbeanstalk:command".into(),
name: "MinSize".into(),
left: Some("4".into()),
right: Some("5".into()),
},
];
let keys = crate::app::parse_ignore_keys(Some("aws:autoscaling:asg:MinSize"));
let filtered = crate::app::filter_config_diffs(diffs, &keys);
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].namespace, "aws:elasticbeanstalk:command");
}
#[test]
fn filter_config_diffs_empty_ignore_keys_is_passthrough() {
let diffs = vec![crate::app::ConfigDiff {
namespace: "ns".into(),
name: "MinSize".into(),
left: Some("2".into()),
right: Some("3".into()),
}];
let original_len = diffs.len();
assert_eq!(
crate::app::filter_config_diffs(diffs, &[]).len(),
original_len
);
}
#[test]
fn build_env_edit_body_sorts_keys_and_emits_header() {
let vars = vec![
("LOG_LEVEL".into(), "info".into()),
("DB_HOST".into(), "db.example".into()),
("DB_PORT".into(), "5432".into()),
];
let body = crate::app::build_env_edit_body("prod", &vars);
assert!(body.starts_with("# ebman env-var editor — prod\n"));
assert!(body.contains("Secrets Manager"));
let db_host_pos = body.find("DB_HOST=").expect("DB_HOST line");
let db_port_pos = body.find("DB_PORT=").expect("DB_PORT line");
let log_pos = body.find("LOG_LEVEL=").expect("LOG_LEVEL line");
assert!(db_host_pos < db_port_pos && db_port_pos < log_pos);
}
#[test]
fn parse_env_edit_body_drops_invalid_keys() {
let body = "= no-key\n KEY WITH SPACES=foo\nGOOD=val\n";
let parsed = crate::app::parse_env_edit_body(body);
assert_eq!(parsed.len(), 1);
assert!(parsed.contains_key("GOOD"));
}
#[tokio::test]
async fn first_run_hint_dismisses_on_first_key() {
let mut app = test_app();
app.first_run_hint = true;
press(&mut app, KeyCode::Char('j'), KeyModifiers::NONE);
assert!(
!app.first_run_hint,
"first key event should clear first_run_hint"
);
}
#[test]
fn render_options_overlay_groups_by_namespace_and_marks_set_vs_default() {
let rows = vec![
opt("aws:autoscaling:asg", "MinSize", Some("2"), Some("1")),
opt("aws:autoscaling:asg", "MaxSize", None, Some("4")),
opt(
"aws:elasticbeanstalk:command",
"DeploymentPolicy",
Some("Rolling"),
Some("AllAtOnce"),
),
];
let body = crate::app::render_options_overlay(&rows, None, "uflexi-prod");
assert!(body.contains("── aws:autoscaling:asg ──"));
assert!(body.contains("── aws:elasticbeanstalk:command ──"));
assert!(body.contains("▸ MinSize"));
assert!(body.contains("• MaxSize"));
assert!(body.contains("▸ DeploymentPolicy"));
assert!(body.contains("default: 1"));
assert!(body.contains("default: 4"));
assert!(body.contains("2/3 options are operator-set"));
}
#[test]
fn render_options_overlay_filters_to_namespace_when_given() {
let rows = vec![
opt("aws:autoscaling:asg", "MinSize", Some("2"), None),
opt(
"aws:elasticbeanstalk:command",
"DeploymentPolicy",
Some("Rolling"),
None,
),
];
let body =
crate::app::render_options_overlay(&rows, Some("aws:autoscaling:asg"), "uflexi-prod");
assert!(body.contains("MinSize"));
assert!(!body.contains("DeploymentPolicy"));
}
#[test]
fn delta_toast_key_extracts_bucket_for_delta_shapes() {
assert_eq!(
crate::app::delta_toast_key("▲2 Red").as_deref(),
Some("Red")
);
assert_eq!(
crate::app::delta_toast_key("▼1 Yellow").as_deref(),
Some("Yellow")
);
assert_eq!(
crate::app::delta_toast_key(" ▲10 Green").as_deref(),
Some("Green")
);
}
#[test]
fn parse_s3_url_extracts_bucket_and_key() {
let (b, k) = crate::app::parse_s3_url("s3://my-bucket/path/to/bundle.zip").unwrap();
assert_eq!(b, "my-bucket");
assert_eq!(k, "path/to/bundle.zip");
}
#[test]
fn delta_toast_key_returns_none_for_non_delta_text() {
assert_eq!(crate::app::delta_toast_key("refreshing…"), None);
assert_eq!(crate::app::delta_toast_key(""), None);
assert_eq!(crate::app::delta_toast_key("▲"), None);
assert_eq!(crate::app::delta_toast_key("▲ Red"), None);
assert_eq!(crate::app::delta_toast_key("▲5 "), None);
}
#[tokio::test]
async fn diff_ignore_keys_flag_parses_and_unknown_flag_errors() {
let mut app = test_app();
app.environments = vec![
mk_env("staging", "uflexi", "Web", "Green"),
mk_env("prod", "uflexi", "Web", "Green"),
];
app.rebuild_view();
app.execute_command("diff staging prod --ignore-keys version,updated");
assert!(
matches!(app.current_overlay, Some(Overlay::Diff(_))),
"expected Overlay::Diff with --ignore-keys present"
);
assert!(
app.error_message.is_none(),
"unexpected: {:?}",
app.error_message
);
app.current_overlay = None;
app.execute_command("diff staging prod --bogus");
assert!(app.current_overlay.is_none(), "shouldn't open on bad flag");
assert!(
app.error_message
.as_deref()
.unwrap_or("")
.contains("unknown arg '--bogus'"),
"expected unknown-arg error, got {:?}",
app.error_message
);
app.error_message = None;
app.current_overlay = None;
app.execute_command("diff staging prod extra");
assert!(
app.current_overlay.is_none(),
"shouldn't open with 3 positionals"
);
assert!(
app.error_message
.as_deref()
.unwrap_or("")
.contains("at most two"),
"expected too-many-args error, got {:?}",
app.error_message
);
}
#[tokio::test]
async fn slash_enters_filter_mode_and_text_lands() {
let mut app = test_app();
app.environments = vec![
mk_env("prod-web", "uflexi", "Web", "Green"),
mk_env("staging-web", "uflexi", "Web", "Green"),
];
app.rebuild_view();
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Filter);
for c in "prod".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
assert_eq!(app.view.filter().text(), "prod");
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Normal);
assert!(app.view.filter().is_empty());
}
#[tokio::test]
async fn enter_on_red_env_opens_why_via_bang_keybind() {
let mut app = test_app();
app.environments = vec![mk_env("prod-web", "uflexi", "Web", "Red")];
app.rebuild_view();
app.table_state.select(Some(0));
press(&mut app, KeyCode::Char('!'), KeyModifiers::NONE);
assert!(
matches!(app.current_overlay, Some(Overlay::WhyRed { .. })),
"expected WhyRed overlay, got {:?}",
app.current_overlay
);
}
#[tokio::test]
async fn ctrl_x_toggles_redact() {
let mut app = test_app();
assert!(!app.view.redact);
press(&mut app, KeyCode::Char('x'), KeyModifiers::CONTROL);
assert!(app.view.redact);
press(&mut app, KeyCode::Char('x'), KeyModifiers::CONTROL);
assert!(!app.view.redact);
}
#[tokio::test]
async fn space_toggles_multi_select_and_esc_clears_it() {
let mut app = test_app();
app.environments = vec![
mk_env("api-prod", "uflexi", "Web", "Green"),
mk_env("api-staging", "uflexi", "Web", "Green"),
];
app.rebuild_view();
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
assert_eq!(app.multi_selected.len(), 1);
assert!(app.multi_selected.contains("api-prod"));
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
assert!(app.multi_selected.is_empty());
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
press(&mut app, KeyCode::Char('j'), KeyModifiers::NONE);
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
assert_eq!(app.multi_selected.len(), 2);
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert!(app.multi_selected.is_empty());
}
#[tokio::test]
async fn filter_mode_text_input_and_backspace_round_trips() {
let mut app = test_app();
app.environments = vec![
mk_env("api-prod", "uflexi", "Web", "Green"),
mk_env("api-staging", "uflexi", "Web", "Green"),
];
app.rebuild_view();
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Filter);
for c in "prod".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
assert_eq!(app.view.filter().text(), "prod");
press(&mut app, KeyCode::Backspace, KeyModifiers::NONE);
assert_eq!(app.view.filter().text(), "pro");
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Normal);
assert_eq!(app.view.filter().text(), "pro");
}
#[tokio::test]
async fn filter_input_is_cursor_aware_via_shared_textinput() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.rebuild_view();
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
for c in "prod".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
assert_eq!(app.view.filter().text(), "prod");
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Char('X'), KeyModifiers::NONE);
assert_eq!(app.view.filter().text(), "proXd");
}
#[tokio::test]
async fn esc_in_filter_mode_clears_the_filter() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.rebuild_view();
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
for c in "x".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
assert_eq!(app.view.filter().text(), "x");
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Normal);
assert!(app.view.filter().is_empty());
}
#[tokio::test]
async fn slash_with_active_filter_rebuilds_to_full_fleet() {
let mut app = test_app();
app.environments = vec![
mk_env("prod-web", "uflexi", "Web", "Green"),
mk_env("staging-web", "uflexi", "Web", "Green"),
];
app.rebuild_view();
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
for c in "prod".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Normal);
assert_eq!(app.view.filtered().len(), 1, "filter should hide one env");
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Filter);
assert!(app.view.filter().is_empty());
assert_eq!(
app.view.filtered().len(),
2,
"full fleet should be visible the moment filter mode opens"
);
}
#[tokio::test]
async fn star_toggles_pinned_set_for_selected_env() {
let mut app = test_app();
app.environments = vec![
mk_env("api-prod", "uflexi", "Web", "Green"),
mk_env("api-staging", "uflexi", "Web", "Green"),
];
app.rebuild_view();
press(&mut app, KeyCode::Char('*'), KeyModifiers::NONE);
assert!(app.pinned.contains("api-prod"));
press(&mut app, KeyCode::Char('*'), KeyModifiers::NONE);
assert!(!app.pinned.contains("api-prod"));
}
#[tokio::test]
async fn quickjump_input_is_cursor_aware_via_shared_textinput() {
let mut app = test_app();
app.environments = vec![mk_env("prod", "uflexi", "Web", "Green")];
app.rebuild_view();
press(&mut app, KeyCode::Char('\''), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::QuickJump);
for c in "abc".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
assert_eq!(app.quickjump_input.text(), "abc");
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Char('X'), KeyModifiers::NONE);
assert_eq!(app.quickjump_input.text(), "abXc");
press(&mut app, KeyCode::Backspace, KeyModifiers::NONE);
assert_eq!(app.quickjump_input.text(), "abc");
}
#[tokio::test]
async fn picker_filter_is_cursor_aware_via_shared_textinput() {
let mut app = test_app();
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Picker);
for c in "eu".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
let txt = |a: &App| a.picker.as_ref().unwrap().filter.text().to_string();
assert_eq!(txt(&app), "eu");
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Char('X'), KeyModifiers::NONE);
assert_eq!(txt(&app), "eXu");
press(&mut app, KeyCode::Backspace, KeyModifiers::NONE);
assert_eq!(txt(&app), "eu");
}
#[tokio::test]
async fn apps_scope_space_toggles_apps_selected() {
let mut app = test_app();
app.applications = vec![
crate::aws::Application {
name: "billing".into(),
description: String::new(),
date_created: None,
date_updated: None,
version_count: 0,
templates: vec![],
latest_version_label: None,
latest_version_created: None,
},
crate::aws::Application {
name: "checkout".into(),
description: String::new(),
date_created: None,
date_updated: None,
version_count: 0,
templates: vec![],
latest_version_label: None,
latest_version_created: None,
},
];
app.set_scope(Scope::Apps);
app.app_table_state.select(Some(0));
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
assert!(app.apps_selected.contains("billing"));
press(&mut app, KeyCode::Char(' '), KeyModifiers::NONE);
assert!(!app.apps_selected.contains("billing"));
}
#[tokio::test]
async fn apps_scope_star_pins_and_unpins_app() {
let mut app = test_app();
app.applications = vec![crate::aws::Application {
name: "billing".into(),
description: String::new(),
date_created: None,
date_updated: None,
version_count: 0,
templates: vec![],
latest_version_label: None,
latest_version_created: None,
}];
app.set_scope(Scope::Apps);
app.app_table_state.select(Some(0));
assert!(!app.pinned_apps.contains("billing"));
press(&mut app, KeyCode::Char('*'), KeyModifiers::SHIFT);
assert!(app.pinned_apps.contains("billing"));
press(&mut app, KeyCode::Char('*'), KeyModifiers::SHIFT);
assert!(!app.pinned_apps.contains("billing"));
}
#[tokio::test]
async fn esc_clears_apps_selected_when_no_envs_selected() {
let mut app = test_app();
app.apps_selected.insert("billing".into());
app.apps_selected.insert("checkout".into());
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert!(app.apps_selected.is_empty());
}
#[tokio::test]
async fn unconsumed_key_in_filter_mode_leaves_the_view_fresh() {
let mut app = test_app();
app.environments = vec![
fake_env_with("api-prod", "Ready", "Green", None),
fake_env_with("web-prod", "Ready", "Green", None),
];
app.rebuild_view();
press(&mut app, KeyCode::Char('/'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Filter);
press(&mut app, KeyCode::Down, KeyModifiers::NONE);
assert!(
!app.view.is_stale(),
"a key the filter buffer ignores must not leave the cache stale"
);
let _ = render(&mut app, 120, 30);
}
#[tokio::test]
async fn the_control_socket_screen_op_reports_the_last_frame() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.view.invalidate();
app.rebuild_view();
assert!(
app.screen_text().contains("no frame rendered yet"),
"{}",
app.screen_text()
);
app.last_rendered_buffer = Some(render_buf(&mut app, 120, 20));
let text = app.screen_text();
assert!(
text.contains("api-prod"),
"the screenshot carries the rendered fleet:\n{text}"
);
}