1use crate::time;
6use serde_json::Value;
7
8#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
9pub struct Window {
10 pub kind: String,
11 pub scope: Option<String>,
12 pub percent: f64,
13 pub resets_at: Option<i64>,
14 pub is_active: bool,
15 #[serde(default)]
18 pub severity: Option<String>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub length_seconds: Option<i64>,
27}
28
29pub fn anthropic_window_length(kind: &str) -> Option<i64> {
35 match kind {
36 "session" | "five_hour" => Some(5 * 3600),
37 "seven_day" => Some(7 * 86_400),
38 weekly if weekly.starts_with("weekly_") => Some(7 * 86_400),
39 _ => None,
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
45#[serde(rename_all = "snake_case")]
46pub enum Source {
47 Live,
49 Remembered,
51}
52
53#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
54pub struct Snapshot {
55 pub windows: Vec<Window>,
56 pub observed_at: Option<i64>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub answered_at: Option<i64>,
61 #[serde(default)]
65 pub lists_every_limit: bool,
66 pub source: Source,
67}
68
69impl Snapshot {
70 pub(crate) fn answered(&self) -> bool {
73 self.answered_at.is_some()
74 }
75}
76
77impl Window {
78 pub(crate) fn used(&self, now: i64) -> f64 {
81 if self.resets_at.is_some_and(|at| at <= now) {
82 0.0
83 } else {
84 self.percent
85 }
86 }
87
88 pub(crate) fn same_limit(&self, other: &Window) -> bool {
90 limit_name(&self.kind) == limit_name(&other.kind) && self.scope == other.scope
91 }
92
93 pub(crate) fn same_window(&self, other: &Window) -> bool {
96 self.same_limit(other)
97 && matches!(
98 (self.resets_at, other.resets_at),
99 (Some(x), Some(y)) if same_reset(x, y)
100 )
101 }
102}
103
104pub(crate) fn limit_name(kind: &str) -> &str {
110 match kind {
111 "five_hour" => "session",
112 "seven_day" => "weekly_all",
113 other => other,
114 }
115}
116
117pub fn whole(percent: f64) -> i64 {
121 percent.round() as i64
122}
123
124pub(crate) fn reached(percent: f64, share: u8) -> bool {
127 whole(percent) >= i64::from(share)
128}
129
130const SAME_RESET: u64 = 60;
138
139pub fn same_reset(a: i64, b: i64) -> bool {
143 a.abs_diff(b) < SAME_RESET
144}
145
146pub(crate) fn may_still_run(at: i64, now: i64) -> bool {
149 at > now || same_reset(at, now)
150}
151
152pub(crate) fn answered(known: Option<&Snapshot>, answer: &Snapshot, now: i64) -> Snapshot {
170 let Some(known) = known else {
171 return answer.clone();
172 };
173 let stamped = |at: Option<i64>| at.filter(|at| *at <= now);
174 if answer.answered_at < stamped(known.answered_at) {
175 return known.clone();
176 }
177 let moved_since = stamped(known.observed_at) >= answer.answered_at;
178 let higher = |given: &Window| {
179 known
180 .windows
181 .iter()
182 .find(|had| moved_since && had.same_window(given) && had.percent > given.percent)
183 };
184 let take = |given: &Window| match higher(given) {
185 Some(had) => Window {
186 percent: had.percent,
187 severity: had.severity.clone(),
188 ..given.clone()
189 },
190 None => given.clone(),
191 };
192 let windows = if answer.lists_every_limit {
193 answer.windows.iter().map(take).collect()
194 } else {
195 let answer_of = |had: &Window| answer.windows.iter().find(|given| given.same_limit(had));
196 let running = |had: &Window| {
197 had.resets_at
198 .zip(answer.answered_at)
199 .is_some_and(|(reset, taken)| reset > taken)
200 };
201 let added = answer
202 .windows
203 .iter()
204 .filter(|given| !known.windows.iter().any(|had| had.same_limit(given)));
205 known
206 .windows
207 .iter()
208 .filter_map(|had| match answer_of(had) {
209 Some(given) => Some(take(given)),
210 None => running(had).then(|| had.clone()),
211 })
212 .chain(added.map(take))
213 .collect()
214 };
215 Snapshot {
216 windows,
217 observed_at: stamped(known.observed_at).max(answer.observed_at),
218 answered_at: answer.answered_at,
219 lists_every_limit: answer.lists_every_limit,
220 source: answer.source,
221 }
222}
223
224pub(crate) fn moved(known: &Snapshot, passed: &[Window], now: i64) -> Option<Snapshot> {
239 let mut windows = known.windows.clone();
240 let mut changed = false;
241 for given in passed
242 .iter()
243 .filter(|w| w.resets_at.is_some_and(|at| at > now))
244 {
245 let Some(had) = windows.iter_mut().find(|had| had.same_limit(given)) else {
246 continue;
247 };
248 let running = had.resets_at.is_some_and(|at| at > now);
249 had.resets_at = match (had.same_window(given), running) {
250 (true, _) if given.percent > had.percent => had.resets_at,
251 (false, false) => given.resets_at,
252 _ => continue,
253 };
254 had.percent = given.percent;
255 had.severity = None;
256 changed = true;
257 }
258 changed.then(|| Snapshot {
259 windows,
260 observed_at: Some(now),
261 ..known.clone()
262 })
263}
264
265#[derive(Debug, Clone, Copy, PartialEq)]
267pub struct Room {
268 pub used: Option<f64>,
270 pub fullest: f64,
272}
273
274pub fn room(
286 theirs: &[Window],
287 lists_every_limit: bool,
288 mine: &[Window],
289 limit: &Window,
290 share: u8,
291 now: i64,
292) -> Option<Room> {
293 if theirs.is_empty() || theirs.iter().any(|window| reached(window.used(now), share)) {
294 return None;
295 }
296 let given = |had: &Window| theirs.iter().find(|window| window.same_limit(had));
297 if !lists_every_limit {
298 let gives = |kind: &str| {
299 theirs
300 .iter()
301 .any(|window| window.scope.is_none() && limit_name(&window.kind) == kind)
302 };
303 let overall = gives("session") && gives("weekly_all");
304 let unknown = |had: &Window| {
305 given(had).is_none() && !(overall && had.scope.is_some() && !had.same_limit(limit))
306 };
307 if std::iter::once(limit).chain(mine).any(unknown) {
308 return None;
309 }
310 }
311 let fullest = theirs
312 .iter()
313 .filter(|window| !window.same_limit(limit))
314 .map(|window| window.used(now))
315 .fold(0.0, f64::max);
316 Some(Room {
317 used: given(limit).map(|window| window.used(now)),
318 fullest,
319 })
320}
321
322pub fn roomiest<T>(candidates: impl IntoIterator<Item = (T, Room)>) -> Option<(T, Room)> {
325 candidates.into_iter().min_by(|(_, a), (_, b)| {
326 a.used
327 .unwrap_or(0.0)
328 .total_cmp(&b.used.unwrap_or(0.0))
329 .then_with(|| a.fullest.total_cmp(&b.fullest))
330 })
331}
332
333fn percent(v: &Value) -> Option<f64> {
335 let p = v.as_f64()?;
336 (p.is_finite() && p >= 0.0).then_some(p)
337}
338
339fn window_from_limit(l: &Value) -> Option<Window> {
340 let named = |what: &str| {
343 l.get("scope")
344 .and_then(|s| s.get(what))
345 .and_then(|m| m.get("display_name"))
346 .and_then(Value::as_str)
347 .map(str::to_owned)
348 };
349 Some(Window {
350 kind: l.get("kind")?.as_str()?.to_string(),
351 scope: named("model").or_else(|| named("surface")),
352 severity: l.get("severity").and_then(Value::as_str).map(str::to_owned),
353 percent: percent(l.get("percent")?)?,
354 resets_at: l
355 .get("resets_at")
356 .and_then(Value::as_str)
357 .and_then(time::parse),
358 is_active: l.get("is_active").and_then(Value::as_bool).unwrap_or(false),
359 length_seconds: l
360 .get("kind")
361 .and_then(Value::as_str)
362 .and_then(anthropic_window_length),
363 })
364}
365
366fn window_from_named(kind: &str, v: &Value) -> Option<Window> {
367 Some(Window {
368 kind: kind.to_string(),
369 scope: None,
370 severity: None,
371 percent: percent(v.get("utilization")?)?,
372 resets_at: v
373 .get("resets_at")
374 .and_then(Value::as_str)
375 .and_then(time::parse),
376 is_active: false,
377 length_seconds: anthropic_window_length(kind),
378 })
379}
380
381pub fn from_usage_object(u: &Value, observed_at: i64) -> Snapshot {
390 let limits = u.get("limits").and_then(Value::as_array);
391 let mut windows: Vec<Window> = limits
392 .into_iter()
393 .flatten()
394 .filter_map(window_from_limit)
395 .collect();
396 let lists_every_limit =
397 !windows.is_empty() && limits.is_some_and(|rows| rows.len() == windows.len());
398 if windows.is_empty() {
399 for kind in ["five_hour", "seven_day"] {
400 if let Some(w) = u.get(kind).and_then(|v| window_from_named(kind, v)) {
401 windows.push(w);
402 }
403 }
404 }
405 Snapshot {
406 windows,
407 observed_at: Some(observed_at),
408 answered_at: Some(observed_at),
409 lists_every_limit,
410 source: Source::Live,
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417
418 fn real_answer() -> Value {
421 serde_json::json!({
422 "five_hour": {"utilization": 62, "resets_at": "2026-09-20T22:20:00.095287+00:00"},
423 "seven_day": {"utilization": 48, "resets_at": "2026-09-27T02:00:00.095306+00:00"},
424 "limits": [
425 {"kind": "session", "group": "session", "percent": 62,
426 "resets_at": "2026-09-20T22:20:00.095287+00:00", "scope": null, "is_active": true},
427 {"kind": "weekly_all", "group": "weekly", "percent": 48,
428 "resets_at": "2026-09-27T02:00:00.095306+00:00", "scope": null, "is_active": false},
429 {"kind": "weekly_scoped", "group": "weekly", "percent": 0,
430 "resets_at": "2026-09-27T02:00:00+00:00",
431 "scope": {"model": {"id": null, "display_name": "Fable"}}, "is_active": false}
432 ]})
433 }
434
435 #[test]
436 fn reads_the_real_answers_shape() {
437 let s = from_usage_object(&real_answer(), 1789933772);
438 assert_eq!(s.windows.len(), 3);
439 assert_eq!(s.observed_at, Some(1789933772));
440 assert_eq!(s.answered_at, Some(1789933772));
441 assert!(s.lists_every_limit);
442 assert_eq!(s.source, Source::Live);
443 let scoped = s
444 .windows
445 .iter()
446 .find(|w| w.kind == "weekly_scoped")
447 .unwrap();
448 assert_eq!(scoped.scope.as_deref(), Some("Fable"));
449 }
450
451 #[test]
452 fn falls_back_to_the_named_windows_when_limits_is_missing() {
453 let mut answer = real_answer();
454 answer.as_object_mut().unwrap().remove("limits");
455 let s = from_usage_object(&answer, 1789933772);
456 assert_eq!(s.windows.len(), 2);
457 assert_eq!(s.windows[0].kind, "five_hour");
458 assert_eq!(s.windows[0].percent, 62.0);
459 assert!(!s.lists_every_limit, "nothing scoped to a model is named");
460 }
461
462 #[test]
463 fn a_nonsense_percentage_is_dropped_rather_than_drawn() {
464 for nonsense in [serde_json::json!(-5), serde_json::json!("75")] {
465 let mut answer = real_answer();
466 answer["limits"][0]["percent"] = nonsense;
467 let s = from_usage_object(&answer, 1789933772);
468 assert_eq!(s.windows.len(), 2);
469 assert!(
470 !s.lists_every_limit,
471 "the row dropped is a limit all the same"
472 );
473 }
474 }
475
476 #[test]
477 fn an_exceeded_limit_is_kept_not_dropped() {
478 let mut answer = real_answer();
479 answer["limits"][0]["percent"] = serde_json::json!(104);
480 let s = from_usage_object(&answer, 1789933772);
481 assert_eq!(s.windows.len(), 3);
482 assert_eq!(s.windows[0].percent, 104.0);
483 }
484
485 #[test]
486 fn a_share_is_one_whole_percentage_a_half_rounded_up() {
487 assert_eq!(whole(94.5), 95);
488 assert_eq!(whole(94.49), 94);
489 assert_eq!(whole(104.0), 104);
490 assert_eq!(whole(0.4), 0);
491 }
492
493 const NOW: i64 = 1_789_935_000;
494 const HOUR: i64 = 3_600;
495
496 fn measured(kind: &str, percent: f64, resets_at: Option<i64>) -> Window {
497 Window {
498 kind: kind.into(),
499 scope: None,
500 percent,
501 resets_at,
502 is_active: true,
503 severity: None,
504 length_seconds: anthropic_window_length(kind),
505 }
506 }
507
508 fn answer(windows: Vec<Window>, at: i64) -> Snapshot {
510 Snapshot {
511 windows,
512 observed_at: Some(at),
513 answered_at: Some(at),
514 lists_every_limit: true,
515 source: Source::Live,
516 }
517 }
518
519 fn held(windows: Vec<Window>, observed_at: i64, answered_at: Option<i64>) -> Snapshot {
522 Snapshot {
523 windows,
524 observed_at: Some(observed_at),
525 answered_at,
526 lists_every_limit: true,
527 source: Source::Remembered,
528 }
529 }
530
531 fn shares(reading: &Snapshot) -> Vec<(&str, f64)> {
532 reading
533 .windows
534 .iter()
535 .map(|w| (w.kind.as_str(), w.percent))
536 .collect()
537 }
538
539 #[test]
542 fn resets_under_a_minute_apart_are_one_reset() {
543 assert!(same_reset(NOW, NOW));
544 assert!(same_reset(NOW, NOW + 59));
545 assert!(same_reset(NOW + 59, NOW));
546 assert!(!same_reset(NOW, NOW + 60));
547 assert!(!same_reset(NOW + 60, NOW));
548 assert!(
549 !same_reset(0, NOW),
550 "no reset known is no reset in particular"
551 );
552 }
553
554 #[test]
557 fn a_window_may_still_run_until_a_minute_past_its_reset() {
558 assert!(may_still_run(NOW, NOW - 1));
559 assert!(may_still_run(NOW, NOW));
560 assert!(may_still_run(NOW, NOW + 59));
561 assert!(!may_still_run(NOW, NOW + 60));
562 assert!(!may_still_run(0, NOW));
563 }
564
565 #[test]
569 fn an_answer_replaces_every_limit_it_does_not_give() {
570 let known = held(
571 vec![
572 measured("session", 10.0, Some(NOW + HOUR)),
573 measured("weekly_all", 100.0, Some(NOW + 50 * HOUR)),
574 ],
575 NOW - 60,
576 None,
577 );
578 let given = answer(
579 vec![
580 measured("session", 20.0, Some(NOW + HOUR)),
581 Window {
582 scope: Some("Fable".into()),
583 ..measured("weekly_scoped", 0.0, Some(NOW + 50 * HOUR))
584 },
585 ],
586 NOW,
587 );
588 let reading = answered(Some(&known), &given, NOW);
589 assert_eq!(
590 shares(&reading),
591 [("session", 20.0), ("weekly_scoped", 0.0)]
592 );
593 assert_eq!(reading.answered_at, Some(NOW));
594 assert_eq!(reading.observed_at, Some(NOW));
595 }
596
597 #[test]
601 fn a_limit_whose_row_could_not_be_read_is_not_taken_away() {
602 let mut spent = real_answer();
603 spent["limits"][1]["percent"] = serde_json::json!(100);
604 let known = from_usage_object(&spent, NOW - 600);
605 let mut unread = spent.clone();
606 unread["limits"][0]["percent"] = serde_json::json!(70);
607 unread["limits"][1]["percent"] = Value::Null;
608 let reading = answered(Some(&known), &from_usage_object(&unread, NOW), NOW);
609 assert_eq!(
610 shares(&reading),
611 [
612 ("session", 70.0),
613 ("weekly_all", 100.0),
614 ("weekly_scoped", 0.0)
615 ]
616 );
617 }
618
619 #[test]
622 fn an_answer_in_the_older_shape_takes_no_limit_away() {
623 let mut spent = real_answer();
624 spent["limits"][2]["percent"] = serde_json::json!(100);
625 let known = from_usage_object(&spent, NOW - 600);
626 let mut older = real_answer();
627 older.as_object_mut().unwrap().remove("limits");
628 let reading = answered(Some(&known), &from_usage_object(&older, NOW), NOW);
629 assert_eq!(
630 shares(&reading),
631 [
632 ("five_hour", 62.0),
633 ("seven_day", 48.0),
634 ("weekly_scoped", 100.0)
635 ]
636 );
637 }
638
639 #[test]
642 fn a_reading_stamped_ahead_of_the_clock_holds_no_answer_back() {
643 let known = held(
644 vec![measured("session", 80.0, Some(NOW + 2 * HOUR))],
645 NOW + HOUR,
646 Some(NOW + HOUR),
647 );
648 let given = answer(vec![measured("session", 20.0, Some(NOW + 2 * HOUR))], NOW);
649 let reading = answered(Some(&known), &given, NOW);
650 assert_eq!(shares(&reading), [("session", 20.0)]);
651 assert_eq!(
652 (reading.observed_at, reading.answered_at),
653 (Some(NOW), Some(NOW))
654 );
655 }
656
657 #[test]
662 fn a_window_openai_leaves_out_stands_until_its_reset() {
663 let known = held(
664 vec![
665 measured("five_hour", 30.0, Some(NOW + HOUR)),
666 measured("1_day", 60.0, Some(NOW - 60)),
667 measured("2_day", 20.0, None),
668 measured("seven_day", 40.0, Some(NOW + 50 * HOUR)),
669 ],
670 NOW - 600,
671 Some(NOW - 600),
672 );
673 let whole = answer(
674 vec![measured("seven_day", 45.0, Some(NOW + 50 * HOUR))],
675 NOW,
676 );
677 let given = Snapshot {
678 lists_every_limit: false,
679 ..whole.clone()
680 };
681 let reading = answered(Some(&known), &given, NOW);
682 assert_eq!(shares(&reading), [("five_hour", 30.0), ("seven_day", 45.0)]);
683 assert!(!reading.lists_every_limit);
684 assert_eq!(
685 shares(&answered(Some(&known), &whole, NOW)),
686 [("seven_day", 45.0)],
687 "where the answer lists every limit"
688 );
689 }
690
691 #[test]
694 fn a_session_moves_a_share_only_inside_a_window_it_was_given() {
695 let known = held(
696 vec![measured("session", 22.0, Some(NOW + HOUR))],
697 NOW - 60,
698 Some(NOW - 60),
699 );
700 let higher = moved(
701 &known,
702 &[measured("five_hour", 25.0, Some(NOW + HOUR))],
703 NOW,
704 )
705 .expect("a share moved");
706 assert_eq!(
707 shares(&higher),
708 [("session", 25.0)],
709 "under the name the service gave it"
710 );
711 assert_eq!(higher.answered_at, Some(NOW - 60), "and no answer since");
712 assert_eq!(
713 moved(
714 &known,
715 &[measured("five_hour", 5.0, Some(NOW + 6 * HOUR))],
716 NOW
717 ),
718 None,
719 "another reset still ahead"
720 );
721 assert_eq!(
722 moved(
723 &known,
724 &[measured("seven_day", 40.0, Some(NOW + 50 * HOUR))],
725 NOW
726 ),
727 None,
728 "a limit the service did not give"
729 );
730 }
731
732 #[test]
736 fn a_session_takes_the_next_window_of_a_limit_whose_reset_has_passed() {
737 let mut over = measured("session", 90.0, Some(NOW - 60));
738 over.severity = Some("warning".into());
739 let not_running = measured("session", 0.0, None);
740 for had in [over, not_running] {
741 let known = held(vec![had.clone()], NOW - HOUR, Some(NOW - HOUR));
742 let next = moved(
743 &known,
744 &[measured("five_hour", 3.0, Some(NOW + 5 * HOUR))],
745 NOW,
746 )
747 .expect("the next window");
748 let window = &next.windows[0];
749 assert_eq!(window.kind, "session", "{had:?}");
750 assert_eq!(
751 (window.percent, window.resets_at),
752 (3.0, Some(NOW + 5 * HOUR))
753 );
754 assert_eq!(window.severity, None, "a share the service has not graded");
755 assert!(window.is_active, "working against it as the service said");
756 assert_eq!(next.observed_at, Some(NOW));
757 assert_eq!(next.answered_at, Some(NOW - HOUR));
758 }
759 }
760
761 #[test]
765 fn a_session_never_moves_a_share_back() {
766 let known = held(
767 vec![measured("session", 22.0, Some(NOW + HOUR))],
768 NOW - 60,
769 Some(NOW - 60),
770 );
771 for behind in [
772 measured("five_hour", 20.0, Some(NOW + HOUR)),
773 measured("five_hour", 22.0, Some(NOW + HOUR)),
774 measured("five_hour", 95.0, Some(NOW - 4 * HOUR)),
775 ] {
776 assert_eq!(
777 moved(&known, std::slice::from_ref(&behind), NOW),
778 None,
779 "{behind:?}"
780 );
781 }
782 }
783
784 #[test]
788 fn resets_a_second_apart_are_one_window() {
789 let known = held(
790 vec![measured("session", 22.0, Some(NOW + HOUR))],
791 NOW - 60,
792 Some(NOW - 60),
793 );
794 let ahead = moved(
795 &known,
796 &[measured("five_hour", 25.0, Some(NOW + HOUR + 1))],
797 NOW,
798 )
799 .expect("a share moved");
800 assert_eq!(shares(&ahead), [("session", 25.0)]);
801 assert_eq!(
802 ahead.windows[0].resets_at,
803 Some(NOW + HOUR),
804 "the reset is the service's"
805 );
806 }
807
808 #[test]
814 fn an_answer_taken_after_everything_known_is_what_the_limits_are_now() {
815 let known = held(
816 vec![
817 measured("session", 0.0, None),
818 measured("weekly_all", 100.0, Some(NOW + 33 * HOUR)),
819 ],
820 NOW - HOUR,
821 Some(NOW - HOUR),
822 );
823 let given = answer(
824 vec![
825 measured("session", 5.0, Some(NOW + 5 * HOUR)),
826 measured("weekly_all", 14.0, Some(NOW + 33 * HOUR)),
827 ],
828 NOW,
829 );
830 let reading = answered(Some(&known), &given, NOW);
831 assert_eq!(shares(&reading), [("session", 5.0), ("weekly_all", 14.0)]);
832 assert_eq!(reading.observed_at, Some(NOW));
833 assert_eq!(reading.source, Source::Live);
834
835 let unused = answer(vec![measured("weekly_all", 0.0, None)], NOW);
836 assert_eq!(
837 shares(&answered(Some(&known), &unused, NOW)),
838 [("weekly_all", 0.0)],
839 "and one that finds nothing used and no window running"
840 );
841 }
842
843 #[test]
846 fn an_answer_taken_before_a_session_moved_a_share_does_not_lower_it() {
847 let known = held(
848 vec![measured("weekly_all", 100.0, Some(NOW + 33 * HOUR))],
849 NOW - 60,
850 Some(NOW - 2 * HOUR),
851 );
852 for taken in [NOW - 60, NOW - HOUR] {
853 let given = answer(
854 vec![measured("weekly_all", 14.0, Some(NOW + 33 * HOUR))],
855 taken,
856 );
857 let reading = answered(Some(&known), &given, NOW);
858 assert_eq!(
859 shares(&reading),
860 [("weekly_all", 100.0)],
861 "taken at {taken}"
862 );
863 assert_eq!(reading.observed_at, Some(NOW - 60));
864 assert_eq!(reading.answered_at, Some(taken));
865 }
866 }
867
868 #[test]
870 fn an_answer_older_than_the_last_changes_nothing() {
871 let known = held(
872 vec![measured("session", 40.0, Some(NOW + HOUR))],
873 NOW,
874 Some(NOW),
875 );
876 let late = answer(vec![measured("session", 30.0, Some(NOW + HOUR))], NOW - 60);
877 assert_eq!(answered(Some(&known), &late, NOW), known);
878 }
879
880 #[test]
882 fn a_session_leaves_the_limits_it_does_not_pass_as_they_were() {
883 let scoped = Window {
884 scope: Some("Fable".into()),
885 ..measured("weekly_scoped", 5.0, Some(NOW + 50 * HOUR))
886 };
887 let known = held(
888 vec![measured("session", 22.0, Some(NOW + HOUR)), scoped],
889 NOW - 60,
890 Some(NOW - 60),
891 );
892 let reading = moved(
893 &known,
894 &[measured("five_hour", 25.0, Some(NOW + HOUR))],
895 NOW,
896 )
897 .expect("a share moved");
898 assert_eq!(
899 shares(&reading),
900 [("session", 25.0), ("weekly_scoped", 5.0)]
901 );
902 }
903
904 #[test]
908 fn a_session_never_takes_a_limit_away() {
909 let known = held(
910 vec![
911 measured("session", 40.0, Some(NOW - 10)),
912 measured("weekly_all", 30.0, Some(NOW + 50 * HOUR)),
913 ],
914 NOW - HOUR,
915 Some(NOW - HOUR),
916 );
917 let reading = moved(
918 &known,
919 &[measured("seven_day", 31.0, Some(NOW + 50 * HOUR))],
920 NOW,
921 )
922 .expect("a share moved");
923 assert_eq!(shares(&reading), [("session", 40.0), ("weekly_all", 31.0)]);
924 assert_eq!(reading.windows[0].used(NOW), 0.0);
925 }
926
927 #[test]
929 fn a_first_answer_is_the_reading() {
930 let given = answer(vec![measured("session", 22.0, Some(NOW + HOUR))], NOW);
931 assert_eq!(answered(None, &given, NOW), given);
932 assert_eq!(
933 moved(&held(Vec::new(), NOW, None), &given.windows, NOW),
934 None
935 );
936 }
937
938 #[test]
942 fn a_reading_is_stamped_when_something_moves_or_the_service_answers() {
943 let known = held(
944 vec![measured("session", 22.0, Some(NOW + HOUR))],
945 NOW - 600,
946 Some(NOW - 600),
947 );
948 let repeated = measured("five_hour", 22.0, Some(NOW + HOUR));
949 assert_eq!(moved(&known, &[repeated], NOW), None);
950
951 let ahead = measured("five_hour", 23.0, Some(NOW + HOUR));
952 assert_eq!(moved(&known, &[ahead], NOW).unwrap().observed_at, Some(NOW));
953
954 let confirmed = answer(known.windows.clone(), NOW - 5);
955 let reading = answered(Some(&known), &confirmed, NOW);
956 assert_eq!(reading.windows, known.windows);
957 assert_eq!(
958 (reading.observed_at, reading.answered_at),
959 (Some(NOW - 5), Some(NOW - 5)),
960 "an answer that finds the same confirms it"
961 );
962 assert_eq!(reading.source, Source::Live);
963 }
964
965 #[test]
969 fn a_window_past_its_reset_is_reset_by_an_answer_that_finds_nothing_used() {
970 let known = held(
971 vec![measured("session", 100.0, Some(NOW - HOUR))],
972 NOW - 2 * HOUR,
973 Some(NOW - 2 * HOUR),
974 );
975 for said in [
976 measured("session", 0.0, None),
977 measured("session", 0.0, Some(NOW - HOUR)),
978 measured("five_hour", 0.0, None),
979 ] {
980 let given = answer(vec![said.clone()], NOW - 5);
981 let reading = answered(Some(&known), &given, NOW);
982 assert_eq!(reading.windows, std::slice::from_ref(&said), "{said:?}");
983 assert_eq!(
984 answered(Some(&reading), &given, NOW),
985 reading,
986 "a repeat changes nothing"
987 );
988 }
989 assert_eq!(
990 moved(&known, &[measured("five_hour", 0.0, None)], NOW),
991 None,
992 "a session's window with no reset says nothing about now"
993 );
994 }
995}