use super::super::*;
#[allow(unused_imports)]
use super::support::*;
#[test]
fn render_env_resources_tree_marks_orphan_instances_when_no_asg() {
let mut res = empty_resources();
res.instances = vec!["i-stranded".into()];
let body = crate::app::render_env_resources_tree(&res, "env", "Web");
assert!(body.contains("orphan (no ASG attached)"));
assert!(body.contains("i-stranded"));
}
#[tokio::test]
async fn tab_in_command_mode_cycles_through_matches() {
let mut app = test_app();
app.mode = Mode::Command;
app.command_input = "bat".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
let first = app.command_input.text().to_string();
assert!(
first.starts_with("bat"),
"Tab should keep the bat-prefix; got {first:?}"
);
assert!(
crate::commands::all_names().contains(&first.as_str()),
"Tab should expand to a real command name; got {first:?}"
);
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
let second = app.command_input.text().to_string();
assert_ne!(first, second, "second Tab should advance the cycle");
}
#[tokio::test]
async fn shift_tab_cycles_backward() {
let mut app = test_app();
app.mode = Mode::Command;
app.command_input = "ba".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
let forward = app.command_input.clone();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
press(&mut app, KeyCode::BackTab, KeyModifiers::NONE);
assert_eq!(
app.command_input, forward,
"Tab Tab BackTab should land on the first match"
);
}
#[tokio::test]
async fn first_tab_lands_on_the_first_candidate() {
let mut app = test_app();
app.mode = Mode::Command;
app.command_input = "ba".into();
let expected = crate::app::completion_candidates("ba");
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(
app.command_input.text(),
expected[0],
"first Tab should land on the first match, not skip it"
);
}
#[tokio::test]
async fn tab_completes_env_name_for_diff() {
let mut app = test_app();
app.environments = vec![
mk_env("prod-api", "shop", "Web", "Red"),
mk_env("prod-worker", "shop", "Worker", "Green"),
];
app.mode = Mode::Command;
app.command_input = "diff prod".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(app.command_input.text(), "diff prod-api");
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(app.command_input.text(), "diff prod-worker");
}
#[tokio::test]
async fn tab_completes_second_env_name_for_diff() {
let mut app = test_app();
app.environments = vec![
mk_env("prod-api", "shop", "Web", "Red"),
mk_env("staging-api", "shop", "Web", "Green"),
];
app.mode = Mode::Command;
app.command_input = "diff prod-api sta".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(app.command_input.text(), "diff prod-api staging-api");
}
#[tokio::test]
async fn tab_on_non_env_command_preserves_arg_tail() {
let mut app = test_app();
app.environments = vec![mk_env("prod-api", "shop", "Web", "Green")];
app.mode = Mode::Command;
app.command_input = "set-option aws".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
let out = app.command_input.text().to_string();
assert!(
out.starts_with("set-option") && out.ends_with(" aws"),
"verb re-completed, arg tail preserved; got {out:?}"
);
}
#[tokio::test]
async fn tab_env_arg_with_no_match_restores_and_hints() {
let mut app = test_app();
app.environments = vec![mk_env("prod-api", "shop", "Web", "Green")];
app.mode = Mode::Command;
app.command_input = "diff zzz".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(
app.command_input.text(),
"diff zzz",
"no env match restores the typed input"
);
assert!(app
.status_message
.as_deref()
.is_some_and(|m| m.contains("no environment matches")));
}
#[tokio::test]
async fn tab_env_arg_survives_multibyte_whitespace() {
let mut app = test_app();
app.environments = vec![mk_env("prod-api", "shop", "Web", "Green")];
app.mode = Mode::Command;
app.command_input = "diff prod\u{00A0}".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(app.command_input.text(), "diff prod\u{00A0}prod-api");
}
#[test]
fn alarm_kind_to_metric_covers_known_kinds() {
use crate::app::alarm_kind_to_metric;
let (m, op, _) = alarm_kind_to_metric("health").unwrap();
assert_eq!(m, "EnvironmentHealth");
assert_eq!(op, "LessThanOrEqualToThreshold");
let (m, op, _) = alarm_kind_to_metric("5xx").unwrap();
assert_eq!(m, "ApplicationRequests5xx");
assert_eq!(op, "GreaterThanThreshold");
for (kind, metric, op, stat) in [
(
"health",
"EnvironmentHealth",
"LessThanOrEqualToThreshold",
"Maximum",
),
(
"4xx",
"ApplicationRequests4xx",
"GreaterThanThreshold",
"Sum",
),
(
"req4xx",
"ApplicationRequests4xx",
"GreaterThanThreshold",
"Sum",
),
(
"5xx",
"ApplicationRequests5xx",
"GreaterThanThreshold",
"Sum",
),
(
"req5xx",
"ApplicationRequests5xx",
"GreaterThanThreshold",
"Sum",
),
(
"latency",
"ApplicationLatencyP90",
"GreaterThanThreshold",
"Average",
),
(
"p90",
"ApplicationLatencyP90",
"GreaterThanThreshold",
"Average",
),
] {
assert_eq!(
alarm_kind_to_metric(kind),
Some((metric, op, stat)),
"alarm_kind_to_metric({kind:?})"
);
}
assert!(alarm_kind_to_metric("cpu").is_none());
assert!(alarm_kind_to_metric("").is_none());
}
#[test]
fn palette_score_prefers_label_prefix_then_substring_then_detail() {
assert_eq!(palette_score("", "anything", "anything"), Some(0));
assert_eq!(palette_score("reg", "region", "switch AWS region"), Some(0));
let s_label = palette_score("ion", "region", "switch AWS region").unwrap();
assert!(s_label > 0 && s_label < 1_000);
let s_detail = palette_score("aws", ":region", "switch AWS profile").unwrap();
let s_label_match = palette_score("aws", "aws-thing", "irrelevant").unwrap();
assert!(s_detail >= 1_000);
assert!(s_label_match < s_detail);
assert_eq!(palette_score("xyzzy", "region", "switch AWS region"), None);
}
#[tokio::test]
async fn ssh_no_arg_without_detail_errors_clearly() {
let mut app = test_app();
app.execute_command("ssh");
assert!(app.picker.is_none());
let err = app.error_message.as_deref().unwrap_or("");
assert!(
err.contains("Detail") || err.contains("instance ID"),
"expected guidance about Detail/Instances or instance ID, got: {err}"
);
}
#[tokio::test]
async fn ssm_run_without_detail_errors_with_instances_guidance() {
let mut app = test_app();
app.execute_command("ssm-run \"uptime\"");
let err = app.error_message.as_deref().unwrap_or("");
assert!(
err.contains("Detail") && err.contains("Instances"),
"expected Detail/Instances guidance, got: {err}"
);
}
#[test]
fn format_alarm_history_renders_entries_and_empty_stub() {
use chrono::TimeZone;
let ts = |h, mi| chrono::Utc.with_ymd_and_hms(2026, 5, 24, h, mi, 0).unwrap();
let mk = |t, kind: &str, summary: &str| crate::aws::AlarmHistoryEntry {
at: Some(t),
kind: kind.into(),
summary: summary.into(),
};
let stub = crate::app::format_alarm_history("high-cpu", &[]);
assert!(stub.contains("No history items"));
assert!(stub.contains("90 days"));
let entries = vec![
mk(ts(12, 5), "StateUpdate", "Alarm updated from OK to ALARM"),
mk(ts(11, 0), "ConfigurationUpdate", "Threshold changed to 80"),
];
let body = crate::app::format_alarm_history("high-cpu", &entries);
assert!(body.contains("[StateUpdate]"));
assert!(body.contains("[ConfigurationUpdate]"));
assert!(body.contains("Alarm updated from OK to ALARM"));
assert!(body.contains("Threshold changed to 80"));
let p_state = body.find("StateUpdate").unwrap();
let p_cfg = body.find("ConfigurationUpdate").unwrap();
assert!(p_state < p_cfg);
}
#[test]
fn format_alarm_history_handles_missing_timestamp() {
let entries = vec![crate::aws::AlarmHistoryEntry {
at: None,
kind: "StateUpdate".into(),
summary: "Alarm went ALARM".into(),
}];
let body = crate::app::format_alarm_history("high-cpu", &entries);
assert!(body.contains("—"));
assert!(body.contains("Alarm went ALARM"));
}
#[test]
fn format_alarms_handles_empty_and_error() {
let none = format_alarms(Ok(vec![]));
assert!(none.contains("no CloudWatch alarms"));
let err = format_alarms(Err("boom".into()));
assert!(err.contains("error"));
let alarms = format_alarms(Ok(vec![CwAlarm {
name: "high-cpu".into(),
state: "ALARM".into(),
state_reason: "CPU > 80%".into(),
metric_name: "CPUUtilization".into(),
namespace: "AWS/EC2".into(),
}]));
assert!(alarms.contains("ALARM"));
assert!(alarms.contains("high-cpu"));
assert!(alarms.contains("CPU > 80%"));
}
#[test]
fn detail_tab_titles_are_distinct() {
use std::collections::HashSet;
let titles: HashSet<&str> = [
DetailTab::Health,
DetailTab::Events,
DetailTab::Instances,
DetailTab::Metrics,
DetailTab::Queue,
DetailTab::Config,
]
.iter()
.map(|t| t.title())
.collect();
assert_eq!(titles.len(), 6);
}
#[tokio::test]
async fn tab_cycles_scope_envs_to_apps_and_back() {
let mut app = test_app();
assert_eq!(app.scope, Scope::Envs);
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(app.scope, Scope::Apps);
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(app.scope, Scope::Envs);
}
#[tokio::test]
async fn tab_completes_an_env_name_after_an_env_taking_command() {
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();
app.mode = Mode::Command;
app.command_input = "config-diff api".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert_eq!(
app.command_input.text(),
"config-diff api-prod",
"the command prefix is preserved and the env name completed"
);
}
#[tokio::test]
async fn tab_completes_an_env_name_across_a_multibyte_space() {
let mut app = test_app();
app.environments = vec![fake_env_with("api-prod", "Ready", "Green", None)];
app.rebuild_view();
app.mode = Mode::Command;
app.command_input = "config-diff\u{00A0}api".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert!(
app.command_input.text().ends_with("api-prod"),
"got {:?}",
app.command_input.text()
);
}
#[tokio::test]
async fn config_tab_renders_the_in_place_value_editor() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let detail = app.detail.as_mut().expect("detail opened");
detail.tags = vec![("Owner".into(), "platform".into())];
detail.config_cursor = 0;
detail.tab_idx = detail
.tabs
.iter()
.position(|t| *t == DetailTab::Config)
.expect("Config tab present");
app.start_config_edit();
let edit = app
.detail
.as_ref()
.and_then(|d| d.config_edit.as_ref())
.expect("editor open");
assert_eq!(edit.mode, crate::app::ConfigEditMode::Value);
let out = render(&mut app, 140, 40);
assert!(out.contains("Owner"), "key cell still renders:\n{out}");
assert!(
out.contains("platform"),
"the value being edited renders:\n{out}"
);
}
#[tokio::test]
async fn the_detail_tick_does_not_stack_a_slow_scan() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Red")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let detail = app.detail.as_mut().expect("detail opened");
detail.tab_idx = detail
.tabs
.iter()
.position(|t| *t == DetailTab::Instances)
.expect("Instances tab present");
detail.loading_instances = false;
detail.loading_events = false;
app.detail_fetch_started = None;
assert!(
!app.detail.as_ref().unwrap().tab_loading(),
"nothing outstanding yet"
);
app.detail_refresh_active_tab();
assert!(app.detail.as_ref().unwrap().loading_instances);
let fired_at = app.detail_fetch_started.expect("stamped");
app.detail_refresh_active_tab();
assert_eq!(
app.detail_fetch_started,
Some(fired_at),
"a second refresh must not have been fired"
);
app.detail.as_mut().unwrap().loading_instances = false;
app.detail_refresh_active_tab();
assert_ne!(
app.detail_fetch_started,
Some(fired_at),
"a finished fetch must not block the next one"
);
}
#[tokio::test]
async fn a_lost_detail_fetch_does_not_wedge_the_tab() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Red")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let detail = app.detail.as_mut().expect("detail opened");
detail.tab_idx = detail
.tabs
.iter()
.position(|t| *t == DetailTab::Instances)
.expect("Instances tab present");
app.detail_refresh_active_tab();
app.detail_fetch_started =
Some(std::time::Instant::now() - std::time::Duration::from_secs(121));
let stale = app.detail_fetch_started;
app.detail_refresh_active_tab();
assert_ne!(
app.detail_fetch_started, stale,
"an outstanding fetch this old is lost, not slow — retry it"
);
}
#[tokio::test]
async fn every_detail_tab_reports_its_own_loading_state() {
let mut app = test_app();
app.environments = vec![mk_env("wk-prod", "uflexi", "Worker", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let d = app.detail.as_mut().expect("detail opened");
assert!(
d.tabs.contains(&DetailTab::Queue),
"worker envs get the Queue tab"
);
d.loading_events = false;
d.loading_instances = false;
d.loading_queues = false;
d.loading_metrics = false;
d.cw_alarms = Default::default();
d.recent_versions = Default::default();
d.log_tail.stage = crate::app::LogTailStage::Idle;
for idx in 0..d.tabs.len() {
d.tab_idx = idx;
assert!(!d.tab_loading(), "{:?} with no flags set", d.tab());
}
type LoadingCase = (
&'static str,
fn(&mut crate::app::DetailState),
&'static [DetailTab],
);
let cases: &[LoadingCase] = &[
(
"events",
|d| d.loading_events = true,
&[DetailTab::Health, DetailTab::Events],
),
(
"instances",
|d| d.loading_instances = true,
&[DetailTab::Instances],
),
(
"queues",
|d| d.loading_queues = true,
&[DetailTab::Health, DetailTab::Queue],
),
(
"metrics",
|d| d.loading_metrics = true,
&[DetailTab::Metrics],
),
("alarms", |d| d.cw_alarms.begin(), &[DetailTab::Health]),
(
"recent versions",
|d| d.recent_versions.begin(),
&[DetailTab::Health],
),
(
"log tail",
|d| d.log_tail.stage = crate::app::LogTailStage::Polling,
&[DetailTab::Logs],
),
];
for (label, set, owners) in cases {
let d = app.detail.as_mut().unwrap();
d.loading_events = false;
d.loading_instances = false;
d.loading_queues = false;
d.loading_metrics = false;
d.cw_alarms = Default::default();
d.recent_versions = Default::default();
d.log_tail.stage = crate::app::LogTailStage::Idle;
set(d);
for idx in 0..d.tabs.len() {
d.tab_idx = idx;
let tab = d.tab();
assert_eq!(
d.tab_loading(),
owners.contains(&tab),
"{label} loading, on the {tab:?} tab"
);
}
}
}
fn detail_app_on_tab(tab: DetailTab) -> App {
detail_app_on_tab_for_tier(tab, "Web")
}
fn detail_app_on_tab_for_tier(tab: DetailTab, tier: &str) -> App {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", tier, "Red")];
app.view.invalidate();
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let detail = app.detail.as_mut().expect("detail opened");
detail.tab_idx = detail
.tabs
.iter()
.position(|t| *t == tab)
.expect("tab present");
app
}
#[tokio::test]
async fn detail_events_tab_renders_its_events() {
let mut app = detail_app_on_tab(DetailTab::Events);
let d = app.detail.as_mut().unwrap();
d.loading_events = false;
d.events = vec![make_event("EVENT-CANARY deployment failed")];
let out = render(&mut app, 160, 44);
assert!(out.contains("EVENT-CANARY"), "event text renders:\n{out}");
}
#[tokio::test]
async fn detail_instances_tab_renders_its_instances() {
let mut app = detail_app_on_tab(DetailTab::Instances);
let d = app.detail.as_mut().unwrap();
d.loading_instances = false;
d.instances = vec![crate::aws::Instance {
id: "i-0canary99".into(),
health: "Severe".into(),
color: "Red".into(),
causes: vec!["ELB health failing".into()],
instance_type: "t3.medium".into(),
availability_zone: "eu-west-2a".into(),
launched_at: None,
}];
let out = render(&mut app, 160, 44);
assert!(out.contains("i-0canary99"), "instance id renders:\n{out}");
}
#[tokio::test]
async fn detail_queue_tab_renders_its_queues() {
let mut app = detail_app_on_tab_for_tier(DetailTab::Queue, "Worker");
let d = app.detail.as_mut().unwrap();
d.loading_queues = false;
d.queues = crate::aws::WorkerQueues {
main_url: Some("https://sqs.eu-west-2.amazonaws.com/1/awseb-CANARY".into()),
dlq_url: None,
main_stats: None,
dlq_stats: None,
dlq_origin: None,
};
let out = render(&mut app, 160, 44);
assert!(out.contains("awseb-CANARY"), "queue url renders:\n{out}");
}
#[tokio::test]
async fn detail_logs_tab_draws_even_with_nothing_tailing() {
let mut app = detail_app_on_tab(DetailTab::Logs);
let out = render(&mut app, 160, 44);
assert!(
out.contains("instance(s)"),
"the Logs pane's own title counts instances and lines:\n{out}"
);
}
#[tokio::test]
async fn the_alarms_pair_encodes_two_orthogonal_facts_not_one_redundant_state() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "WebServer", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let d = app.detail.as_mut().expect("detail opened");
d.tab_idx = d
.tabs
.iter()
.position(|t| *t == DetailTab::Health)
.expect("Health tab");
d.loading_events = false;
d.loading_queues = false;
d.recent_versions = Default::default();
let alarm = || {
vec![crate::aws::CwAlarm {
name: "cpu-high".into(),
state: "ALARM".into(),
state_reason: String::new(),
metric_name: "CPUUtilization".into(),
namespace: "AWS/EC2".into(),
}]
};
d.cw_alarms = Default::default();
assert!(!d.tab_loading(), "idle must not claim to be loading");
assert!(!d.cw_alarms.is_first_load());
assert!(d.cw_alarms.ready().is_none() && d.cw_alarms.error().is_none());
d.cw_alarms.begin();
assert!(d.tab_loading(), "first load is in flight");
assert!(
d.cw_alarms.is_first_load(),
"nothing to show yet, so ui/detail.rs draws `fetching alarms…`"
);
d.cw_alarms.settle(Ok(alarm()));
assert!(!d.tab_loading(), "settled must not claim to be loading");
assert!(!d.cw_alarms.is_first_load());
assert_eq!(d.cw_alarms.ready().map(Vec::len), Some(1));
d.cw_alarms.begin();
assert!(
d.tab_loading(),
"a refresh must still report in-flight even when data is present"
);
assert!(
!d.cw_alarms.is_first_load(),
"but it must NOT draw the spinner — the old data stays visible"
);
assert_eq!(
d.cw_alarms.ready().map(Vec::len),
Some(1),
"and the previous result must survive `begin()`"
);
d.cw_alarms.settle(Err("DescribeAlarms denied".into()));
assert!(!d.tab_loading());
assert!(d.cw_alarms.ready().is_none(), "a failure is not a value");
assert!(d.cw_alarms.error().is_some_and(|e| e.contains("denied")));
}
#[tokio::test]
async fn a_successful_fetch_does_not_erase_another_fetchs_error() {
let mut app = test_app();
app.environments = vec![mk_env("wk-prod", "uflexi", "Worker", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
let gen = app.generation;
app.handle_msg(crate::app::AppMsg::DetailEvents {
gen,
env_name: "wk-prod".to_string(),
result: Err("AccessDenied: DescribeEvents".to_string()),
});
let after_failure = app
.detail
.as_ref()
.and_then(|d| d.error.as_ref().map(|e| e.message.clone()));
assert_eq!(
after_failure.as_deref(),
Some("AccessDenied: DescribeEvents"),
"the failure must be recorded"
);
app.handle_msg(crate::app::AppMsg::DetailInstances {
gen,
env_name: "wk-prod".to_string(),
result: Ok(Vec::new()),
});
let after_success = app
.detail
.as_ref()
.and_then(|d| d.error.as_ref().map(|e| e.message.clone()));
assert_eq!(
after_success.as_deref(),
Some("AccessDenied: DescribeEvents"),
"an unrelated success must NOT erase it — the operator would see \
a clean panel and never learn the events fetch was denied"
);
}
fn detail_on(env: &str) -> App {
let mut app = test_app();
app.environments = vec![mk_env(env, "uflexi", "Web", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
app.mode = crate::app::Mode::Detail;
app
}
fn focus_tab(app: &mut App, tab: DetailTab) {
let detail = app.detail.as_mut().expect("detail open");
detail.tab_idx = detail
.tabs
.iter()
.position(|t| *t == tab)
.unwrap_or_else(|| panic!("{tab:?} not present"));
}
#[tokio::test]
async fn scrolling_an_empty_detail_list_is_inert_not_fatal() {
for tab in [DetailTab::Instances, DetailTab::Health, DetailTab::Config] {
for delta in [1, -1] {
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.instances.clear();
d.tags.clear();
d.env_vars = Vec::new();
d.events = Vec::new();
}
focus_tab(&mut app, tab);
app.detail_scroll(delta);
let d = app.detail.as_ref().expect("detail still open");
let cursor = match tab {
DetailTab::Instances => d.instances_cursor,
DetailTab::Health => d.health_cursor,
DetailTab::Config => d.config_cursor,
_ => 0,
};
assert_eq!(
cursor, 0,
"{tab:?} with delta {delta}: cursor moved on an empty list"
);
}
}
}
#[tokio::test]
async fn the_queue_cursor_wraps_and_the_config_cursor_clamps() {
let mut app = detail_on("worker-prod");
{
let d = app.detail.as_mut().expect("detail open");
if !d.tabs.contains(&DetailTab::Queue) {
d.tabs.push(DetailTab::Queue);
}
}
focus_tab(&mut app, DetailTab::Queue);
app.detail_scroll(1);
assert_eq!(app.detail.as_ref().unwrap().queue_cursor, 1);
app.detail_scroll(1);
assert_eq!(
app.detail.as_ref().unwrap().queue_cursor,
0,
"the queue cursor wraps past the last row"
);
app.detail_scroll(-1);
assert_eq!(
app.detail.as_ref().unwrap().queue_cursor,
1,
"and wraps backwards off the first"
);
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.tags = vec![
("Owner".into(), "platform".into()),
("Team".into(), "infra".into()),
];
d.env_vars = Vec::new();
d.config_cursor = 0;
}
focus_tab(&mut app, DetailTab::Config);
let n = crate::app::config_editable_items(app.detail.as_ref().unwrap()).len();
assert!(n >= 2, "the fixture needs at least two editable rows");
app.detail_scroll(-1);
assert_eq!(
app.detail.as_ref().unwrap().config_cursor,
0,
"the config cursor clamps at the top rather than wrapping to the end"
);
for _ in 0..(n + 3) {
app.detail_scroll(1);
}
assert_eq!(
app.detail.as_ref().unwrap().config_cursor,
n - 1,
"and clamps at the bottom rather than wrapping to the top"
);
}
#[tokio::test]
async fn detail_tab_cycling_wraps_both_ways() {
let mut app = detail_on("api-prod");
let n = app.detail.as_ref().unwrap().tabs.len();
assert!(
n >= 4,
"detail_cycle_tab calls rem_euclid(tabs.len()) with no zero guard; \
it is safe only because the list is built non-empty and only \
grows. Found {n} tabs."
);
app.detail.as_mut().unwrap().tab_idx = 0;
app.detail_cycle_tab(-1);
assert_eq!(
app.detail.as_ref().unwrap().tab_idx,
n - 1,
"cycling back off the first tab wraps to the last"
);
app.detail_cycle_tab(1);
assert_eq!(
app.detail.as_ref().unwrap().tab_idx,
0,
"and forward off the last wraps to the first"
);
app.detail_cycle_tab(1);
assert_eq!(app.detail.as_ref().unwrap().tab_idx, 1, "plain forward");
}
#[tokio::test]
async fn detail_search_steps_to_the_next_match_and_wraps() {
let armed = |cursor: u16| {
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.events = vec![
make_event("alpha match"),
make_event("beta"),
make_event("gamma match"),
make_event("delta"),
make_event("epsilon match"),
];
d.search_pattern = Some(regex::Regex::new("match").expect("valid regex"));
d.events_scroll = cursor;
}
app
};
let scroll = |app: &App| app.detail.as_ref().unwrap().events_scroll;
let mut app = armed(0);
app.detail_search_jump(1);
assert_eq!(
scroll(&app),
2,
"forward search must step past the match under the cursor, or `n` \
would never advance"
);
app.detail_search_jump(1);
assert_eq!(scroll(&app), 4);
app.detail_search_jump(1);
assert_eq!(scroll(&app), 0, "forward search wraps past the end");
let mut app = armed(0);
app.detail_search_jump(-1);
assert_eq!(
scroll(&app),
4,
"backward search wraps past the start to the last match"
);
let mut app = armed(3);
app.detail_search_jump(-1);
assert_eq!(scroll(&app), 2, "backward finds the preceding match");
let mut fwd = armed(2);
fwd.detail_search_jump(1);
let mut back = armed(2);
back.detail_search_jump(-1);
assert_ne!(
scroll(&fwd),
scroll(&back),
"`n` and `N` must move in opposite directions"
);
}
#[tokio::test]
async fn detail_search_with_nothing_to_find_is_inert() {
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.events = vec![make_event("alpha"), make_event("beta")];
d.search_pattern = Some(regex::Regex::new("nothing-here").expect("valid regex"));
d.events_scroll = 1;
}
app.detail_search_jump(1);
assert_eq!(
app.detail.as_ref().unwrap().events_scroll,
1,
"a search with no match leaves the cursor where it was"
);
let any = regex::Regex::new(".").expect("valid regex");
for delta in [1, -1] {
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.events = Vec::new();
d.search_pattern = Some(any.clone());
d.events_scroll = 0;
}
app.detail_search_jump(delta);
assert_eq!(app.detail.as_ref().unwrap().events_scroll, 0);
}
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.events = vec![make_event("alpha")];
d.search_pattern = None;
d.events_scroll = 0;
}
app.detail_search_jump(1);
assert_eq!(app.detail.as_ref().unwrap().events_scroll, 0);
}
#[tokio::test]
async fn detail_results_for_another_env_are_dropped() {
use crate::app::AppMsg;
let armed = || {
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.cw_log_groups = Some(vec!["existing-group".into()]);
}
app
};
let mut app = armed();
app.handle_msg(AppMsg::DetailLogGroups {
gen: app.generation,
env_name: "worker-prod".into(),
groups: vec!["wrong-env-group".into()],
});
assert_eq!(
app.detail.as_ref().unwrap().cw_log_groups,
Some(vec!["existing-group".to_string()]),
"another env's log groups were shown against this one"
);
let mut app = armed();
app.handle_msg(AppMsg::DetailAlarms {
gen: app.generation,
env_name: "worker-prod".into(),
result: Ok(Vec::new()),
});
assert!(
app.detail.as_ref().unwrap().cw_alarms.ready().is_none(),
"another env's alarm result settled this env's alarm panel"
);
let mut app = armed();
app.handle_msg(AppMsg::DetailRecentVersions {
gen: app.generation,
env_name: "worker-prod".into(),
result: Ok(vec![]),
});
assert!(
app.detail
.as_ref()
.unwrap()
.recent_versions
.ready()
.is_none(),
"another env's version list settled this env's panel"
);
let mut app = armed();
app.handle_msg(AppMsg::DetailLogGroups {
gen: app.generation,
env_name: "api-prod".into(),
groups: vec!["right-env-group".into()],
});
assert_eq!(
app.detail.as_ref().unwrap().cw_log_groups,
Some(vec!["right-env-group".to_string()]),
"the matching env's result must be applied, or the drop tests \
above prove nothing"
);
}
#[tokio::test]
async fn a_form_prefill_for_another_env_is_dropped() {
use crate::app::AppMsg;
let armed = || {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "shop", "WebServer", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
let mut form = crate::form::Form {
title: "Capacity".into(),
fields: vec![crate::form::FormField::text(
"MinSize",
"Min",
None::<String>,
)],
cursor: 0,
state: crate::form::FormState::Loading,
submit: crate::form::FormSubmit::OptionSettings {
mappings: vec![(
"MinSize".to_string(),
"aws:autoscaling:asg".to_string(),
"MinSize".to_string(),
)],
},
summary: "capacity".into(),
env_name: "api-prod".into(),
banner: String::new(),
scroll: 0,
};
form.fields[0].value = "untouched".into();
app.form = Some(form);
app
};
let mut app = armed();
app.handle_msg(AppMsg::FormPrefilled {
gen: app.generation,
env_name: "worker-prod".into(),
settings: Ok(vec![(
"aws:autoscaling:asg".into(),
"MinSize".into(),
"99".into(),
)]),
});
assert_eq!(
app.form.as_ref().unwrap().fields[0].value,
"untouched",
"another env's settings were prefilled into this env's form — the \
next submit would write them back to the wrong environment"
);
let mut app = armed();
app.handle_msg(AppMsg::FormPrefilled {
gen: app.generation,
env_name: "api-prod".into(),
settings: Ok(vec![(
"aws:autoscaling:asg".into(),
"MinSize".into(),
"4".into(),
)]),
});
assert_ne!(
app.form.as_ref().unwrap().fields[0].value,
"untouched",
"the matching env's prefill must be applied"
);
}
#[tokio::test]
async fn a_stale_dlq_depth_says_so_rather_than_reading_as_current() {
let mut app = test_app();
app.environments = vec![mk_env("worker-prod", "uflexi", "Worker", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.open_detail();
app.mode = crate::app::Mode::Detail;
app.worker_dlq_depths.insert("worker-prod".to_string(), 42);
let live = render(&mut app, 200, 40);
assert!(
live.contains("DLQ:42"),
"a worker env shows its DLQ depth; got:\n{live}"
);
assert!(
!live.contains("DLQ:42 (stale)"),
"a fresh reading is not marked stale; got:\n{live}"
);
app.worker_dlq_stale.insert("worker-prod".to_string());
let stale = render(&mut app, 200, 40);
assert!(
stale.contains("DLQ:42 (stale)"),
"a failed check marks the depth stale; got:\n{stale}"
);
}
#[tokio::test]
async fn the_detail_header_drops_whole_fields_on_a_narrow_terminal() {
let mut app = detail_on("api-prod");
let narrow = render(&mut app, 80, 26);
let dangling = narrow
.lines()
.any(|l| l.contains("CNAME:") && !l.contains("example.com"));
assert!(
!dangling,
"a CNAME label with a half-rendered value; got:\n{narrow}"
);
assert!(narrow.contains("Region:"), "got:\n{narrow}");
assert!(narrow.contains("Platform:"), "got:\n{narrow}");
let wide = render(&mut app, 200, 26);
assert!(
wide.contains("CNAME:") && wide.contains("example.com"),
"CNAME returns when there is room; got:\n{wide}"
);
}
#[tokio::test]
async fn the_detail_header_keeps_the_env_identity_at_any_width() {
let mut app = detail_on("api-prod");
for w in [70u16, 80, 100, 160] {
let f = render(&mut app, w, 26);
assert!(f.contains("api-prod"), "no env name at {w}; got:\n{f}");
assert!(f.contains("Health:"), "no health at {w}; got:\n{f}");
}
}
#[tokio::test]
async fn x_arms_the_delete_belonging_to_the_focused_tab_and_no_other() {
let armed = |app: &App| -> (Option<usize>, Option<usize>) {
let d = app.detail.as_ref().expect("detail open");
(d.config_delete_confirm, d.instance_terminate_confirm)
};
let open_on = |tab: DetailTab| {
let mut app = detail_on("api-prod");
{
let d = app.detail.as_mut().expect("detail open");
d.tags = vec![
("Owner".into(), "platform".into()),
("Team".into(), "infra".into()),
];
d.config_cursor = 0;
d.instances = vec![crate::aws::Instance {
id: "i-0123456789abcdef0".into(),
health: "Ok".into(),
color: "Green".into(),
causes: vec![],
instance_type: "t3.small".into(),
availability_zone: "us-east-1a".into(),
launched_at: None,
}];
d.instances_cursor = 0;
}
focus_tab(&mut app, tab);
app
};
let mut app = open_on(DetailTab::Config);
press(&mut app, KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(
armed(&app),
(Some(0), None),
"`x` on Config must arm the CONFIG delete only"
);
let mut app = open_on(DetailTab::Instances);
press(&mut app, KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(
armed(&app),
(None, Some(0)),
"`x` on Instances must arm the INSTANCE termination only"
);
for tab in [
DetailTab::Health,
DetailTab::Events,
DetailTab::Metrics,
DetailTab::Logs,
] {
let mut app = open_on(tab);
press(&mut app, KeyCode::Char('x'), KeyModifiers::NONE);
assert_eq!(
armed(&app),
(None, None),
"`x` on {tab:?} armed a delete that belongs to another tab"
);
}
}