use super::render::*;
use crate::render::Snapshot;
#[test]
fn bar_boundaries() {
let fill = |used: f64, cap: f64, w: usize| {
let b = bar(used, cap, w);
b.chars().filter(|c| *c == '━').count()
};
let empty = |used: f64, cap: f64, w: usize| {
let b = bar(used, cap, w);
b.chars().filter(|c| *c == '╱').count()
};
assert_eq!(fill(0.0, 10.0, 20), 0);
assert_eq!(empty(0.0, 10.0, 20), 20);
assert_eq!(fill(10.0, 10.0, 20), 20);
assert_eq!(fill(0.0, 0.0, 20), 0); assert_eq!(fill(50.0, 10.0, 20), 20); assert_eq!(fill(5.0, 10.0, 20), 10); assert_eq!(empty(5.0, 10.0, 20), 10);
assert_eq!(fill(5.0, 10.0, 5), 3); assert_eq!(empty(5.0, 10.0, 5), 2);
assert!(bar(7.5, 10.0, 20).contains("75.0%"));
assert!(bar(95.0, 10.0, 20).contains("100.0%")); }
#[test]
fn window_line_includes_elapsed_and_flag() {
use crate::api::Window;
let now = 1_000_000u64;
let w = Window {
used: 5.0,
cap: 10.0,
exceeded: false,
reset_at: Some((now as f64 + 2.5 * 3600.0) * 1000.0),
};
let line = window_line("5-hour", &w, now, 20, Some(5 * 3600), false);
assert!(line.contains("50.0%"));
assert!(line.contains("$5.00 / $10.00"));
assert!(line.contains("resets in"));
let w_exceeded = Window {
used: 15.0,
cap: 10.0,
exceeded: true,
reset_at: None,
};
let line = window_line("Weekly", &w_exceeded, now, 20, Some(7 * 86400), false);
assert!(line.contains("LIMIT EXCEEDED"));
let line = window_line("Monthly", &w, now, 20, None, false);
assert!(!line.contains("elapsed"));
}
#[test]
fn pace_warns_only_after_10pct_elapsed() {
use crate::api::Window;
let now = 1_000_000u64;
let d = 5 * 3600;
let start5 = now - d / 20;
let w_early = Window {
used: 5.0,
cap: 10.0,
exceeded: false,
reset_at: Some((start5 + d) as f64 * 1000.0),
};
let line = window_line("5-hour", &w_early, now, 20, Some(d), false);
assert!(
!line.contains("on pace"),
"must not warn at 5% elapsed: {line}"
);
let start10 = now - d / 10;
let w_at = Window {
used: 5.0,
cap: 10.0,
exceeded: false,
reset_at: Some((start10 + d) as f64 * 1000.0),
};
let line = window_line("5-hour", &w_at, now, 20, Some(d), false);
assert!(
line.contains("on pace to hit cap in 30m"),
"ETA must render the duration, not an absolute reset: {line}"
);
assert!(
line.find("on pace").unwrap() < line.find("window 10.0%").unwrap(),
"pace must precede elapsed: {line}"
);
}
#[test]
fn plans_table_marks_current_by_exact_name() {
let out = plans_table("individual-goat", false);
assert!(out.contains("*GOAT"));
assert!(
!out.contains("*Go "),
"substring 'go' must not mark Go: {out}"
);
assert!(
!out.contains('\x1b'),
"colors=false must emit no ANSI: {out:?}"
);
let out = plans_table("unknown-plan", false);
assert!(!out.contains('*'), "no current plan → no mark: {out}");
let out = plans_table("individual-goat", true);
assert!(out.contains("\x1b[1m*GOAT"));
}
#[test]
fn plans_json_marks_current() {
let out = plans_json("individual-goat");
assert!(out.starts_with('[') && out.ends_with(']'));
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
let arr = v.as_array().unwrap();
let goat = arr.iter().find(|p| p["name"] == "GOAT").unwrap();
assert_eq!(goat["price"], "$10");
assert_eq!(goat["creditsMonthly"], "$70");
assert_eq!(goat["fiveHour"], "$14");
assert_eq!(goat["weekly"], "$35");
assert_eq!(goat["current"], true);
let go = arr.iter().find(|p| p["name"] == "Go").unwrap();
assert_eq!(go["price"], "$1");
assert_eq!(go["current"], false);
}
#[test]
fn render_json_is_valid_and_complete() {
let s = snapshot_fixture();
let out = render_json(&s);
assert!(out.starts_with('{') && out.ends_with('}'));
assert!(out.contains("\"plan\":\"GOAT\""));
assert!(out.contains("\"fiveHour\":{"));
assert!(out.contains("\"error\":null"));
let v: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(v["plan"], "GOAT");
assert_eq!(v["monthlyCap"], 70.0);
}
#[test]
fn render_json_carries_the_period_bounds() {
use cmduse_core::dates::parse_iso_utc;
let s = snapshot_fixture();
let v: serde_json::Value = serde_json::from_str(&render_json(&s)).unwrap();
assert_eq!(
v["periodStartAt"].as_f64(),
parse_iso_utc("2026-08-27T12:23:00.000Z")
);
assert_eq!(
v["periodEndAt"].as_f64(),
parse_iso_utc("2026-09-27T12:23:00.000Z")
);
assert_eq!(v["periodEnd"], "2026-09-27");
assert_eq!(v["source"], "api");
assert_eq!(v["periodStart"], v["periodStartAt"]);
}
#[test]
fn plain_render_contains_sections() {
let s = snapshot_fixture();
let out = render_plain(&s);
assert!(out.contains("Command Code Usage"));
assert!(out.contains("GOAT"));
assert!(out.contains("Credits:"));
assert!(out.contains("5-hour:"));
assert!(out.contains("Weekly:"));
assert!(out.contains("Monthly:"));
assert!(out.contains("Period:"));
assert!(out.contains("/ $70.00 monthly"));
assert!(out.contains("Period ends "));
assert!(out.contains("window ") && out.contains("% elapsed"));
}
#[test]
fn ansi_render_contains_sections_and_quota() {
let s = snapshot_fixture();
let out = render(&s, 20);
assert!(out.contains("Command Code Usage"));
assert!(out.contains("GOAT"));
assert!(out.contains("$61.44 / $70.00 monthly"));
assert!(out.contains("Usage windows"));
assert!(out.contains("Monthly"));
assert!(out.contains("This billing period"));
assert!(out.contains("Requests"));
let mut err_s = snapshot_fixture();
err_s.err = Some("boom".into());
let out = render(&err_s, 20);
assert!(out.contains("error:"));
assert!(out.contains("boom"));
}
#[test]
fn render_free_plan_no_monthly_cap() {
let mut s = snapshot_fixture();
s.sub.plan_id = "free".into();
let out = render(&s, 20);
assert!(!out.contains("/ $70.00"));
assert!(out.contains("$61.44 monthly")); let out_plain = render_plain(&s);
assert!(out_plain.contains("$61.44 monthly"));
}
#[test]
fn period_line_carries_the_share_of_monthly_credits() {
let s = snapshot_fixture();
let cap = plan_monthly_cap(&s.sub.plan_id).expect("GOAT publishes a monthly cap");
let share = cmduse_core::pct(s.summary.total_cost.unwrap_or(0.0), cap);
let coloured = render(&s, 20);
assert!(coloured.contains(&share), "{coloured}");
assert!(coloured.contains("of credits"), "{coloured}");
let plain = render_plain(&s);
assert!(plain.contains(&format!("{share} of credits")), "{plain}");
assert!(!plain.contains('\x1b'), "plain must carry no SGR: {plain}");
let mut free = snapshot_fixture();
free.sub.plan_id = "free".into();
assert!(
!render(&free, 20).contains("of credits"),
"no cap, no share"
);
assert!(
!render_plain(&free).contains("of credits"),
"no cap, no share"
);
}
#[test]
fn plain_window_line_has_elapsed_and_pace() {
let now = 1_000_000;
let w = crate::api::Window {
used: 5.0,
cap: 10.0,
exceeded: false,
reset_at: Some(1_016_200_000.0),
};
let line = crate::render::plain_window_line("5-hour", &w, now, Some(18_000), false);
assert!(line.contains("window 10.0% elapsed"), "{line}");
assert!(line.contains("on pace to hit cap in"), "{line}");
assert!(
line.find("on pace").unwrap() < line.find("window 10.0%").unwrap(),
"{line}"
);
assert!(
!line.contains("\x1b"),
"plain output must carry no SGR: {line}"
);
}
#[test]
fn plain_window_line_flags_exceeded() {
let now = 1_000_000;
let w = crate::api::Window {
used: 15.0,
cap: 10.0,
exceeded: true,
reset_at: Some(1_016_200_000.0),
};
let line = crate::render::plain_window_line("Weekly", &w, now, None, false);
assert!(line.contains("LIMIT EXCEEDED"), "{line}");
assert!(
!line.contains("elapsed"),
"no duration → no elapsed: {line}"
);
}
#[test]
fn monthly_pro_rata_shows_by_now_and_others_do_not() {
let now = 1_000_000u64;
let w = crate::api::Window {
used: 2.0,
cap: 10.0,
exceeded: false,
reset_at: Some(1_016_200_000.0), };
let monthly = window_line("Monthly", &w, now, 20, Some(18_000), true);
assert!(monthly.contains("$1.00 by now"), "{monthly}");
let five_hour = window_line("5-hour", &w, now, 20, Some(18_000), false);
assert!(!five_hour.contains("by now"), "{five_hour}");
let no_dur = window_line("Monthly", &w, now, 20, None, true);
assert!(!no_dur.contains("by now"), "{no_dur}");
let plain = crate::render::plain_window_line("Monthly", &w, now, Some(18_000), true);
assert!(plain.contains("$1.00 by now"), "{plain}");
assert!(!plain.contains('\x1b'), "{plain}");
}
fn snapshot_fixture() -> Snapshot {
Snapshot {
sub: crate::api::SubData {
status: "active".into(),
plan_id: "individual-goat".into(),
current_period_start: Some("2026-08-27T12:23:00.000Z".into()),
current_period_end: Some("2026-09-27T12:23:00.000Z".into()),
},
credits: crate::api::CreditsResp {
credits: crate::api::Credits {
monthly_credits: 61.44,
purchased_credits: 0.0,
free_credits: 0.0,
},
window_limits: crate::api::WindowLimits {
five_hour: Some(crate::api::Window {
used: 2.79,
cap: 14.0,
exceeded: false,
reset_at: Some(1_789_368_000_000.0),
}),
weekly: Some(crate::api::Window {
used: 2.79,
cap: 35.0,
exceeded: false,
reset_at: Some(1_789_912_000_000.0),
}),
},
},
summary: crate::api::UsageSummary {
total_count: Some(733),
total_cost: Some(9.25),
success_rate: Some(100.0),
total_tokens_in: Some(64_096_255),
total_tokens_out: Some(236_706),
},
now: 1_789_368_000_000 / 1000,
err: None,
}
}
#[test]
fn statusline_json_uses_the_family_window_names() {
let v = crate::statusline_json("GOAT", 70.0, 0.0, (1.0, 14.0), (2.0, 35.0));
assert_eq!(v["monthSpent"], 70.0);
assert_eq!(v["monthCap"], 70.0);
assert_eq!(v["fiveHourSpent"], 1.0);
assert_eq!(v["weeklyCap"], 35.0);
let v = crate::statusline_json("GOAT", 70.0, 30.0, (0.0, 0.0), (0.0, 0.0));
assert_eq!(v["monthSpent"], 40.0);
let v = crate::statusline_json("GOAT", 70.0, 90.0, (0.0, 0.0), (0.0, 0.0));
assert_eq!(v["monthSpent"], 0.0);
let v = crate::statusline_json("GOAT", 70.0, 30.0, (1.0, 14.0), (2.0, 35.0));
assert_eq!(v["monthlyRemaining"], 30.0);
assert_eq!(v["monthlyCap"], 70.0);
assert_eq!(v["fiveHourUsed"], 1.0);
assert_eq!(v["weeklyUsed"], 2.0);
}
#[test]
fn statusline_templates() {
use crate::report_render::{render_statusline, StatusData};
let h5 = Some((2.79, 14.0));
let wk = Some((3.61, 35.0));
let d = |tpl_colors: bool, ascii: bool| StatusData {
plan: "GOAT",
monthly_remaining: 59.3,
monthly_cap: 70.0,
five_hour: &h5,
weekly: &wk,
five_hour_eta: None,
weekly_eta: None,
bar_width: 10,
colors: tpl_colors,
ascii,
};
let d0 = d(true, false);
let out = render_statusline("{plan} {credits}/{cap} · 5h {5h_bar} · wk {wk_bar}", &d0);
assert!(out.contains("\x1b["));
let plain = d(false, false);
let out = render_statusline("{plan} {credits}/{cap} · 5h {5h_bar} · wk {wk_bar}", &plain);
assert!(out.contains("GOAT"));
assert!(out.contains("$59.30/$70.00"));
assert!(out.contains("5h"));
assert!(out.contains("wk"));
assert!(!out.contains('\x1b'));
let out = render_statusline("{plan}", &d0);
assert_eq!(out, "GOAT");
let out = render_statusline("{5h_pct}|{wk_pct}", &d0);
assert_eq!(out, "19.9%|10.3%");
let out = render_statusline("{5h_used} of {5h_cap}", &d0);
assert_eq!(out, "$2.79 of $14.00");
let out = render_statusline("{credits_bar}", &d0);
assert!(out.contains("15.3%"));
let out = render_statusline("{plan} {bogus} end", &d0);
assert_eq!(out, "GOAT end");
let out = render_statusline("{plan} {oops", &d0);
assert_eq!(out, "GOAT {oops");
let d1 = d(false, true);
let out = render_statusline("{5h_bar}", &d1);
assert!(out.contains('#'));
assert!(out.contains('-'));
assert!(!out.contains('━'));
let d2 = d(false, false);
let out = render_statusline("{credits}", &d2);
assert!(!out.contains('\x1b'));
assert_eq!(out, "$59.30");
let out = render_statusline("{plan}\n{credits_bar}", &d0);
assert!(out.contains('\n'));
assert_eq!(render_statusline("{5h_eta}", &d0), "");
let deta = StatusData {
plan: "GOAT",
monthly_remaining: 1.0,
monthly_cap: 70.0,
five_hour: &h5,
weekly: &wk,
five_hour_eta: Some("on pace to hit cap in 1h 2m".into()),
weekly_eta: None,
bar_width: 10,
colors: false,
ascii: false,
};
assert_eq!(
render_statusline("{5h_eta}|{wk_eta}", &deta),
"on pace to hit cap in 1h 2m|"
);
let h5z: Option<(f64, f64)> = None;
let dz = StatusData {
plan: "Free",
monthly_remaining: 0.0,
monthly_cap: 0.0,
five_hour: &h5z,
weekly: &h5z,
five_hour_eta: None,
weekly_eta: None,
bar_width: 10,
colors: true,
ascii: false,
};
let out = render_statusline("{plan} {5h_pct} {wk_pct} {credits_bar}", &dz);
assert!(out.contains("0%"));
}
#[test]
fn sparkline_small_deltas_visible() {
let out = sparkline(&[0.5, 0.0, 0.02]);
assert_ne!(out.chars().last(), Some('▁'));
let out = sparkline(&[0.5, 0.0, 0.0]);
assert_eq!(out.chars().last(), Some('▁'));
}
#[test]
fn sparkline_shapes() {
assert_eq!(sparkline(&[1.0, 1.0, 1.0]), "███"); assert_eq!(sparkline(&[0.0, 5.0, 10.0]), "▁▅█"); assert_eq!(sparkline(&[]), "");
assert_eq!(sparkline(&[5.0]), "");
assert_eq!(sparkline(&[0.0, 0.0]), "▁▁");
}
#[test]
fn update_box_borders_align_and_wraps() {
let b = crate::render::update_box("update available 0.6.7 → 0.6.8", None, false);
let lines: Vec<&str> = b.lines().collect();
assert_eq!(lines.len(), 3);
let w = lines[0].chars().count();
assert!(lines.iter().all(|l| l.chars().count() == w), "{b}");
assert!(b.contains("update available"), "{b}");
assert!(b.starts_with('╭') && b.trim_end().ends_with('╯'), "{b}");
let narrow = crate::render::update_box("aaa bbb ccc ddd eee fff", Some(16), false);
assert!(narrow.lines().count() > 3, "{narrow}");
let nw = narrow.lines().next().unwrap().chars().count();
assert!(narrow.lines().all(|l| l.chars().count() == nw), "{narrow}");
let colored = crate::render::update_box("update available", None, true);
assert_eq!(colored.lines().count(), 3, "{colored}");
assert!(colored.contains("\x1b[33m") && colored.contains("update available"));
}