1use std::collections::BTreeMap;
15
16use super::config::{
17 apply_common_extras, parse_percent_format, parse_reset_format, CommonRateLimitConfig,
18 PercentFormat, ResetFormat, PRIORITY,
19};
20use super::format::{format_jsonl_tokens, format_percent, format_reset, render_error, ResetWindow};
21use super::window::{resolve_five_hour_reset, ResetSource, UsageWindow, WindowResolution};
22use crate::data_context::{DataContext, DataDep};
23use crate::segments::extras::parse_bool;
24use crate::segments::{RenderContext, RenderResult, RenderedSegment, Segment, SegmentDefaults};
25use crate::theme::Role;
26
27#[non_exhaustive]
28pub struct RateLimit5hSegment {
29 pub format: PercentFormat,
30 pub invert: bool,
31 pub config: CommonRateLimitConfig,
32}
33
34impl Default for RateLimit5hSegment {
35 fn default() -> Self {
36 Self {
37 format: PercentFormat::Percent,
38 invert: false,
39 config: CommonRateLimitConfig::new("5h"),
40 }
41 }
42}
43
44impl RateLimit5hSegment {
45 #[must_use]
48 pub fn from_extras(
49 extras: &BTreeMap<String, toml::Value>,
50 warn: &mut impl FnMut(&str),
51 ) -> Self {
52 let mut seg = Self::default();
53 apply_common_extras(&mut seg.config, extras, "rate_limit_5h", warn);
54 if let Some(f) = parse_percent_format(extras, "rate_limit_5h", warn) {
55 seg.format = f;
56 }
57 if let Some(b) = parse_bool(extras, "invert", "rate_limit_5h", warn) {
58 seg.invert = b;
59 }
60 if seg.config.invalid_progress_width {
63 seg.format = PercentFormat::Percent;
64 }
65 seg
66 }
67}
68
69impl Segment for RateLimit5hSegment {
70 fn render(&self, ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
71 let usage = ctx.usage();
72 let text = match &*usage {
73 Ok(data) => match UsageWindow::FiveHour.resolve_percent(data) {
74 Ok(WindowResolution::Endpoint(bucket)) => {
75 format_percent(bucket, self.format, self.invert, &self.config)
76 }
77 Ok(WindowResolution::JsonlTokens(total)) => {
78 format_jsonl_tokens(total, &self.config)
79 }
80 Err(reason) => {
81 crate::lsm_debug!("rate_limit_5h: {reason}; hiding");
82 return Ok(None);
83 }
84 },
85 Err(err) => render_error(err, &self.config),
86 };
87 Ok(Some(RenderedSegment::new(text).with_role(Role::Info)))
88 }
89
90 fn data_deps(&self) -> &'static [DataDep] {
91 &[DataDep::Usage]
92 }
93
94 fn defaults(&self) -> SegmentDefaults {
95 SegmentDefaults::with_priority(PRIORITY)
96 }
97}
98
99#[non_exhaustive]
100pub struct RateLimit5hResetSegment {
101 pub format: ResetFormat,
102 pub compact: bool,
103 pub use_days: bool,
104 pub config: CommonRateLimitConfig,
105}
106
107impl Default for RateLimit5hResetSegment {
108 fn default() -> Self {
109 Self {
110 format: ResetFormat::Duration,
111 compact: false,
112 use_days: true,
113 config: CommonRateLimitConfig::new("5h reset"),
114 }
115 }
116}
117
118impl RateLimit5hResetSegment {
119 #[must_use]
120 pub fn from_extras(
121 extras: &BTreeMap<String, toml::Value>,
122 warn: &mut impl FnMut(&str),
123 ) -> Self {
124 let mut seg = Self::default();
125 apply_common_extras(&mut seg.config, extras, "rate_limit_5h_reset", warn);
126 if let Some(f) = parse_reset_format(extras, "rate_limit_5h_reset", warn) {
127 seg.format = f;
128 }
129 if let Some(b) = parse_bool(extras, "compact", "rate_limit_5h_reset", warn) {
130 seg.compact = b;
131 }
132 if let Some(b) = parse_bool(extras, "use_days", "rate_limit_5h_reset", warn) {
133 seg.use_days = b;
134 }
135 if seg.config.invalid_progress_width && matches!(seg.format, ResetFormat::Progress) {
140 seg.format = ResetFormat::Duration;
141 }
142 seg
143 }
144}
145
146impl Segment for RateLimit5hResetSegment {
147 fn render(&self, ctx: &DataContext, _rc: &RenderContext) -> RenderResult {
148 let usage = ctx.usage();
149 let text = match &*usage {
150 Ok(data) => {
151 let (resets_at, jsonl) = match resolve_five_hour_reset(data) {
152 Ok(ResetSource::Endpoint(at)) => (at, false),
153 Ok(ResetSource::JsonlBlockEnd(at)) => (at, true),
154 Err(reason) => {
155 crate::lsm_debug!("rate_limit_5h_reset: {reason}; hiding");
156 return Ok(None);
157 }
158 };
159 let remaining = resets_at.duration_since(jiff::Timestamp::now());
160 if remaining <= jiff::SignedDuration::ZERO {
161 crate::lsm_debug!(
162 "rate_limit_5h_reset: resets_at in the past ({resets_at}); hiding"
163 );
164 return Ok(None);
165 }
166 format_reset(
167 resets_at,
168 remaining,
169 &self.format,
170 self.compact,
171 self.use_days,
172 ResetWindow::FiveHour,
173 jsonl,
174 &self.config,
175 )
176 }
177 Err(err) => render_error(err, &self.config),
178 };
179 Ok(Some(RenderedSegment::new(text).with_role(Role::Info)))
180 }
181
182 fn data_deps(&self) -> &'static [DataDep] {
183 &[DataDep::Usage]
184 }
185
186 fn defaults(&self) -> SegmentDefaults {
187 SegmentDefaults::with_priority(PRIORITY)
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use crate::data_context::{
195 EndpointUsage, ExtraUsage, FiveHourWindow, JsonlUsage, SevenDayWindow, TokenCounts,
196 UsageBucket, UsageData, UsageError,
197 };
198 use crate::input::{ModelInfo, Percent, StatusContext, Tool, WorkspaceInfo};
199 use jiff::{SignedDuration, Timestamp};
200 use std::path::PathBuf;
201 use std::sync::Arc;
202
203 fn rc() -> RenderContext {
204 RenderContext::new(80)
205 }
206
207 fn ctx_with_usage(usage: Result<UsageData, UsageError>) -> DataContext {
208 let dc = DataContext::new(StatusContext {
209 tool: Tool::ClaudeCode,
210 model: Some(ModelInfo {
211 display_name: "X".into(),
212 }),
213 workspace: Some(WorkspaceInfo {
214 project_dir: PathBuf::from("/repo"),
215 git_worktree: None,
216 }),
217 context_window: None,
218 cost: None,
219 effort: None,
220 vim: None,
221 output_style: None,
222 agent_name: None,
223 version: None,
224 raw: Arc::new(serde_json::Value::Null),
225 });
226 dc.preseed_usage(usage).expect("seed");
227 dc
228 }
229
230 fn endpoint_data_with_five_hour(pct: f32) -> UsageData {
231 UsageData::Endpoint(EndpointUsage {
232 five_hour: Some(UsageBucket {
233 utilization: Percent::new(pct).unwrap(),
234 resets_at: None,
235 }),
236 seven_day: None,
237 seven_day_opus: None,
238 seven_day_sonnet: None,
239 seven_day_oauth_apps: None,
240 extra_usage: None,
241 unknown_buckets: std::collections::HashMap::new(),
242 })
243 }
244
245 fn endpoint_empty() -> UsageData {
246 UsageData::Endpoint(EndpointUsage {
247 five_hour: None,
248 seven_day: None,
249 seven_day_opus: None,
250 seven_day_sonnet: None,
251 seven_day_oauth_apps: None,
252 extra_usage: None,
253 unknown_buckets: std::collections::HashMap::new(),
254 })
255 }
256
257 fn jsonl_with_five_hour_tokens(total: u64) -> UsageData {
258 let tokens = TokenCounts::from_parts(total, 0, 0, 0);
259 let start = Timestamp::now() - SignedDuration::from_hours(1);
261 UsageData::Jsonl(JsonlUsage::new(
262 Some(FiveHourWindow::new(tokens, start)),
263 SevenDayWindow::new(TokenCounts::default()),
264 ))
265 }
266
267 fn data_with_reset_in(minutes: i64) -> UsageData {
268 let slack = if minutes > 0 {
272 SignedDuration::from_secs(30)
273 } else {
274 SignedDuration::ZERO
275 };
276 UsageData::Endpoint(EndpointUsage {
277 five_hour: Some(UsageBucket {
278 utilization: Percent::new(42.0).unwrap(),
279 resets_at: Some(Timestamp::now() + SignedDuration::from_mins(minutes) + slack),
280 }),
281 seven_day: None,
282 seven_day_opus: None,
283 seven_day_sonnet: None,
284 seven_day_oauth_apps: None,
285 extra_usage: None,
286 unknown_buckets: std::collections::HashMap::new(),
287 })
288 }
289
290 fn jsonl_data_with_reset_in(minutes: i64) -> UsageData {
291 let slack = if minutes > 0 {
292 SignedDuration::from_secs(30)
293 } else {
294 SignedDuration::ZERO
295 };
296 let start = Timestamp::now() + SignedDuration::from_mins(minutes) + slack
299 - SignedDuration::from_hours(5);
300 UsageData::Jsonl(JsonlUsage::new(
301 Some(FiveHourWindow::new(TokenCounts::default(), start)),
302 SevenDayWindow::new(TokenCounts::default()),
303 ))
304 }
305
306 #[test]
307 fn hidden_when_five_hour_bucket_absent() {
308 let rendered = RateLimit5hSegment::default()
309 .render(&ctx_with_usage(Ok(endpoint_empty())), &rc())
310 .expect("render ok");
311 assert_eq!(rendered, None);
312 }
313
314 #[test]
315 fn renders_percent_happy_path() {
316 let dc = ctx_with_usage(Ok(endpoint_data_with_five_hour(22.0)));
317 let rendered = RateLimit5hSegment::default()
318 .render(&dc, &rc())
319 .expect("render ok")
320 .expect("visible");
321 assert_eq!(rendered.text(), "5h: 22.0%");
322 }
323
324 #[test]
325 fn renders_inverted_percent_when_configured() {
326 let dc = ctx_with_usage(Ok(endpoint_data_with_five_hour(22.0)));
327 let seg = RateLimit5hSegment {
328 invert: true,
329 ..Default::default()
330 };
331 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
332 assert_eq!(rendered.text(), "5h: 78.0%");
333 }
334
335 #[test]
336 fn jsonl_mode_renders_compact_tokens_with_stale_marker() {
337 let dc = ctx_with_usage(Ok(jsonl_with_five_hour_tokens(420_000)));
338 let rendered = RateLimit5hSegment::default()
339 .render(&dc, &rc())
340 .unwrap()
341 .expect("visible");
342 assert_eq!(rendered.text(), "~5h: 420k");
343 }
344
345 #[test]
346 fn jsonl_mode_hides_when_no_active_block() {
347 let dc = ctx_with_usage(Ok(UsageData::Jsonl(JsonlUsage::new(
350 None,
351 SevenDayWindow::new(TokenCounts::default()),
352 ))));
353 assert_eq!(
354 RateLimit5hSegment::default().render(&dc, &rc()).unwrap(),
355 None
356 );
357 }
358
359 #[test]
360 fn jsonl_mode_ignores_invert_and_progress_knobs() {
361 let dc = ctx_with_usage(Ok(jsonl_with_five_hour_tokens(1_200_000)));
365 let seg = RateLimit5hSegment {
366 format: PercentFormat::Progress,
367 invert: true,
368 ..Default::default()
369 };
370 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
371 assert_eq!(rendered.text(), "~5h: 1.2M");
372 }
373
374 #[test]
375 fn renders_progress_bar_when_format_is_progress() {
376 let dc = ctx_with_usage(Ok(endpoint_data_with_five_hour(50.0)));
377 let seg = RateLimit5hSegment {
378 format: PercentFormat::Progress,
379 ..Default::default()
380 };
381 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
382 assert!(rendered.text().starts_with("5h: "), "{}", rendered.text());
383 assert!(rendered.text().contains("█"));
384 assert!(rendered.text().ends_with("50.0%"), "{}", rendered.text());
385 }
386
387 #[test]
388 fn progress_bar_at_zero_is_entirely_empty_cells() {
389 let dc = ctx_with_usage(Ok(endpoint_data_with_five_hour(0.0)));
390 let seg = RateLimit5hSegment {
391 format: PercentFormat::Progress,
392 ..Default::default()
393 };
394 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
395 assert!(rendered.text().contains("░"));
396 assert!(!rendered.text().contains("█"));
397 assert!(rendered.text().ends_with("0.0%"), "{}", rendered.text());
398 }
399
400 #[test]
401 fn progress_bar_at_one_hundred_is_entirely_filled_cells() {
402 let dc = ctx_with_usage(Ok(endpoint_data_with_five_hour(100.0)));
403 let seg = RateLimit5hSegment {
404 format: PercentFormat::Progress,
405 ..Default::default()
406 };
407 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
408 assert!(rendered.text().contains("█"));
409 assert!(!rendered.text().contains("░"));
410 assert!(rendered.text().ends_with("100.0%"), "{}", rendered.text());
411 }
412
413 #[test]
414 fn renders_error_table_strings() {
415 let dc = ctx_with_usage(Err(UsageError::Timeout));
416 let rendered = RateLimit5hSegment::default()
417 .render(&dc, &rc())
418 .unwrap()
419 .expect("visible");
420 assert_eq!(rendered.text(), "5h: [Timeout]");
421 }
422
423 #[test]
424 fn declares_usage_as_its_only_data_dep() {
425 let deps = RateLimit5hSegment::default().data_deps();
426 assert_eq!(deps, &[DataDep::Usage]);
427 }
428
429 #[test]
430 fn from_extras_applies_format_invert_and_common_knobs() {
431 let mut extras = std::collections::BTreeMap::new();
432 extras.insert("format".into(), toml::Value::String("progress".into()));
433 extras.insert("invert".into(), toml::Value::Boolean(true));
434 extras.insert("label".into(), toml::Value::String("five".into()));
435 extras.insert("icon".into(), toml::Value::String("⏱".into()));
436 extras.insert("stale_marker".into(), toml::Value::String("*".into()));
437 extras.insert("progress_width".into(), toml::Value::Integer(10));
438 let mut warnings = Vec::new();
439 let seg = RateLimit5hSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
440 assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
441 assert_eq!(seg.format, PercentFormat::Progress);
442 assert!(seg.invert);
443 assert_eq!(seg.config.label, "five");
444 assert_eq!(seg.config.icon, "⏱");
445 assert_eq!(seg.config.stale_marker, "*");
446 assert_eq!(seg.config.progress_width, 10);
447 }
448
449 #[test]
450 fn from_extras_flips_progress_to_percent_on_invalid_width() {
451 let mut extras = std::collections::BTreeMap::new();
455 extras.insert("format".into(), toml::Value::String("progress".into()));
456 extras.insert("progress_width".into(), toml::Value::Integer(0));
457 let mut warnings = Vec::new();
458 let seg = RateLimit5hSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
459 assert_eq!(warnings.len(), 1);
460 assert!(warnings[0].contains("progress_width"), "{:?}", warnings[0]);
461 assert_eq!(seg.format, PercentFormat::Percent);
462 }
463
464 #[test]
465 fn from_extras_warns_on_bad_format_string() {
466 let mut extras = std::collections::BTreeMap::new();
467 extras.insert("format".into(), toml::Value::String("bogus".into()));
468 let mut warnings = Vec::new();
469 let seg = RateLimit5hSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
470 assert_eq!(warnings.len(), 1);
471 assert!(warnings[0].contains("format"), "{:?}", warnings[0]);
472 assert_eq!(seg.format, PercentFormat::Percent);
473 }
474
475 #[test]
476 fn does_not_read_extra_usage_field() {
477 let data = UsageData::Endpoint(EndpointUsage {
480 five_hour: None,
481 seven_day: None,
482 seven_day_opus: None,
483 seven_day_sonnet: None,
484 seven_day_oauth_apps: None,
485 extra_usage: Some(ExtraUsage {
486 is_enabled: Some(true),
487 utilization: Some(Percent::new(50.0).unwrap()),
488 monthly_limit: Some(100.0),
489 used_credits: Some(50.0),
490 currency: Some("USD".into()),
491 }),
492 unknown_buckets: std::collections::HashMap::new(),
493 });
494 let rendered = RateLimit5hSegment::default()
495 .render(&ctx_with_usage(Ok(data)), &rc())
496 .unwrap();
497 assert_eq!(rendered, None);
498 }
499
500 #[test]
503 fn reset_renders_countdown_in_default_format() {
504 let dc = ctx_with_usage(Ok(data_with_reset_in(4 * 60 + 37)));
505 let rendered = RateLimit5hResetSegment::default()
506 .render(&dc, &rc())
507 .unwrap()
508 .expect("visible");
509 assert_eq!(rendered.text(), "5h reset: 4hr 37m");
510 }
511
512 #[test]
513 fn reset_hidden_when_resets_at_in_past() {
514 let dc = ctx_with_usage(Ok(data_with_reset_in(-10)));
515 assert_eq!(
516 RateLimit5hResetSegment::default()
517 .render(&dc, &rc())
518 .unwrap(),
519 None
520 );
521 }
522
523 #[test]
524 fn reset_hidden_when_resets_at_missing() {
525 let data = UsageData::Endpoint(EndpointUsage {
526 five_hour: Some(UsageBucket {
527 utilization: Percent::new(42.0).unwrap(),
528 resets_at: None,
529 }),
530 seven_day: None,
531 seven_day_opus: None,
532 seven_day_sonnet: None,
533 seven_day_oauth_apps: None,
534 extra_usage: None,
535 unknown_buckets: std::collections::HashMap::new(),
536 });
537 assert_eq!(
538 RateLimit5hResetSegment::default()
539 .render(&ctx_with_usage(Ok(data)), &rc())
540 .unwrap(),
541 None,
542 );
543 }
544
545 #[test]
546 fn reset_hidden_when_five_hour_bucket_absent() {
547 assert_eq!(
548 RateLimit5hResetSegment::default()
549 .render(&ctx_with_usage(Ok(endpoint_empty())), &rc())
550 .unwrap(),
551 None,
552 );
553 }
554
555 #[test]
556 fn reset_compact_format_drops_suffix_spaces() {
557 let seg = RateLimit5hResetSegment {
558 compact: true,
559 ..Default::default()
560 };
561 let dc = ctx_with_usage(Ok(data_with_reset_in(4 * 60 + 37)));
562 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
563 assert_eq!(rendered.text(), "5h reset: 4h37m");
564 }
565
566 #[test]
567 fn reset_renders_error_when_usage_fails() {
568 let dc = ctx_with_usage(Err(UsageError::Timeout));
569 let rendered = RateLimit5hResetSegment::default()
570 .render(&dc, &rc())
571 .unwrap()
572 .expect("visible");
573 assert_eq!(rendered.text(), "5h reset: [Timeout]");
574 }
575
576 #[test]
577 fn reset_progress_format_divides_by_five_hour_window_not_seven_day() {
578 let dc = ctx_with_usage(Ok(data_with_reset_in(30)));
582 let seg = RateLimit5hResetSegment {
583 format: ResetFormat::Progress,
584 ..Default::default()
585 };
586 let rendered = seg.render(&dc, &rc()).unwrap().expect("visible");
587 let pct_str = rendered
588 .text()
589 .rsplit(' ')
590 .next()
591 .expect("percent suffix")
592 .trim_end_matches('%');
593 let pct: f64 = pct_str.parse().expect("numeric percent");
594 assert!(
595 (88.0..=92.0).contains(&pct),
596 "expected ~90% elapsed, got {pct}% from {:?}",
597 rendered.text(),
598 );
599 }
600
601 #[test]
602 fn reset_jsonl_mode_derives_reset_from_five_hour_window_ends_at() {
603 let dc = ctx_with_usage(Ok(jsonl_data_with_reset_in(4 * 60 + 37)));
607 let rendered = RateLimit5hResetSegment::default()
608 .render(&dc, &rc())
609 .unwrap()
610 .expect("visible");
611 assert_eq!(rendered.text(), "~5h reset: 4hr 37m");
612 }
613
614 #[test]
615 fn reset_jsonl_mode_hides_when_block_inactive() {
616 let dc = ctx_with_usage(Ok(UsageData::Jsonl(JsonlUsage::new(
617 None,
618 SevenDayWindow::new(TokenCounts::default()),
619 ))));
620 assert_eq!(
621 RateLimit5hResetSegment::default()
622 .render(&dc, &rc())
623 .unwrap(),
624 None,
625 );
626 }
627
628 #[test]
629 fn reset_jsonl_mode_hides_when_ends_at_in_past() {
630 let dc = ctx_with_usage(Ok(jsonl_data_with_reset_in(-10)));
637 assert_eq!(
638 RateLimit5hResetSegment::default()
639 .render(&dc, &rc())
640 .unwrap(),
641 None,
642 );
643 }
644
645 #[test]
646 fn reset_declares_usage_as_its_only_data_dep() {
647 assert_eq!(
648 RateLimit5hResetSegment::default().data_deps(),
649 &[DataDep::Usage],
650 );
651 }
652
653 #[test]
654 fn reset_from_extras_applies_duration_format_knobs() {
655 let mut extras = std::collections::BTreeMap::new();
656 extras.insert("format".into(), toml::Value::String("duration".into()));
657 extras.insert("compact".into(), toml::Value::Boolean(true));
658 extras.insert("use_days".into(), toml::Value::Boolean(false));
659 let mut warnings = Vec::new();
660 let seg =
661 RateLimit5hResetSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
662 assert!(warnings.is_empty(), "{warnings:?}");
663 assert_eq!(seg.format, ResetFormat::Duration);
664 assert!(seg.compact);
665 assert!(!seg.use_days);
666 }
667
668 #[test]
669 fn reset_from_extras_warns_on_percent_format_string() {
670 let mut extras = std::collections::BTreeMap::new();
673 extras.insert("format".into(), toml::Value::String("percent".into()));
674 let mut warnings = Vec::new();
675 let _ =
676 RateLimit5hResetSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
677 assert_eq!(warnings.len(), 1);
678 assert!(warnings[0].contains("format"), "{:?}", warnings[0]);
679 }
680
681 #[test]
682 fn reset_invalid_progress_width_does_not_clobber_absolute_format() {
683 let mut extras = std::collections::BTreeMap::new();
689 extras.insert("format".into(), toml::Value::String("absolute".into()));
690 extras.insert("progress_width".into(), toml::Value::Integer(0));
691 let mut warnings = Vec::new();
692 let seg =
693 RateLimit5hResetSegment::from_extras(&extras, &mut |m| warnings.push(m.to_string()));
694 assert!(
695 matches!(seg.format, ResetFormat::Absolute(_)),
696 "absolute survived: {:?}",
697 seg.format
698 );
699 assert!(
700 warnings.iter().any(|w| w.contains("progress_width")),
701 "expected progress_width warning: {warnings:?}"
702 );
703 }
704
705 #[test]
706 fn reset_absolute_format_renders_end_to_end_from_toml() {
707 use crate::config::Config;
712 use std::str::FromStr;
713
714 let cfg = Config::from_str(
715 r#"
716 [segments.rate_limit_5h_reset]
717 format = "absolute"
718 timezone = "America/Los_Angeles"
719 hour_format = "12h"
720 label = "5h reset"
721 "#,
722 )
723 .expect("config parses");
724 let extras = &cfg
725 .segments
726 .get("rate_limit_5h_reset")
727 .expect("segment block")
728 .extra;
729 let mut warnings = Vec::new();
730 let seg =
731 RateLimit5hResetSegment::from_extras(extras, &mut |m| warnings.push(m.to_string()));
732 assert!(warnings.is_empty(), "no warnings expected: {warnings:?}");
733 assert!(matches!(seg.format, ResetFormat::Absolute(_)));
734
735 let dc = ctx_with_usage(Ok(data_with_reset_in(60)));
739 let rendered = seg
740 .render(&dc, &rc())
741 .expect("render ok")
742 .expect("segment visible");
743 assert!(
744 rendered.text.contains("5h reset:"),
745 "label missing: {}",
746 rendered.text
747 );
748 assert!(
749 rendered.text.contains(" AM ") || rendered.text.contains(" PM "),
750 "12h marker missing: {}",
751 rendered.text
752 );
753 }
754}