use super::super::*;
#[allow(unused_imports)]
use super::support::*;
#[test]
fn action_destructive_covers_terminate_and_ssm_run() {
assert!(Action::Terminate.destructive());
assert!(Action::SsmRun.destructive());
assert!(!Action::Rebuild.destructive());
assert!(!Action::RestartAppServer.destructive());
assert!(!Action::SwapCnames.destructive());
assert!(!Action::Deploy.destructive());
assert!(!Action::UpgradePlatform.destructive());
assert!(!Action::Clone.destructive());
assert!(!Action::Scale.destructive());
assert!(!Action::Capacity.destructive());
assert!(!Action::AbortUpdate.destructive());
assert!(!Action::ConfigSave.destructive());
assert!(!Action::ConfigDelete.destructive());
assert!(!Action::ConfigApply.destructive());
assert!(!Action::TerminateInstance.destructive());
}
#[test]
fn deny_write_refuses_in_demo_mode_even_when_not_read_only() {
let mut app = test_app();
app.demo_mode = true;
assert!(!app.read_only);
let denied = app.deny_write("any-env", "rebuild");
assert!(denied, "demo mode must deny writes");
let err = app
.error_message
.as_deref()
.expect("demo-mode deny_write should set error_message");
assert!(
err.contains("demo mode"),
"expected demo-mode reason in toast, got: {err}"
);
assert!(
!err.contains("would also refuse"),
"no pin configured → no compose suffix, got: {err}"
);
}
#[test]
fn deny_write_demo_mode_composes_pin_reason_in_toast() {
let mut app = test_app();
app.demo_mode = true;
app.cfg.safety_envs.insert("prod-eu-1".into(), true);
let denied = app.deny_write("prod-eu-1", "rebuild");
assert!(denied);
let err = app.error_message.as_deref().unwrap();
assert!(err.contains("demo mode"), "got: {err}");
assert!(
err.contains("would also refuse"),
"expected pin compose suffix, got: {err}"
);
assert!(
err.contains("safety.envs.prod-eu-1"),
"expected pin source in suffix, got: {err}"
);
}
#[test]
fn deny_write_allows_writes_when_not_demo_and_not_read_only() {
let mut app = test_app();
let denied = app.deny_write("any-env", "rebuild");
assert!(!denied, "non-demo non-readonly path must allow writes");
assert!(
app.error_message.is_none(),
"no error toast on allowed write, got: {:?}",
app.error_message
);
}
#[tokio::test]
async fn rollback_to_label_opens_confirm_for_named_label() {
let mut app = test_app();
app.environments = vec![mk_env("prod", "shop", "Web", "Red")];
app.rebuild_view();
app.table_state.select(Some(0));
app.deploy_snapshots.insert(
"prod".into(),
DeploySnapshot {
env_name: "prod".into(),
previous_version_label: "build-snap".into(),
taken_at: chrono::Utc::now(),
},
);
app.execute_command("rollback --to build-820");
match &app.action_flow {
Some(ActionFlow::Confirm(modal)) => {
assert_eq!(modal.params.deploy_version.as_deref(), Some("build-820"));
assert!(modal.params.auto_rollback_secs.is_none());
}
_ => panic!("expected confirm modal open"),
}
}
#[tokio::test]
async fn freeze_deploys_blocks_writes_with_reason_surfaced() {
let _marker_guard = crate::freeze::MARKER_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut app = test_app();
app.execute_command("freeze-deploys incident #1234");
assert!(app.deploy_freeze.is_some(), "freeze should be set");
assert!(
app.is_read_only_for("any-env"),
"freeze must block every env"
);
let reason = app.read_only_reason("any-env").unwrap_or_default();
assert!(
reason.contains("deploys frozen") && reason.contains("incident #1234"),
"expected reason to surface, got: {reason}"
);
}
#[tokio::test]
async fn freeze_deploys_with_no_reason_still_blocks() {
let _marker_guard = crate::freeze::MARKER_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut app = test_app();
app.execute_command("freeze-deploys");
assert!(app.deploy_freeze.is_some());
let reason = app.read_only_reason("env").unwrap_or_default();
assert!(
reason.contains("deploys frozen") && !reason.contains(": "),
"no-reason wording shouldn't include `: <reason>`, got: {reason}"
);
}
#[tokio::test]
async fn thaw_deploys_clears_the_freeze() {
let _marker_guard = crate::freeze::MARKER_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut app = test_app();
app.execute_command("freeze-deploys testing");
assert!(app.deploy_freeze.is_some());
app.execute_command("thaw-deploys");
assert!(app.deploy_freeze.is_none(), "thaw should clear freeze");
assert!(
!app.is_read_only_for("env"),
"thaw must restore writes (no other locks set in this test)"
);
}
#[tokio::test]
async fn freeze_overrides_per_env_pin_in_read_only_reason() {
let _marker_guard = crate::freeze::MARKER_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut app = test_app();
app.cfg.safety_envs.insert("prod".into(), true);
app.execute_command("freeze-deploys incident");
let reason = app.read_only_reason("prod").unwrap_or_default();
assert!(
reason.contains("deploys frozen"),
"freeze reason must win over per-env pin, got: {reason}"
);
}
#[tokio::test]
async fn incident_start_freezes_and_end_thaws() {
let _marker_guard = crate::freeze::MARKER_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let mut app = test_app();
app.execute_command("incident START \"checkout 5xx spike\"");
assert!(app.incident.is_some(), "incident should be active");
assert!(app.deploy_freeze.is_some(), "START must set the freeze");
assert!(
app.is_read_only_for("any-env"),
"incident freeze blocks every env"
);
let reason = app.read_only_reason("any-env").unwrap_or_default();
assert!(
reason.contains("incident: checkout 5xx spike"),
"freeze reason carries the headline, got: {reason}"
);
app.execute_command("incident END");
assert!(app.incident.is_none(), "END clears the incident");
assert!(app.deploy_freeze.is_none(), "END thaws deploys");
let status = app.status_message.clone().unwrap_or_default();
assert!(
status.contains("incident closed") && status.contains("checkout 5xx spike"),
"END summary names the incident, got: {status}"
);
}
#[tokio::test]
async fn promote_env_opens_deploy_confirm_on_target_with_sources_version() {
let mut app = test_app();
let mut staging = mk_env("staging", "shop", "Web", "Green");
staging.version_label = "build-900".into();
let mut prod = mk_env("prod", "shop", "Web", "Green");
prod.version_label = "build-820".into();
app.environments = vec![staging, prod];
app.rebuild_view();
app.table_state.select(Some(0));
app.execute_command("promote-env staging prod");
match &app.action_flow {
Some(ActionFlow::Confirm(modal)) => {
assert_eq!(modal.target_env, "prod");
assert_eq!(modal.params.deploy_version.as_deref(), Some("build-900"));
assert!(matches!(modal.action, Action::Deploy));
}
_ => panic!("expected confirm modal open on target"),
}
}
#[tokio::test]
async fn handle_confirm_modal_lint_stuffs_issues_into_modal() {
let mut app = test_app();
app.environments = vec![mk_env("prod", "shop", "Web", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
app.execute_command("deploy build-900");
app.handle_msg(AppMsg::ConfirmModalLint {
gen: app.generation,
env_name: "prod".into(),
issues: vec![],
});
match &app.action_flow {
Some(ActionFlow::Confirm(modal)) => {
assert!(!modal.loading_lint, "loading flag should clear");
assert_eq!(modal.lint_issues.as_ref().map(|v| v.len()), Some(0));
}
_ => panic!("expected confirm modal open"),
}
}
#[tokio::test]
async fn handle_confirm_modal_lint_drops_stale_target_results() {
let mut app = test_app();
app.environments = vec![
mk_env("prod", "shop", "Web", "Green"),
mk_env("staging", "shop", "Web", "Green"),
];
app.rebuild_view();
app.table_state.select(Some(1));
app.execute_command("deploy build-900");
app.handle_msg(AppMsg::ConfirmModalLint {
gen: app.generation,
env_name: "prod".into(),
issues: vec![crate::lint::Issue {
rule_id: "EBL001".into(),
severity: crate::lint::Severity::Warn,
env_name: Some("prod".into()),
title: "stale".into(),
detail: "stale".into(),
suggestion: None,
fields: Default::default(),
}],
});
match &app.action_flow {
Some(ActionFlow::Confirm(modal)) => {
assert!(
modal.loading_lint,
"loading flag must stay true on stale result"
);
assert!(
modal.lint_issues.is_none(),
"stale result must not populate"
);
}
_ => panic!("expected confirm modal open"),
}
}
#[tokio::test]
async fn is_read_only_for_layers_global_env_and_account() {
let mut app = test_app();
app.read_only = true;
assert!(app.is_read_only_for("any-env"));
assert!(app.read_only_reason("any-env").unwrap().contains("global"));
let mut app = test_app();
app.cfg.safety_envs.insert("uflexi-prod".into(), true);
app.cfg.safety_envs.insert("uflexi-staging".into(), false);
assert!(app.is_read_only_for("uflexi-prod"));
assert!(!app.is_read_only_for("uflexi-staging"));
assert!(!app.is_read_only_for("uflexi-dev"));
assert!(app
.read_only_reason("uflexi-prod")
.unwrap()
.contains("safety.envs.uflexi-prod"));
let mut app = test_app();
app.context.profile = Some("prod-acct".into());
app.cfg.safety_accounts.insert("prod-acct".into(), true);
assert!(app.is_read_only_for("any-env"));
assert!(app
.read_only_reason("any-env")
.unwrap()
.contains("safety.accounts.prod-acct"));
app.context.profile = Some("dev-acct".into());
assert!(!app.is_read_only_for("any-env"));
let app = test_app();
assert!(!app.is_read_only_for("any-env"));
assert!(app.read_only_reason("any-env").is_none());
}
#[tokio::test]
async fn deny_write_batch_refuses_when_any_selected_env_is_pinned() {
let mut app = test_app();
app.cfg.safety_envs.insert("prod-web".into(), true);
let selection = vec!["staging-web".to_string(), "prod-web".to_string()];
assert!(
app.deny_write_batch(&selection, "batch action"),
"a pinned env in the selection must refuse the batch"
);
let msg = app.error_message.clone().unwrap();
assert!(msg.contains("prod-web"), "got: {msg}");
assert!(msg.contains("safety.envs.prod-web"), "got: {msg}");
assert!(msg.contains("1 of 2"), "got: {msg}");
}
#[tokio::test]
async fn deny_write_batch_allows_when_no_env_pinned() {
let mut app = test_app();
app.cfg.safety_envs.insert("other-env".into(), true); let selection = vec!["staging-web".to_string(), "dev-web".to_string()];
assert!(
!app.deny_write_batch(&selection, "batch action"),
"no selected env pinned → batch proceeds"
);
assert!(app.error_message.is_none());
}
#[tokio::test]
async fn deny_write_batch_global_flag_uses_fleet_message_not_per_env_list() {
let mut app = test_app();
app.read_only = true;
let selection = vec!["a".to_string(), "b".to_string()];
assert!(app.deny_write_batch(&selection, "batch action"));
let msg = app.error_message.clone().unwrap();
assert!(msg.contains("read-only mode"), "got: {msg}");
assert!(!msg.contains(" of "), "got: {msg}");
}
#[tokio::test]
async fn render_greens_type_to_confirm_only_on_exact_match() {
let mut app = test_app();
app.environments = vec![mk_env("prod", "uflexi", "Web", "Red")];
app.rebuild_view();
app.mode = Mode::Action;
let mut modal = mk_modal(Action::Terminate, "prod");
modal.kind = ConfirmKind::TypeName;
let theme = app.theme.clone();
modal.typed = "pro".into();
app.action_flow = Some(ActionFlow::Confirm(modal.clone()));
let no_match_green = count_fg(&render_buf(&mut app, 120, 30), theme.health_green);
modal.typed = "prod".into();
app.action_flow = Some(ActionFlow::Confirm(modal));
let matched_green = count_fg(&render_buf(&mut app, 120, 30), theme.health_green);
assert!(
matched_green > no_match_green,
"exact type-to-confirm match should paint more green than a partial match \
(matched={matched_green}, no_match={no_match_green})"
);
}
#[tokio::test]
async fn every_write_command_is_refused_in_read_only_mode() {
for cmd in WRITE_COMMANDS.iter().chain(APPLICATION_SCOPED_WRITES) {
let mut app = read_only_app_with_env();
app.execute_command(cmd);
let err = app.error_message.as_deref().unwrap_or_default();
assert!(
err.contains("read-only mode"),
":{cmd} was not refused by the safety gate — got {err:?}\n\
(a write that doesn't reach `deny_write` ignores --deny-write \
and safety.envs.*.read_only)"
);
}
}
#[tokio::test]
async fn an_application_scoped_write_still_honours_the_global_toggle() {
for cmd in APPLICATION_SCOPED_WRITES {
let mut app = read_only_app_with_env();
app.execute_command(cmd);
let err = app.error_message.as_deref().unwrap_or_default();
assert!(err.contains("read-only mode"), ":{cmd} — got {err:?}");
}
}
#[tokio::test]
async fn every_write_command_is_refused_by_a_per_env_safety_pin() {
for cmd in WRITE_COMMANDS {
let mut app = read_only_app_with_env();
app.read_only = false;
app.cfg.safety_envs.insert("api-prod".into(), true);
app.execute_command(cmd);
let err = app.error_message.as_deref().unwrap_or_default();
assert!(
err.contains("safety.envs"),
":{cmd} ignored the per-env safety pin — got {err:?}"
);
}
}
#[tokio::test]
async fn every_batch_write_is_refused_in_read_only_mode() {
for cmd in BATCH_WRITE_COMMANDS {
let mut app = read_only_app_with_env();
app.multi_selected = ["api-prod".to_string()].into_iter().collect();
app.execute_command(cmd);
let err = app.error_message.as_deref().unwrap_or_default();
assert!(
err.contains("read-only"),
":{cmd} was not refused by the batch safety gate — got {err:?}"
);
}
}
#[tokio::test]
async fn every_batch_write_is_refused_when_one_member_is_pinned() {
for cmd in BATCH_WRITE_COMMANDS {
let mut app = read_only_app_with_env();
app.environments = vec![
mk_env("api-prod", "uflexi", "Web", "Green"),
mk_env("api-staging", "uflexi", "Web", "Green"),
];
app.rebuild_view();
app.table_state.select(Some(0));
app.read_only = false;
app.cfg.safety_envs.insert("api-prod".into(), true);
app.multi_selected = ["api-prod".to_string(), "api-staging".to_string()]
.into_iter()
.collect();
app.execute_command(cmd);
let err = app.error_message.as_deref().unwrap_or_default();
assert!(
err.contains("safety.envs"),
":{cmd} wrote to a batch containing a pinned env — got {err:?}"
);
}
}
#[tokio::test]
async fn the_confirm_modal_commands_arm_the_right_action() {
for (cmd, expected) in [
("restart", Action::RestartAppServer),
("rebuild", Action::Rebuild),
("stop", Action::Scale),
("start", Action::Scale),
] {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.view.invalidate();
app.rebuild_view();
app.table_state.select(Some(0));
app.execute_command(cmd);
let Some(ActionFlow::Confirm(modal)) = app.action_flow.as_ref() else {
panic!(
":{cmd} armed no confirm modal at all (error: {:?})",
app.error_message
);
};
assert_eq!(modal.action, expected, ":{cmd} armed the wrong action");
assert_eq!(
modal.target_env, "api-prod",
":{cmd} aimed at the wrong env"
);
assert_eq!(app.mode, Mode::Action, ":{cmd} left the mode behind");
}
}
#[tokio::test]
async fn terminate_routes_to_the_strict_typed_name_guard() {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.view.invalidate();
app.rebuild_view();
app.table_state.select(Some(0));
app.execute_command("terminate");
let Some(ActionFlow::Confirm(modal)) = app.action_flow.as_ref() else {
panic!(
":terminate armed no confirm at all (error: {:?})",
app.error_message
);
};
assert_eq!(modal.action, Action::Terminate);
assert_eq!(modal.target_env, "api-prod");
assert_eq!(
modal.kind,
ConfirmKind::TypeName,
":terminate must demand the typed env name, not a Y/N"
);
}
#[tokio::test]
async fn destructive_commands_still_refuse_under_deny_write() {
for cmd in ["restart", "rebuild", "terminate", "stop", "start"] {
let mut app = read_only_app_with_env();
app.execute_command(cmd);
assert!(
app.action_flow.is_none(),
":{cmd} armed an action despite --deny-write"
);
assert!(
app.error_message.is_some(),
":{cmd} refused silently — the operator needs to be told"
);
}
}
#[tokio::test]
async fn every_gated_command_is_refused_in_read_only_mode() {
for cmd in GATED_COMMANDS {
let mut app = read_only_app_with_env();
app.execute_command(cmd);
let err = app.error_message.as_deref().unwrap_or_default();
assert!(
err.contains("read-only mode"),
":{cmd} was not refused by the safety gate — got {err:?}"
);
}
}
#[tokio::test]
async fn swap_is_refused_in_read_only_mode() {
let mut app = read_only_app_with_env();
app.environments
.push(mk_env("api-staging", "uflexi", "Web", "Green"));
app.view.invalidate();
app.rebuild_view();
app.table_state.select(Some(0));
app.execute_command("swap api-staging");
let err = app.error_message.as_deref().unwrap_or_default();
assert!(err.contains("read-only mode"), ":swap got {err:?}");
}
#[tokio::test]
async fn ssm_run_is_refused_in_read_only_mode() {
let mut app = read_only_app_with_env();
app.open_detail();
if let Some(d) = app.detail.as_mut() {
d.instances = vec![crate::aws::Instance {
id: "i-0abc".into(),
health: "Ok".into(),
color: "Green".into(),
causes: Vec::new(),
instance_type: "t3.medium".into(),
availability_zone: "eu-west-2a".into(),
launched_at: None,
}];
}
app.execute_command("ssm-run uptime");
let err = app.error_message.as_deref().unwrap_or_default();
assert!(err.contains("read-only mode"), ":ssm-run got {err:?}");
}
#[tokio::test]
async fn a_read_only_env_cannot_be_swapped_into() {
let mut app = test_app();
app.environments = vec![
mk_env("blue", "shop", "WebServer", "Green"),
mk_env("green", "shop", "WebServer", "Green"),
];
app.rebuild_view();
app.cfg.safety_envs.insert("green".into(), true);
assert!(app.is_read_only_for("green"), "the pin must be in effect");
assert!(!app.is_read_only_for("blue"), "the source is writable");
app.table_state.select(Some(0));
assert!(app.open_action_menu(), "`blue` is writable");
app.advance_action_flow(crate::app::Action::SwapCnames);
let picking = matches!(
app.action_flow,
Some(crate::app::ActionFlow::SwapTarget { .. })
);
assert!(picking, "swap opens a target picker");
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
let confirmed = matches!(app.action_flow, Some(crate::app::ActionFlow::Confirm(_)));
assert!(
!confirmed,
"a swap INTO a read-only env must be refused before the confirm \
modal, not dispatched"
);
let msg = app
.error_message
.clone()
.or_else(|| app.status_message.clone())
.unwrap_or_default();
assert!(
msg.contains("green"),
"the refusal must name the pinned env so the operator knows which \
pin stopped it, got: {msg:?}"
);
}
#[tokio::test]
async fn swap_cnames_command_also_gates_the_target() {
let mut app = test_app();
app.environments = vec![
mk_env("blue", "shop", "WebServer", "Green"),
mk_env("green", "shop", "WebServer", "Green"),
];
app.rebuild_view();
app.cfg.safety_envs.insert("green".into(), true);
app.table_state.select(Some(0));
app.execute_command("swap green");
let confirmed = matches!(app.action_flow, Some(crate::app::ActionFlow::Confirm(_)));
assert!(
!confirmed,
"`:swap green` must be refused when `green` is pinned \
read-only, even though the selected env `blue` is writable"
);
let msg = app
.error_message
.clone()
.or_else(|| app.status_message.clone())
.unwrap_or_default();
assert!(
msg.contains("green"),
"the refusal must name the pinned env, got: {msg:?}"
);
}
#[tokio::test]
async fn the_swap_picker_carries_its_target_into_the_confirm_modal() {
let mut app = test_app();
app.environments = vec![
mk_env("blue", "shop", "WebServer", "Green"),
mk_env("green", "shop", "WebServer", "Green"),
];
app.rebuild_view();
app.table_state.select(Some(0));
assert!(app.open_action_menu());
app.advance_action_flow(crate::app::Action::SwapCnames);
assert!(
matches!(
app.action_flow,
Some(crate::app::ActionFlow::SwapTarget { .. })
),
"swap opens a target picker"
);
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
let Some(crate::app::ActionFlow::Confirm(modal)) = &app.action_flow else {
panic!("choosing a target must open the confirm modal");
};
assert_eq!(
modal.action,
crate::app::Action::SwapCnames,
"and it must be the swap it was opened for"
);
assert_eq!(
modal.params.swap_with.as_deref(),
Some("green"),
"the chosen target must ride on the modal — without it the dispatch \
reaches `swap target missing` only after the operator confirms"
);
assert_eq!(
modal.target_env, "blue",
"and the source must be the selected env, not the picked one"
);
}
#[tokio::test]
async fn a_rollback_result_does_not_target_whatever_env_is_selected_now() {
let mut app = test_app();
app.environments = vec![
mk_env("api-prod", "shop", "WebServer", "Green"),
mk_env("worker-prod", "shop", "Worker", "Green"),
];
app.rebuild_view();
app.table_state.select(Some(0));
let ev = |vl: Option<&str>| crate::aws::Event {
at: None,
env: "api-prod".into(),
application: "shop".into(),
message: "Deploying new version".into(),
severity: "INFO".into(),
version_label: vl.map(String::from),
};
let events = vec![ev(Some("v2")), ev(Some("v1"))];
app.table_state.select(Some(1));
let gen = app.generation;
app.handle_msg(crate::app::AppMsg::RollbackTarget {
gen,
env_name: "api-prod".to_string(),
current_version: "v2".to_string(),
result: Ok(events),
});
assert!(
app.action_flow.is_none(),
"no confirm modal may open: the modal targets the SELECTED env, \
which is now worker-prod, and this result is about api-prod"
);
let msg = app.error_message.clone().unwrap_or_default();
assert!(
msg.contains("selection moved"),
"and the operator must be told why nothing happened, got: {msg:?}"
);
}
#[tokio::test]
async fn a_rollback_result_opens_the_modal_when_the_cursor_stayed_put() {
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 ev = |vl: Option<&str>| crate::aws::Event {
at: None,
env: "api-prod".into(),
application: "shop".into(),
message: "Deploying new version".into(),
severity: "INFO".into(),
version_label: vl.map(String::from),
};
let gen = app.generation;
app.handle_msg(crate::app::AppMsg::RollbackTarget {
gen,
env_name: "api-prod".to_string(),
current_version: "v2".to_string(),
result: Ok(vec![ev(Some("v2")), ev(Some("v1"))]),
});
let Some(crate::app::ActionFlow::Confirm(modal)) = &app.action_flow else {
panic!("the rollback should open a deploy-confirm modal");
};
assert_eq!(modal.target_env, "api-prod");
assert_eq!(
modal.params.deploy_version.as_deref(),
Some("v1"),
"and it must offer the PREVIOUS version, not the current one"
);
}
#[tokio::test]
async fn terminate_requires_the_env_name_typed_exactly() {
async fn modal_for_terminate() -> App {
let mut app = test_app();
app.environments = vec![mk_env("api-prod", "shop", "WebServer", "Green")];
app.rebuild_view();
app.table_state.select(Some(0));
assert!(app.open_action_menu(), "the action menu should open");
app.advance_action_flow(crate::app::Action::Terminate);
let Some(crate::app::ActionFlow::Confirm(m)) = &app.action_flow else {
panic!("terminate should open a confirm modal");
};
assert_eq!(
m.kind,
crate::app::ConfirmKind::TypeName,
"terminate must be type-to-confirm, not Y/N"
);
app
}
let mut app = modal_for_terminate().await;
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
assert!(
app.pending_dispatch.is_none(),
"Enter on an EMPTY type-to-confirm field must not queue a terminate"
);
assert!(app.action_flow.is_some(), "and the modal must stay open");
let mut app = modal_for_terminate().await;
for c in "api-prd".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
assert!(
app.pending_dispatch.is_none(),
"a MISTYPED env name must not queue a terminate — this is the guard \
the modal's own comment calls the thing that prevents accidental \
dispatch"
);
let mut app = modal_for_terminate().await;
for c in "api-prod".chars() {
press(&mut app, KeyCode::Char(c), KeyModifiers::NONE);
}
press(&mut app, KeyCode::Enter, KeyModifiers::NONE);
assert!(
app.pending_dispatch.is_some(),
"the EXACT env name must confirm, or terminate is unreachable"
);
assert!(
app.action_flow.is_none(),
"and the modal closes into the cancel window"
);
}