use super::super::*;
#[allow(unused_imports)]
use super::support::*;
#[test]
fn edit_distance_basic_cases() {
assert_eq!(crate::app::edit_distance("", ""), 0);
assert_eq!(crate::app::edit_distance("abc", ""), 3);
assert_eq!(crate::app::edit_distance("", "abc"), 3);
assert_eq!(crate::app::edit_distance("kitten", "sitting"), 3);
assert_eq!(crate::app::edit_distance("restart", "restart"), 0);
assert_eq!(crate::app::edit_distance("restrt", "restart"), 1);
assert_eq!(crate::app::edit_distance("rebild", "rebuild"), 1);
assert_eq!(crate::app::edit_distance("scal", "scale"), 1);
}
#[test]
fn suggest_command_catches_one_char_typos() {
assert_eq!(
crate::app::suggest_command("restrt").as_deref(),
Some("restart")
);
assert_eq!(
crate::app::suggest_command("rebild").as_deref(),
Some("rebuild")
);
assert_eq!(
crate::app::suggest_command("scal").as_deref(),
Some("scale")
);
}
#[test]
fn suggest_command_returns_none_when_too_far() {
assert_eq!(crate::app::suggest_command("zzzzzz"), None);
}
#[test]
fn suggest_command_threshold_is_strict_for_short_input() {
let suggestion = crate::app::suggest_command("zz");
assert!(
suggestion.is_none(),
"2-char typo should require distance ≤ 1; got {suggestion:?}"
);
}
#[test]
fn completion_candidates_filters_by_prefix() {
let c = crate::app::completion_candidates("ba");
assert!(
c.iter().any(|s| s == "batch-rebuild"),
"expected batch-rebuild among ba-prefixed candidates; got {c:?}"
);
assert!(
c.iter().all(|s| s.starts_with("ba")),
"every candidate must start with the prefix; got {c:?}"
);
assert_eq!(
c.clone(),
{
let mut sorted = c.clone();
sorted.sort();
sorted
},
"candidates must be alphabetically sorted"
);
}
#[test]
fn completion_candidates_with_empty_prefix_returns_full_list() {
let c = crate::app::completion_candidates("");
assert!(
c.len() > 50,
"expected the full command list; got {} entries",
c.len()
);
assert!(c.iter().any(|s| s == "why"));
assert!(c.iter().any(|s| s == "rebuild"));
}
#[tokio::test]
async fn typing_in_command_mode_breaks_the_completion_cycle() {
let mut app = test_app();
app.mode = Mode::Command;
app.command_input = "re".into();
press(&mut app, KeyCode::Tab, KeyModifiers::NONE);
assert!(app.completion.origin.is_some());
press(&mut app, KeyCode::Char('s'), KeyModifiers::NONE);
assert!(
app.completion.origin.is_none(),
"typing must reset the completion origin"
);
}
#[test]
fn command_takes_env_arg_only_for_env_first_commands() {
for c in ["diff", "config-diff", "rds-detach"] {
assert!(
crate::app::command_takes_env_arg(c),
"{c} takes an env name as its first arg"
);
}
for c in [
"why", "deploy", "rebuild", "region", "profile", "view", "save",
] {
assert!(
!crate::app::command_takes_env_arg(c),
"{c} must not offer env-name completion"
);
}
}
#[tokio::test]
async fn command_input_is_cursor_aware_via_shared_textinput() {
let mut app = test_app();
press(&mut app, KeyCode::Char(':'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Command);
for c in "deploy".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
press(&mut app, KeyCode::Char('e'), KeyModifiers::NONE);
assert_eq!(app.command_input.text(), "depleoy");
assert!(app.completion.origin.is_none());
}
#[test]
fn assign_app_colors_wraps_when_palette_exhausted() {
use ratatui::style::Color;
let palette = vec![Color::Red, Color::Green];
let names = ["a", "b", "c", "d"];
let m = assign_app_colors(names.iter().copied(), &palette);
assert_eq!(m.get("a").copied(), Some(Color::Red));
assert_eq!(m.get("b").copied(), Some(Color::Green));
assert_eq!(m.get("c").copied(), Some(Color::Red));
assert_eq!(m.get("d").copied(), Some(Color::Green));
}
#[test]
fn assign_app_colors_empty_palette_yields_empty_map() {
let m = assign_app_colors(["a", "b"].iter().copied(), &[]);
assert!(m.is_empty());
}
#[test]
fn diff_field_ignored_matches_label_and_version_label_alias() {
assert!(!diff_field_ignored("Version", &[]));
let keys = parse_ignore_keys(Some("version, updated"));
assert!(diff_field_ignored("Version", &keys));
assert!(diff_field_ignored("Updated", &keys));
assert!(!diff_field_ignored("Status", &keys));
let alias = parse_ignore_keys(Some("version_label"));
assert!(diff_field_ignored("Version", &alias));
assert!(!diff_field_ignored("Status", &alias));
}
#[tokio::test]
async fn cmd_undo_with_empty_history_hints_at_the_buffer() {
let mut app = test_app();
app.execute_command("undo");
let status = app.status_message.as_deref().unwrap_or("");
assert!(
status.contains("no undo history"),
"expected empty-history hint, got: {status}"
);
}
#[tokio::test]
async fn cmd_undo_with_no_op_reverse_surfaces_clearly() {
let mut app = test_app();
app.environments = vec![mk_env("prod", "shop", "Web", "Green")];
app.rebuild_view();
app.undo_history.push_back(crate::app::UndoEntry {
env_name: "prod".into(),
to_set: vec![],
to_remove: vec![],
original_summary: "keypair foo".into(),
captured_at: chrono::Utc::now(),
});
app.execute_command("undo");
let status = app.status_message.as_deref().unwrap_or("");
assert!(
status.contains("prior state was identical"),
"expected no-op hint, got: {status}"
);
}
#[tokio::test]
async fn cmd_undo_uses_display_row_index_not_envs_vec_index() {
let mut app = test_app();
let mut prod_api = mk_env("prod-api", "shop", "Web", "Green");
prod_api.application = "shop".into();
let mut staging_api = mk_env("staging-api", "shop", "Web", "Green");
staging_api.application = "shop".into();
let mut prod_web = mk_env("prod-web", "shop", "Web", "Green");
prod_web.application = "shop".into();
app.environments = vec![prod_api, staging_api, prod_web];
app.view.set_filter("prod-");
app.rebuild_view();
app.undo_history.push_back(crate::app::UndoEntry {
env_name: "prod-web".into(),
to_set: vec![(
"aws:autoscaling:launchconfiguration".into(),
"EC2KeyName".into(),
"bar".into(),
)],
to_remove: vec![],
original_summary: "keypair foo".into(),
captured_at: chrono::Utc::now(),
});
app.table_state.select(Some(0));
app.execute_command("undo");
assert!(
app.error_message.is_none(),
"expected dispatch to succeed, got error: {:?}",
app.error_message
);
assert_eq!(
app.table_state.selected(),
Some(0),
"cursor must be restored to the prior selection"
);
}
#[tokio::test]
async fn cmd_undo_refuses_with_hint_when_env_filtered_out() {
let mut app = test_app();
app.environments = vec![
mk_env("prod-api", "shop", "Web", "Green"),
mk_env("staging-api", "shop", "Web", "Green"),
];
app.view.set_filter("staging-");
app.rebuild_view();
app.undo_history.push_back(crate::app::UndoEntry {
env_name: "prod-api".into(),
to_set: vec![("ns".into(), "k".into(), "v".into())],
to_remove: vec![],
original_summary: "keypair foo".into(),
captured_at: chrono::Utc::now(),
});
app.execute_command("undo");
let err = app.error_message.as_deref().unwrap_or("");
assert!(
err.contains("filtered out") && err.contains("clear the filter"),
"expected filter hint, got: {err}"
);
assert_eq!(
app.undo_history.len(),
1,
"entry must be put back on the deque so the operator can retry"
);
}
#[tokio::test]
async fn cmd_undo_refuses_when_target_env_no_longer_visible() {
let mut app = test_app();
app.undo_history.push_back(crate::app::UndoEntry {
env_name: "vanished".into(),
to_set: vec![("ns".into(), "k".into(), "v".into())],
to_remove: vec![],
original_summary: "keypair foo".into(),
captured_at: chrono::Utc::now(),
});
app.execute_command("undo");
let err = app.error_message.as_deref().unwrap_or("");
assert!(
err.contains("no longer in the current view"),
"expected missing-env refusal, got: {err}"
);
}
#[test]
fn expand_command_alias_pass_through_when_no_match() {
use std::collections::HashMap;
let mut aliases = HashMap::new();
aliases.insert("dp".to_string(), "deploy --auto-rollback 5m".to_string());
assert_eq!(
crate::app::expand_command_alias("rebuild", &aliases),
"rebuild"
);
assert_eq!(
crate::app::expand_command_alias("deploy build-x", &HashMap::new()),
"deploy build-x"
);
}
#[test]
fn expand_command_alias_swaps_first_token_and_keeps_args() {
use std::collections::HashMap;
let mut aliases = HashMap::new();
aliases.insert("dp".to_string(), "deploy --auto-rollback 5m".to_string());
assert_eq!(
crate::app::expand_command_alias("dp build-900", &aliases),
"deploy --auto-rollback 5m build-900"
);
assert_eq!(
crate::app::expand_command_alias("dp", &aliases),
"deploy --auto-rollback 5m"
);
}
#[test]
fn expand_command_alias_does_not_chain_transitively() {
use std::collections::HashMap;
let mut aliases = HashMap::new();
aliases.insert("a".to_string(), "b stuff".to_string());
aliases.insert("b".to_string(), "c things".to_string());
assert_eq!(crate::app::expand_command_alias("a", &aliases), "b stuff");
let mut aliases = HashMap::new();
aliases.insert("loop".to_string(), "loop forever".to_string());
assert_eq!(
crate::app::expand_command_alias("loop", &aliases),
"loop forever"
);
}
#[tokio::test]
async fn execute_command_uses_command_aliases() {
let mut app = test_app();
app.cfg
.command_aliases
.insert("emergency".into(), "freeze-deploys incident #1234".into());
app.execute_command("emergency");
assert!(app.deploy_freeze.is_some());
let reason = app
.deploy_freeze
.as_ref()
.map(|f| f.reason.clone())
.unwrap();
assert_eq!(reason, "incident #1234");
}
#[tokio::test]
async fn dispatch_auto_rollback_also_drains_watching_deploys() {
let mut app = test_app();
app.environments = vec![mk_env("prod", "shop", "Web", "Red")];
app.rebuild_view();
app.deploy_snapshots.insert(
"prod".into(),
DeploySnapshot {
env_name: "prod".into(),
previous_version_label: "build-820".into(),
taken_at: chrono::Utc::now(),
},
);
app.armed_watchdogs.insert(
"prod".into(),
ArmedWatchdog {
env_name: "prod".into(),
target_label: "build-820".into(),
armed_at: chrono::Utc::now(),
deadline_at: chrono::Utc::now() - chrono::Duration::seconds(1),
},
);
app.watching_deploys.insert(
"prod".into(),
WatchingDeploy {
env_name: "prod".into(),
target_label: "build-900".into(),
armed_at: chrono::Utc::now(),
deadline_at: chrono::Utc::now() + chrono::Duration::seconds(300),
},
);
app.dispatch_auto_rollback("prod".into(), "Red".into());
assert!(
!app.watching_deploys.contains_key("prod"),
"rollback dispatch must drain the parallel wait-for-green watcher"
);
assert!(
!app.armed_watchdogs.contains_key("prod"),
"rollback dispatch must drain its own armed watchdog"
);
}
#[tokio::test]
async fn apply_refresh_dispatches_rollback_when_deadline_passed_and_env_non_green() {
let mut app = test_app();
let now = chrono::Utc::now();
app.armed_watchdogs.insert(
"prod".into(),
ArmedWatchdog {
env_name: "prod".into(),
target_label: "build-old".into(),
armed_at: now - chrono::Duration::seconds(600),
deadline_at: now - chrono::Duration::seconds(1),
},
);
app.deploy_snapshots.insert(
"prod".into(),
DeploySnapshot {
env_name: "prod".into(),
previous_version_label: "build-old".into(),
taken_at: now - chrono::Duration::seconds(600),
},
);
let pending_before = app.pending_actions.len();
app.apply_refresh(
app.fanout_epoch,
Ok(vec![mk_env("prod", "shop", "Web", "Red")]),
Vec::new(),
);
assert!(
app.armed_watchdogs.is_empty(),
"dispatch should drain the watchdog"
);
assert_eq!(
app.pending_actions.len(),
pending_before + 1,
"rollback dispatch should push a pending row"
);
assert!(app
.pending_actions
.iter()
.any(|p| p.label.contains("Auto-rollback") && p.target == "prod"));
let status = app.status_message.as_deref().unwrap_or("");
assert!(status.contains("redeploying build-old"));
assert!(status.contains("Red"));
}
#[tokio::test]
async fn colon_enters_command_mode_and_esc_cancels() {
let mut app = test_app();
press(&mut app, KeyCode::Char(':'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Command);
press(&mut app, KeyCode::Char('q'), KeyModifiers::NONE);
assert_eq!(app.command_input.text(), "q");
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Normal);
assert!(app.command_input.is_empty());
}
#[tokio::test]
async fn cmd_batch_action_refuses_pinned_env_and_keeps_selection() {
let mut app = test_app();
app.cfg.safety_envs.insert("prod-web".into(), true);
app.multi_selected.insert("prod-web".into());
app.multi_selected.insert("staging-web".into());
app.cmd_batch_action(crate::app::Action::Rebuild);
assert!(app.error_message.is_some());
assert_eq!(
app.multi_selected.len(),
2,
"refused batch must preserve the selection for retry"
);
}
#[tokio::test]
async fn render_places_caret_at_cursor_in_command_mode() {
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 "zzqz".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
let at_end = render(&mut app, 120, 30);
assert!(
at_end.contains("zzqz"),
"caret at end keeps 'zzqz' contiguous"
);
press(&mut app, KeyCode::Left, KeyModifiers::NONE);
let mid = render(&mut app, 120, 30);
assert!(
!mid.contains("zzqz"),
"caret should split 'zzq<caret>z' — 'zzqz' no longer contiguous"
);
assert!(mid.contains("zzq"), "text before the caret still renders");
}
#[tokio::test]
async fn palette_input_is_cursor_aware_via_shared_textinput() {
let mut app = test_app();
press(&mut app, KeyCode::Char('k'), KeyModifiers::CONTROL);
assert_eq!(app.mode, Mode::Palette);
for c in "tag".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
assert_eq!(app.palette_input.text(), "tag");
press(&mut app, KeyCode::Home, KeyModifiers::NONE);
press(&mut app, KeyCode::Char('X'), KeyModifiers::NONE);
assert_eq!(app.palette_input.text(), "Xtag");
press(&mut app, KeyCode::End, KeyModifiers::NONE);
press(&mut app, KeyCode::Char('w'), KeyModifiers::CONTROL);
assert_eq!(app.palette_input.text(), "");
}
#[tokio::test]
async fn picker_workflow_open_filter_enter_dispatches_choice() {
let mut app = test_app();
press(&mut app, KeyCode::Char('r'), KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Picker);
assert!(app.picker.is_some());
press(&mut app, KeyCode::Esc, KeyModifiers::NONE);
assert_eq!(app.mode, Mode::Normal);
assert!(app.picker.is_none());
}
#[tokio::test]
async fn queue_action_dispatch_holds_action_for_cancel_window() {
let mut app = test_app();
let modal = mk_modal(Action::Rebuild, "uflexi-prod");
app.queue_action_dispatch(modal);
let pd = app
.pending_dispatch
.as_ref()
.expect("queue should set pending_dispatch");
assert_eq!(pd.target, "uflexi-prod");
assert!(
matches!(pd.kind, PendingDispatchKind::Single { .. }),
"queue_action_dispatch should produce a Single variant"
);
assert!(
pd.deadline > std::time::Instant::now(),
"deadline must be in the future"
);
let remaining = pd
.deadline
.saturating_duration_since(std::time::Instant::now());
assert!(
remaining <= UNDO_WINDOW && remaining >= UNDO_WINDOW - Duration::from_millis(500),
"deadline should be roughly UNDO_WINDOW from now; got {remaining:?}"
);
}
#[tokio::test]
async fn cancel_pending_dispatch_clears_field_and_emits_status() {
let mut app = test_app();
app.queue_action_dispatch(mk_modal(Action::Terminate, "uflexi-prod"));
assert!(app.pending_dispatch.is_some());
app.cancel_pending_dispatch();
assert!(app.pending_dispatch.is_none());
let msg = app.status_message.as_deref().unwrap_or("");
assert!(
msg.contains("undone") && msg.contains("uflexi-prod"),
"status should mention the undo + env; got: {msg:?}"
);
}
#[tokio::test]
async fn tick_pending_dispatch_fires_after_deadline() {
let mut app = test_app();
let modal = mk_modal(Action::Rebuild, "expired");
app.pending_dispatch = Some(PendingDispatch {
deadline: std::time::Instant::now() - Duration::from_millis(1),
label: "Rebuild env".into(),
target: "expired".into(),
kind: PendingDispatchKind::Single { modal },
});
app.tick_pending_dispatch();
assert!(
app.pending_dispatch.is_none(),
"expired tick should clear the field (dispatch handed to spawn_action)"
);
}
#[tokio::test]
async fn capital_u_cancels_pending_dispatch_in_normal_mode() {
let mut app = test_app();
app.queue_action_dispatch(mk_modal(Action::Rebuild, "uflexi-prod"));
assert!(app.pending_dispatch.is_some());
press(&mut app, KeyCode::Char('U'), KeyModifiers::SHIFT);
assert!(
app.pending_dispatch.is_none(),
"capital U in Normal mode should cancel the pending dispatch"
);
}
#[tokio::test]
async fn alias_command_rebuilds_the_view() {
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.view.set_filter("checkout");
app.rebuild_view();
assert!(
app.view.display().is_empty(),
"nothing matches 'checkout' yet"
);
app.execute_command("alias api-prod checkout-service");
assert!(!app.view.is_stale(), ":alias must rebuild the view");
assert_eq!(
app.view.filtered(),
&[0],
"the aliased env should now match the active filter"
);
app.execute_command("alias-drop api-prod");
assert!(!app.view.is_stale(), ":alias-drop must rebuild the view");
assert!(app.view.display().is_empty());
}
#[derive(PartialEq, Debug)]
pub(super) struct Fingerprint {
mode: String,
status: Option<String>,
error: Option<String>,
overlay: Option<String>,
action_flow: bool,
form: bool,
picker: bool,
detail: bool,
dlq: bool,
shell: bool,
quit: bool,
load_state: String,
toasts: usize,
help_topic: String,
events_visible: bool,
palette_items: usize,
redact: bool,
read_only: bool,
scope: String,
sort: String,
filter: String,
grouped: bool,
pending: usize,
envs: usize,
multi_regions: usize,
hidden_cols: usize,
saved_views: usize,
log_tail_task: bool,
}
pub(super) fn fingerprint(app: &App) -> Fingerprint {
Fingerprint {
mode: format!("{:?}", app.mode),
status: app.status_message.clone(),
error: app.error_message.clone(),
overlay: app
.current_overlay
.as_ref()
.map(|o| format!("{:?}", std::mem::discriminant(o))),
action_flow: app.action_flow.is_some(),
form: app.form.is_some(),
picker: app.picker.is_some(),
detail: app.detail.is_some(),
dlq: app.dlq.is_some(),
shell: app.current_shell.is_some(),
quit: app.quit,
load_state: format!("{:?}", app.load_state),
toasts: app.toasts.len(),
help_topic: format!("{:?}", app.help.topic),
events_visible: app.event_panel.visible,
palette_items: app.palette_items.len(),
redact: app.view.redact,
read_only: app.read_only,
scope: format!("{:?}", app.scope),
sort: format!("{:?}/{}", app.view.sort_key(), app.view.sort_desc()),
filter: app.view.filter().text().to_string(),
grouped: app.view.grouped(),
pending: app.pending_actions.len(),
envs: app.environments.len(),
multi_regions: app.multi_regions.len(),
hidden_cols: app.view.hidden_cols.len(),
saved_views: app.saved_views.len(),
log_tail_task: app.log_tail_task.is_some(),
}
}
pub(super) const OBSERVABLE_COMMANDS: &[&str] = &[
"about",
"accounts",
"alarm-history my-alarm",
"alarms",
"apps-info",
"capacity",
"changes",
"clone api-clone",
"cols list",
"config-diff api-staging",
"config-diff-local",
"custom-platforms",
"deselect",
"drop saved1",
"elb-subnets",
"env list",
"envs-by-version build-900",
"event-tail",
"event-time",
"events off",
"export",
"filter saved1",
"filters",
"find-env api",
"group on",
"help",
"history",
"instance-type t3.small",
"json",
"lineage",
"lint",
"listener-edit 443",
"listeners",
"loglevel debug",
"logs-insights fields @message",
"managed-window Mon 3",
"metric list",
"options",
"org-health",
"pending",
"pin",
"plugins",
"profile default",
"promotions",
"quit",
"rds",
"readonly on",
"redact on",
"refresh",
"report",
"report-bug",
"resources",
"rollbacks-armed",
"save saved2",
"save-view v1",
"saved-configs",
"scaling-triggers",
"secret my-secret",
"secrets",
"security-groups",
"settings",
"sort name",
"subnets",
"update",
"upgrade",
"versions",
"view v1",
"view-drop v1",
"views",
"whatsnew",
"why",
"account acct1",
];
fn app_for_command_probe() -> App {
let mut app = test_app();
app.environments = vec![
mk_env("api-prod", "uflexi", "Web", "Green"),
mk_env("api-staging", "uflexi", "Web", "Green"),
];
app.view.invalidate();
app.rebuild_view();
app.table_state.select(Some(0));
app
}
#[tokio::test]
async fn every_command_moves_observable_state() {
for cmd in OBSERVABLE_COMMANDS {
let mut app = app_for_command_probe();
let before = fingerprint(&app);
app.execute_command(cmd);
let after = fingerprint(&app);
assert_ne!(
before, after,
":{cmd} changed nothing an operator could see — a \
short-circuited dispatch arm looks exactly like this"
);
}
}
#[tokio::test]
async fn logs_tail_starts_its_polling_task() {
let mut app = app_for_command_probe();
assert!(app.log_tail_task.is_none());
app.execute_command("logs-tail");
assert!(
app.log_tail_task.is_some(),
":logs-tail started no polling task"
);
}
#[tokio::test]
async fn config_inspect_dispatches_work() {
let mut app = app_for_command_probe();
app.execute_command("config-inspect tpl");
for _ in 0..50 {
if app.msg_rx.try_recv().is_ok() {
return;
}
tokio::task::yield_now().await;
}
panic!(":config-inspect dispatched no work at all");
}
const COVERED_INDIVIDUALLY: &[(&str, &str)] = &[
("logs-tail", "pure spawn — pinned via log_tail_task"),
(
"config-inspect",
"pure spawn — pinned via the message channel",
),
("region", "fan-out epoch tests in app/tests/refresh.rs"),
("restart", "confirm-modal action — arms the right Action"),
("rebuild", "confirm-modal action"),
(
"terminate",
"confirm-modal action, plus the typed-name guard",
),
("stop", "confirm-modal action"),
("start", "confirm-modal action"),
("swap", "needs a second env in the same app before it gates"),
("ssm-run", "needs cached Detail instances before it gates"),
("scale", "GATED_COMMANDS"),
("abort", "GATED_COMMANDS"),
("rollout", "GATED_COMMANDS"),
("deploy", "GATED_COMMANDS"),
("env-edit", "GATED_COMMANDS"),
("rds-attach", "GATED_COMMANDS"),
("delete-version", "GATED_COMMANDS"),
("custom-platform-delete", "GATED_COMMANDS"),
("unset-option", "GATED_COMMANDS"),
("q", "alias of quit"),
("diff", "two-arg diff form tests in app/tests/overlays.rs"),
("ssh", "instance-id and env-name arg tests"),
("explain", "explain-overlay render tests"),
("cost", "cost fetch + truncation tests"),
("fleet-cost", "fleet-cost rollup render tests"),
("abort-rollback", "named-env disarm tests"),
("freeze-deploys", "freeze marker + refusal-message tests"),
("thaw-deploys", "freeze lifecycle tests"),
("incident", "incident start/restart/end arg tests"),
("undo", "undo-history cap tests"),
("drift", "tfstate parse + exit-code tests"),
("promote-env", "promotion lineage tests"),
("rollback", "rollback --to and --auto-rollback tests"),
("alias", "command-alias expansion tests"),
("alias-drop", "command-alias expansion tests"),
];
#[test]
fn every_registry_command_is_covered_by_some_test() {
let src = std::fs::read_to_string("src/commands.rs").expect("read commands.rs");
let start = src
.find("const COMMANDS")
.expect("COMMANDS table in src/commands.rs");
let body = &src[start..src[start..].find("\n];").expect("table end") + start];
let mut registry: Vec<String> = Vec::new();
let lines: Vec<&str> = body.lines().collect();
for (i, line) in lines.iter().enumerate() {
let t = line.trim_start();
let Some(rest) = ["cmd(", "cmd_env_arg(", "cmd_with_aliases("]
.iter()
.find_map(|p| t.strip_prefix(*p))
else {
continue;
};
let name = rest.split('"').nth(1).map(str::to_string).or_else(|| {
lines[i + 1..]
.iter()
.find(|l| l.contains('"'))
.and_then(|l| l.split('"').nth(1).map(str::to_string))
});
if let Some(n) = name {
registry.push(n);
}
}
assert!(
registry.len() > 120,
"parsed only {} commands out of the registry — the parse broke, \
and an empty result would read as a clean pass",
registry.len()
);
let first_word = |s: &&str| s.split_whitespace().next().unwrap_or("").to_string();
let mut covered: std::collections::HashSet<String> = std::collections::HashSet::new();
for list in [
OBSERVABLE_COMMANDS,
GATED_COMMANDS,
WRITE_COMMANDS,
BATCH_WRITE_COMMANDS,
APPLICATION_SCOPED_WRITES,
] {
covered.extend(list.iter().map(first_word));
}
covered.extend(COVERED_INDIVIDUALLY.iter().map(|(c, _)| c.to_string()));
let mut test_src = String::new();
for entry in std::fs::read_dir("src/app/tests").expect("tests dir") {
let path = entry.expect("entry").path();
if path.extension().and_then(|e| e.to_str()) == Some("rs") {
test_src.push_str(&std::fs::read_to_string(&path).expect("read"));
}
}
if let Some(start) = test_src.find("const COVERED_INDIVIDUALLY") {
if let Some(len) = test_src[start..].find("\n];") {
test_src.replace_range(start..start + len, "");
}
}
let word_appears = |needle: &str| {
test_src.match_indices(needle).any(|(i, _)| {
let before = test_src[..i].chars().next_back();
let after = test_src[i + needle.len()..].chars().next();
let boundary =
|c: Option<char>| c.is_none_or(|c| !c.is_alphanumeric() && c != '_' && c != '-');
boundary(before) && boundary(after)
})
};
let unbacked: Vec<&str> = COVERED_INDIVIDUALLY
.iter()
.filter(|(c, _)| !word_appears(c))
.map(|(c, _)| *c)
.collect();
assert!(
unbacked.is_empty(),
"these commands claim individual coverage but their name appears \
nowhere in src/app/tests — the claim has nothing behind it: {unbacked:?}"
);
let missing: Vec<&String> = registry.iter().filter(|c| !covered.contains(*c)).collect();
assert!(
missing.is_empty(),
"these registry commands are in no coverage list — add them to \
OBSERVABLE_COMMANDS, or to COVERED_INDIVIDUALLY with the reason: {missing:?}"
);
}
#[test]
fn every_render_surface_is_accounted_for() {
let mut found: Vec<String> = Vec::new();
let dir = std::path::Path::new("src/ui");
let mut files = vec![std::path::PathBuf::from("src/ui.rs")];
for entry in std::fs::read_dir(dir).expect("src/ui") {
let path = entry.expect("entry").path();
if path.extension().and_then(|e| e.to_str()) == Some("rs") && !is_test_source(&path) {
files.push(path);
}
}
for path in &files {
let text = std::fs::read_to_string(path).expect("read");
for line in text.lines() {
let t = line
.strip_prefix("pub(crate) ")
.or_else(|| line.strip_prefix("pub(super) "))
.or_else(|| line.strip_prefix("pub "))
.unwrap_or(line);
if let Some(rest) = t.strip_prefix("fn draw_") {
let name = rest
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.next()
.unwrap_or("");
found.push(format!("draw_{name}"));
}
}
}
found.sort();
found.dedup();
assert!(
found.len() > 30,
"parsed only {} render surfaces — the parse broke",
found.len()
);
const KNOWN: usize = 41;
assert_eq!(
found.len(),
KNOWN,
"the set of `draw_*` surfaces changed ({} now, {KNOWN} when the \
coverage sweep last ran and reached 41 of 41). If you added one, \
cover it — stub it with an early return and check a test fails — \
then bump KNOWN. If you removed one, just bump KNOWN.\nfound: {found:?}",
found.len()
);
}
pub(super) fn literals_with_embedded_newlines(src: &str) -> Vec<String> {
fn walk(ts: proc_macro2::TokenStream, out: &mut Vec<String>) {
for tree in ts {
match tree {
proc_macro2::TokenTree::Group(g) => walk(g.stream(), out),
proc_macro2::TokenTree::Literal(l) => {
let text = l.to_string();
if !text.starts_with('"') {
continue;
}
if text.starts_with("\"\\\n") {
continue;
}
const SOURCE_INDENT: usize = 2;
let chars: Vec<char> = text.chars().collect();
for (i, c) in chars.iter().enumerate() {
if *c != '\n' || (i > 0 && chars[i - 1] == '\\') {
continue;
}
let run = chars[i + 1..].iter().take_while(|c| **c == ' ').count();
if run >= SOURCE_INDENT {
let preview: String = text.chars().take(70).collect();
out.push(preview.replace('\n', "\\n"));
break;
}
}
}
_ => {}
}
}
}
let Ok(ts) = src.parse::<proc_macro2::TokenStream>() else {
return Vec::new();
};
let mut out = Vec::new();
walk(ts, &mut out);
out
}
#[test]
fn the_wrapped_literal_scanner_is_accurate() {
let bad = "fn f() { let m = \"some text\n more text\"; }";
assert_eq!(
literals_with_embedded_newlines(bad).len(),
1,
"the actual bug: a literal split with no continuation"
);
for ok in [
"fn f() { let m = \"some text \\\n more text\"; }", "fn f() { let u = \"https://sqs.eu-west-2.amazonaws.com/1/q\"; }",
"fn f() { out.push('\"'); }",
"fn f() { let v = raw.trim_matches('\"'); }",
"fn f() { /* a \"quote\" in a comment */ }",
"fn f() { body.push_str(\"ENV TARGET DEADLINE\\n\"); }",
"fn f() { let s = r\"raw\nmultiline\"; }", ] {
assert!(
literals_with_embedded_newlines(ok).is_empty(),
"false positive on: {ok}"
);
}
}
#[test]
fn no_wrapped_string_literal_leaves_an_indentation_hole() {
let mut offenders: Vec<String> = Vec::new();
let mut stack = vec![std::path::PathBuf::from("src")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src dir") {
let path = entry.expect("entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") || is_test_source(&path) {
continue;
}
let text = std::fs::read_to_string(&path).expect("read");
for lit in literals_with_embedded_newlines(&text) {
offenders.push(format!("{}: {lit}", path.display()));
}
}
}
assert!(
offenders.is_empty(),
"string literals with an embedded newline — a wrapped literal \
missing its `\\` continuation embeds the next line's indentation \
too: {offenders:#?}"
);
}
#[test]
fn terminal_restore_goes_through_the_best_effort_helper() {
let mut offenders: Vec<String> = Vec::new();
let mut stack = vec![std::path::PathBuf::from("src")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src dir") {
let path = entry.expect("entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") || is_test_source(&path) {
continue;
}
if path.file_name().and_then(|f| f.to_str()) == Some("lib.rs") {
continue;
}
let text = std::fs::read_to_string(&path).expect("read");
for (n, line) in text.lines().enumerate() {
let code = super::scan::strip_line_comment(line);
if code.contains("disable_raw_mode()?") {
offenders.push(format!("{}:{}", path.display(), n + 1));
}
}
}
}
assert!(
offenders.is_empty(),
"`disable_raw_mode()?` bails before the alternate screen is left \
— use `ebman::restore_terminal`, which attempts every step: \
{offenders:?}"
);
}
#[test]
fn every_cached_index_is_checked() {
let mut offenders: Vec<String> = Vec::new();
let mut stack = vec![std::path::PathBuf::from("src")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src dir") {
let path = entry.expect("entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") || is_test_source(&path) {
continue;
}
if path.ends_with("app/view.rs") {
continue;
}
let text = std::fs::read_to_string(&path).expect("read");
for (n, line) in text.lines().enumerate() {
let code = super::scan::strip_line_comment(line);
if code.contains("environments[") || code.contains("envs[*") {
offenders.push(format!("{}:{}", path.display(), n + 1));
}
}
}
}
assert!(
offenders.is_empty(),
"unchecked index into the env list — a cached view index can \
outlive a mutation of it, and this panics in the alt screen. \
Use `.get()` / `App::env_at`: {offenders:?}"
);
}
#[test]
fn the_test_suite_does_not_mutate_the_environment() {
let mut offenders: Vec<String> = Vec::new();
let mut stack = vec![std::path::PathBuf::from("src")];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("src dir") {
let path = entry.expect("entry").path();
if path.is_dir() {
stack.push(path);
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let text = std::fs::read_to_string(&path).expect("read");
for (n, line) in text.lines().enumerate() {
let code = super::scan::strip_line_comment(line);
let set = format!("env{}set_var", "::");
let remove = format!("env{}remove_var", "::");
if code.contains(&set) || code.contains(&remove) {
offenders.push(format!("{}:{}", path.display(), n + 1));
}
}
}
}
assert!(
offenders.is_empty(),
"`set_var` / `remove_var` are process-global and the suite runs \
in parallel — split the pure half out and pass the value in \
instead: {offenders:?}"
);
}
mod docs_drift {
#[test]
fn every_config_key_is_documented() {
let src = std::fs::read_to_string("src/config.rs").expect("read config.rs");
let docs = std::fs::read_to_string("docs/configuration.md").expect("read configuration.md");
let mut keys: Vec<String> = Vec::new();
for line in src.lines() {
let t = line.trim();
if let Some(rest) = t.strip_prefix('"') {
if let Some((name, tail)) = rest.split_once('"') {
if tail.trim_start().starts_with("=>")
&& !name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_lowercase() || c == '_' || c == '.')
{
keys.push(name.to_string());
}
}
}
}
keys.sort();
keys.dedup();
assert!(
keys.len() > 15,
"found only {} config keys — the extractor is broken, and a guard \
over nothing passes vacuously: {keys:?}",
keys.len()
);
let missing: Vec<&String> = keys.iter().filter(|k| !docs.contains(*k)).collect();
assert!(
missing.is_empty(),
"config keys accepted by the parser but absent from \
docs/configuration.md: {missing:?}"
);
}
#[test]
fn every_subcommand_is_documented() {
let src = std::fs::read_to_string("src/cli/mod.rs").expect("read cli/mod.rs");
let docs = std::fs::read_to_string("docs/headless.md").expect("read headless.md");
let start = src
.find("SUBCOMMANDS")
.expect("SUBCOMMANDS const in src/cli/mod.rs");
let body = &src[start..start + src[start..].find("];").expect("const end")];
let subs: Vec<String> = body
.split('"')
.skip(1)
.step_by(2)
.filter(|s| !s.is_empty() && s.chars().all(|c| c.is_ascii_lowercase()))
.map(str::to_string)
.collect();
assert!(
subs.len() >= 8,
"found only {} subcommands — extractor broken: {subs:?}",
subs.len()
);
let missing: Vec<&String> = subs
.iter()
.filter(|s| !docs.contains(&format!("ebman {s}")))
.collect();
assert!(
missing.is_empty(),
"subcommands advertised by the CLI but absent from \
docs/headless.md: {missing:?}"
);
}
}
mod mutants_found_these {
use super::*;
#[tokio::test]
async fn abort_opens_a_confirm_modal() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "WebServer", "Updating")];
app.rebuild_view();
app.table_state.select(Some(0));
app.execute_command("abort");
let Some(crate::app::ActionFlow::Confirm(modal)) = &app.action_flow else {
panic!("`:abort` must open a confirm modal, got {:?}", app.mode);
};
assert_eq!(modal.action, Action::AbortUpdate);
assert_eq!(modal.target_env, "api-prod");
}
#[tokio::test]
async fn the_confirm_lint_probe_skips_ssm_run_and_demo_mode() {
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_parameterised_action(
Action::SsmRun,
crate::app::ParameterisedAction {
ssm_run_command: Some("uptime".into()),
ssm_run_instances: Some(vec!["i-1".into()]),
..Default::default()
},
);
let Some(crate::app::ActionFlow::Confirm(modal)) = &app.action_flow else {
panic!("ssm-run should open a confirm modal");
};
assert!(
!modal.loading_lint,
"an ad-hoc shell command is not gated by EB-config-health rules"
);
let mut demo = test_app();
demo.demo_mode = true;
demo.environments = vec![mk_env("api-prod", "uflexi", "WebServer", "Green")];
demo.rebuild_view();
demo.table_state.select(Some(0));
demo.open_parameterised_action(Action::Rebuild, Default::default());
assert!(
demo.action_flow.is_none(),
"demo mode must refuse the write outright, not open a modal"
);
let mut real = test_app();
real.environments = vec![mk_env("api-prod", "uflexi", "WebServer", "Green")];
real.rebuild_view();
real.table_state.select(Some(0));
real.open_parameterised_action(Action::Rebuild, Default::default());
let Some(crate::app::ActionFlow::Confirm(modal)) = &real.action_flow else {
panic!("rebuild should open a confirm modal");
};
assert!(
modal.loading_lint,
"a real rebuild SHOULD arm the lint probe — without this the \
exclusions above prove nothing"
);
}
}