use super::super::*;
#[allow(unused_imports)]
use super::support::*;
#[test]
fn render_fleet_cost_breaks_down_by_app_tier_health() {
let envs = vec![
mk_env("api-prod", "api", "Web", "Green"),
mk_env("api-staging", "api", "Web", "Yellow"),
mk_env("worker-prod", "api", "Worker", "Green"),
mk_env("billing-prod", "billing", "Web", "Red"),
];
let mut costs = std::collections::HashMap::new();
costs.insert("api-prod".to_string(), 100.0);
costs.insert("api-staging".to_string(), 25.5);
costs.insert("worker-prod".to_string(), 40.0);
costs.insert("billing-prod".to_string(), 60.25);
let now = chrono::Utc::now();
let body = crate::app::render_fleet_cost(&envs, &costs, Some(now), now);
assert!(body.contains("Total: $225.75/mo"), "total: {body}");
assert!(body.contains("4 env(s) covered"), "covered count: {body}");
assert!(body.contains("$ 165.50/mo api"), "by app: {body}");
assert!(body.contains("$ 60.25/mo billing"), "by app: {body}");
assert!(body.contains("$ 185.75/mo Web"), "by tier: {body}");
assert!(body.contains("$ 40.00/mo Worker"), "by tier: {body}");
assert!(body.contains("$ 140.00/mo Green"), "by health: {body}");
}
#[test]
fn render_fleet_cost_flags_uncovered_envs() {
let envs = vec![
mk_env("api-prod", "api", "Web", "Green"),
mk_env("api-uncached", "api", "Web", "Green"),
];
let mut costs = std::collections::HashMap::new();
costs.insert("api-prod".to_string(), 50.0);
let now = chrono::Utc::now();
let body = crate::app::render_fleet_cost(&envs, &costs, Some(now), now);
assert!(
body.contains("1 env(s) covered, 1 without cost data"),
"missing count: {body}"
);
}
#[test]
fn render_fleet_cost_flags_stale_cache() {
let envs = vec![mk_env("a", "x", "Web", "Green")];
let mut costs = std::collections::HashMap::new();
costs.insert("a".to_string(), 10.0);
let now = chrono::Utc::now();
let stale = now - chrono::Duration::hours(36);
let body = crate::app::render_fleet_cost(&envs, &costs, Some(stale), now);
assert!(body.contains("stale"), "stale marker: {body}");
}
#[test]
fn render_fleet_cost_no_freshness_line_when_unset() {
let envs = vec![mk_env("a", "x", "Web", "Green")];
let mut costs = std::collections::HashMap::new();
costs.insert("a".to_string(), 10.0);
let now = chrono::Utc::now();
let body = crate::app::render_fleet_cost(&envs, &costs, None, now);
assert!(!body.contains("Cached:"), "no cached line: {body}");
assert!(body.contains("Total: $10.00/mo"));
}
#[test]
fn instance_hourly_usd_known_types() {
assert!(instance_hourly_usd("t3.micro").unwrap() > 0.0);
assert!(instance_hourly_usd("m5.large").unwrap() > 0.0);
assert_eq!(instance_hourly_usd("not-a-real-type"), None);
}
#[test]
fn estimate_cost_handles_mixed() {
let mk = |t: &str, az: &str| Instance {
id: "i-1".into(),
health: "Ok".into(),
color: "Green".into(),
causes: vec![],
instance_type: t.into(),
availability_zone: az.into(),
launched_at: None,
};
let instances = vec![
mk("t3.micro", "us-east-1a"),
mk("t3.micro", "us-east-1b"),
mk("unknown-type-xyz", "us-east-1c"),
];
let (hourly, missing) = estimate_cost(&instances);
assert_eq!(missing, 1);
assert!((hourly - 0.0208).abs() < 1e-9);
}
#[tokio::test]
async fn render_cost_column_red_tints_expensive_envs() {
let mut app = test_app();
crate::demo_fixture::install(&mut app);
app.table_state.select(None);
let theme = app.theme.clone();
let buf = render_buf(&mut app, 160, 30);
let pricey = find_row(&buf, "poly-prod-api").expect("prod-api row");
let cheap = find_row(&buf, "poly-staging-worker").expect("staging-worker row");
assert!(
row_has_fg(&buf, pricey, theme.health_red),
"the $612 env should paint a health_red COST cell"
);
assert!(
!row_has_fg(&buf, cheap, theme.health_red),
"a cheap green-health env row should have no red cell"
);
}
#[tokio::test]
async fn a_truncated_cost_refresh_keeps_the_previous_map() {
let mut app = test_app();
app.costs.insert("api-prod".into(), 100.0);
app.costs.insert("web-prod".into(), 200.0);
app.costs.insert("worker-prod".into(), 50.0);
let before = app.costs.clone();
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: Some("123456789012".into()),
region: "us-east-1".into(),
result: Ok(crate::aws::EnvCosts {
rows: vec![crate::aws::EnvCost {
env_name: "api-prod".into(),
cost_usd: 111.0,
}],
truncated: true,
}),
});
assert_eq!(
app.costs, before,
"a partial walk must not replace a good map"
);
let msg = app.error_message.as_deref().expect("must say so");
assert!(msg.contains("INCOMPLETE"), "{msg}");
assert_no_run_on_spaces(msg);
}
#[tokio::test]
async fn a_truncated_cost_refresh_with_nothing_cached_shows_what_it_has() {
let mut app = test_app();
assert!(app.costs.is_empty());
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(crate::aws::EnvCosts {
rows: vec![crate::aws::EnvCost {
env_name: "api-prod".into(),
cost_usd: 111.0,
}],
truncated: true,
}),
});
assert_eq!(app.costs.len(), 1, "partial data still renders");
assert!(
app.costs_fetched_at.is_none(),
"an incomplete walk must not stamp a fetch time"
);
let msg = app.error_message.as_deref().expect("must say so");
assert!(msg.contains("INCOMPLETE"), "{msg}");
assert_no_run_on_spaces(msg);
}
#[tokio::test]
async fn a_complete_cost_refresh_replaces_the_map() {
let mut app = test_app();
app.costs.insert("stale".into(), 999.0);
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(crate::aws::EnvCosts {
rows: vec![crate::aws::EnvCost {
env_name: "api-prod".into(),
cost_usd: 111.0,
}],
truncated: false,
}),
});
assert_eq!(app.costs.len(), 1);
assert!(app.costs.contains_key("api-prod"));
assert!(!app.costs.contains_key("stale"), "a complete walk replaces");
assert!(app.costs_fetched_at.is_some());
}
#[tokio::test]
async fn a_partial_cost_map_does_not_become_permanent() {
let mut app = test_app();
assert!(app.costs.is_empty());
let truncated = |env: &str, usd: f64| crate::aws::EnvCosts {
rows: vec![crate::aws::EnvCost {
env_name: env.into(),
cost_usd: usd,
}],
truncated: true,
};
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(truncated("api-prod", 100.0)),
});
assert!(!app.costs_complete, "a truncated walk is not complete");
assert_eq!(app.costs.get("api-prod"), Some(&100.0));
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(truncated("web-prod", 55.0)),
});
assert!(!app.costs_complete);
assert_eq!(app.costs.get("web-prod"), Some(&55.0));
assert!(
!app.costs.contains_key("api-prod"),
"the stale partial map must not accumulate"
);
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(crate::aws::EnvCosts {
rows: vec![crate::aws::EnvCost {
env_name: "full".into(),
cost_usd: 1.0,
}],
truncated: false,
}),
});
assert!(app.costs_complete);
assert_eq!(app.costs.len(), 1);
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(truncated("partial", 9.0)),
});
assert!(
app.costs_complete,
"a complete map survives a truncated walk"
);
assert_eq!(app.costs.get("full"), Some(&1.0));
}
#[tokio::test]
async fn cost_status_and_fleet_cost_say_when_the_data_is_partial() {
let mut app = test_app();
app.cost_enabled = true;
app.environments = vec![mk_env("api-prod", "uflexi", "Web", "Green")];
app.rebuild_view();
app.handle_msg(AppMsg::CostsFetched {
gen: app.generation,
account: None,
region: "us-east-1".into(),
result: Ok(crate::aws::EnvCosts {
rows: vec![crate::aws::EnvCost {
env_name: "api-prod".into(),
cost_usd: 100.0,
}],
truncated: true,
}),
});
assert!(!app.costs_complete);
app.execute_command("cost status");
let status = app.status_message.as_deref().unwrap_or_default();
assert!(
status.contains("INCOMPLETE"),
":cost status must not present partial data as settled: {status:?}"
);
app.error_message = None;
app.execute_command("fleet-cost");
let err = app.error_message.as_deref().unwrap_or_default();
assert!(
err.contains("under-reports"),
":fleet-cost must mark a partial total: {err:?}"
);
}
#[tokio::test]
async fn switching_context_resets_the_cost_completeness_verdict() {
let mut app = test_app();
app.cost_enabled = true; app.costs.insert("old".into(), 1.0);
app.costs_complete = false;
app.execute_command("cost off");
assert!(app.costs.is_empty());
assert!(app.costs_complete, "a torn-down map carries no verdict");
}
#[tokio::test]
async fn cost_on_retries_an_incomplete_walk_instead_of_saying_already_on() {
let mut app = test_app();
app.cost_enabled = true;
app.costs.insert("api-prod".into(), 1.0);
app.costs_complete = false;
app.execute_command("cost on");
let msg = app.status_message.as_deref().unwrap_or_default();
assert!(
msg.contains("retrying"),
":cost on must retry an incomplete walk, not report 'already on': {msg:?}"
);
app.costs_complete = true;
app.execute_command("cost on");
let msg = app.status_message.as_deref().unwrap_or_default();
assert!(msg.contains("already on"), "{msg:?}");
}
#[tokio::test]
async fn cost_status_does_not_call_a_partial_result_cached() {
let mut app = test_app();
app.cost_enabled = true;
app.costs.insert("api-prod".into(), 1.0);
app.costs_fetched_at = Some(chrono::Utc::now() - chrono::Duration::hours(3));
app.costs_complete = false;
app.execute_command("cost status");
let msg = app.status_message.as_deref().unwrap_or_default();
assert!(msg.contains("INCOMPLETE"), "{msg:?}");
assert!(
!msg.contains("env(s) cached"),
"a partial result was never cached: {msg:?}"
);
}