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 pub weekly: bool,
275}
276
277pub fn room(
289 theirs: &[Window],
290 lists_every_limit: bool,
291 mine: &[Window],
292 limit: &Window,
293 share: u8,
294 now: i64,
295) -> Option<Room> {
296 if theirs.is_empty() || theirs.iter().any(|window| reached(window.used(now), share)) {
297 return None;
298 }
299 let given = |had: &Window| theirs.iter().find(|window| window.same_limit(had));
300 let gives = |kind: &str| {
301 theirs
302 .iter()
303 .any(|window| window.scope.is_none() && limit_name(&window.kind) == kind)
304 };
305 if !lists_every_limit {
306 let overall = gives("session") && gives("weekly_all");
307 let unknown = |had: &Window| {
308 given(had).is_none() && !(overall && had.scope.is_some() && !had.same_limit(limit))
309 };
310 if std::iter::once(limit).chain(mine).any(unknown) {
311 return None;
312 }
313 }
314 let fullest = theirs
315 .iter()
316 .filter(|window| !window.same_limit(limit))
317 .map(|window| window.used(now))
318 .fold(0.0, f64::max);
319 Some(Room {
320 used: given(limit).map(|window| window.used(now)),
321 fullest,
322 weekly: !lists_every_limit || gives("weekly_all"),
323 })
324}
325
326pub fn preferred<T>(candidates: impl IntoIterator<Item = (T, Room)>) -> Option<(T, Room)> {
331 candidates.into_iter().min_by(|(_, a), (_, b)| {
332 a.weekly
333 .cmp(&b.weekly)
334 .then_with(|| a.used.unwrap_or(0.0).total_cmp(&b.used.unwrap_or(0.0)))
335 .then_with(|| a.fullest.total_cmp(&b.fullest))
336 })
337}
338
339fn percent(v: &Value) -> Option<f64> {
341 let p = v.as_f64()?;
342 (p.is_finite() && p >= 0.0).then_some(p)
343}
344
345fn window_from_limit(l: &Value) -> Option<Window> {
346 let named = |what: &str| {
349 l.get("scope")
350 .and_then(|s| s.get(what))
351 .and_then(|m| m.get("display_name"))
352 .and_then(Value::as_str)
353 .map(str::to_owned)
354 };
355 Some(Window {
356 kind: l.get("kind")?.as_str()?.to_string(),
357 scope: named("model").or_else(|| named("surface")),
358 severity: l.get("severity").and_then(Value::as_str).map(str::to_owned),
359 percent: percent(l.get("percent")?)?,
360 resets_at: l
361 .get("resets_at")
362 .and_then(Value::as_str)
363 .and_then(time::parse),
364 is_active: l.get("is_active").and_then(Value::as_bool).unwrap_or(false),
365 length_seconds: l
366 .get("kind")
367 .and_then(Value::as_str)
368 .and_then(anthropic_window_length),
369 })
370}
371
372fn window_from_named(kind: &str, v: &Value) -> Option<Window> {
373 Some(Window {
374 kind: kind.to_string(),
375 scope: None,
376 severity: None,
377 percent: percent(v.get("utilization")?)?,
378 resets_at: v
379 .get("resets_at")
380 .and_then(Value::as_str)
381 .and_then(time::parse),
382 is_active: false,
383 length_seconds: anthropic_window_length(kind),
384 })
385}
386
387pub fn from_usage_object(u: &Value, observed_at: i64) -> Snapshot {
396 let limits = u.get("limits").and_then(Value::as_array);
397 let mut windows: Vec<Window> = limits
398 .into_iter()
399 .flatten()
400 .filter_map(window_from_limit)
401 .collect();
402 let lists_every_limit =
403 !windows.is_empty() && limits.is_some_and(|rows| rows.len() == windows.len());
404 if windows.is_empty() {
405 for kind in ["five_hour", "seven_day"] {
406 if let Some(w) = u.get(kind).and_then(|v| window_from_named(kind, v)) {
407 windows.push(w);
408 }
409 }
410 }
411 Snapshot {
412 windows,
413 observed_at: Some(observed_at),
414 answered_at: Some(observed_at),
415 lists_every_limit,
416 source: Source::Live,
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 fn real_answer() -> Value {
427 serde_json::json!({
428 "five_hour": {"utilization": 62, "resets_at": "2026-09-20T22:20:00.095287+00:00"},
429 "seven_day": {"utilization": 48, "resets_at": "2026-09-27T02:00:00.095306+00:00"},
430 "limits": [
431 {"kind": "session", "group": "session", "percent": 62,
432 "resets_at": "2026-09-20T22:20:00.095287+00:00", "scope": null, "is_active": true},
433 {"kind": "weekly_all", "group": "weekly", "percent": 48,
434 "resets_at": "2026-09-27T02:00:00.095306+00:00", "scope": null, "is_active": false},
435 {"kind": "weekly_scoped", "group": "weekly", "percent": 0,
436 "resets_at": "2026-09-27T02:00:00+00:00",
437 "scope": {"model": {"id": null, "display_name": "Fable"}}, "is_active": false}
438 ]})
439 }
440
441 #[test]
442 fn reads_the_real_answers_shape() {
443 let s = from_usage_object(&real_answer(), 1789933772);
444 assert_eq!(s.windows.len(), 3);
445 assert_eq!(s.observed_at, Some(1789933772));
446 assert_eq!(s.answered_at, Some(1789933772));
447 assert!(s.lists_every_limit);
448 assert_eq!(s.source, Source::Live);
449 let scoped = s
450 .windows
451 .iter()
452 .find(|w| w.kind == "weekly_scoped")
453 .unwrap();
454 assert_eq!(scoped.scope.as_deref(), Some("Fable"));
455 }
456
457 #[test]
458 fn falls_back_to_the_named_windows_when_limits_is_missing() {
459 let mut answer = real_answer();
460 answer.as_object_mut().unwrap().remove("limits");
461 let s = from_usage_object(&answer, 1789933772);
462 assert_eq!(s.windows.len(), 2);
463 assert_eq!(s.windows[0].kind, "five_hour");
464 assert_eq!(s.windows[0].percent, 62.0);
465 assert!(!s.lists_every_limit, "nothing scoped to a model is named");
466 }
467
468 #[test]
469 fn a_nonsense_percentage_is_dropped_rather_than_drawn() {
470 for nonsense in [serde_json::json!(-5), serde_json::json!("75")] {
471 let mut answer = real_answer();
472 answer["limits"][0]["percent"] = nonsense;
473 let s = from_usage_object(&answer, 1789933772);
474 assert_eq!(s.windows.len(), 2);
475 assert!(
476 !s.lists_every_limit,
477 "the row dropped is a limit all the same"
478 );
479 }
480 }
481
482 #[test]
483 fn an_exceeded_limit_is_kept_not_dropped() {
484 let mut answer = real_answer();
485 answer["limits"][0]["percent"] = serde_json::json!(104);
486 let s = from_usage_object(&answer, 1789933772);
487 assert_eq!(s.windows.len(), 3);
488 assert_eq!(s.windows[0].percent, 104.0);
489 }
490
491 #[test]
492 fn a_share_is_one_whole_percentage_a_half_rounded_up() {
493 assert_eq!(whole(94.5), 95);
494 assert_eq!(whole(94.49), 94);
495 assert_eq!(whole(104.0), 104);
496 assert_eq!(whole(0.4), 0);
497 }
498
499 const NOW: i64 = 1_789_935_000;
500 const HOUR: i64 = 3_600;
501
502 fn measured(kind: &str, percent: f64, resets_at: Option<i64>) -> Window {
503 Window {
504 kind: kind.into(),
505 scope: None,
506 percent,
507 resets_at,
508 is_active: true,
509 severity: None,
510 length_seconds: anthropic_window_length(kind),
511 }
512 }
513
514 fn answer(windows: Vec<Window>, at: i64) -> Snapshot {
516 Snapshot {
517 windows,
518 observed_at: Some(at),
519 answered_at: Some(at),
520 lists_every_limit: true,
521 source: Source::Live,
522 }
523 }
524
525 fn held(windows: Vec<Window>, observed_at: i64, answered_at: Option<i64>) -> Snapshot {
528 Snapshot {
529 windows,
530 observed_at: Some(observed_at),
531 answered_at,
532 lists_every_limit: true,
533 source: Source::Remembered,
534 }
535 }
536
537 fn shares(reading: &Snapshot) -> Vec<(&str, f64)> {
538 reading
539 .windows
540 .iter()
541 .map(|w| (w.kind.as_str(), w.percent))
542 .collect()
543 }
544
545 #[test]
548 fn resets_under_a_minute_apart_are_one_reset() {
549 assert!(same_reset(NOW, NOW));
550 assert!(same_reset(NOW, NOW + 59));
551 assert!(same_reset(NOW + 59, NOW));
552 assert!(!same_reset(NOW, NOW + 60));
553 assert!(!same_reset(NOW + 60, NOW));
554 assert!(
555 !same_reset(0, NOW),
556 "no reset known is no reset in particular"
557 );
558 }
559
560 #[test]
563 fn a_window_may_still_run_until_a_minute_past_its_reset() {
564 assert!(may_still_run(NOW, NOW - 1));
565 assert!(may_still_run(NOW, NOW));
566 assert!(may_still_run(NOW, NOW + 59));
567 assert!(!may_still_run(NOW, NOW + 60));
568 assert!(!may_still_run(0, NOW));
569 }
570
571 #[test]
575 fn an_answer_replaces_every_limit_it_does_not_give() {
576 let known = held(
577 vec![
578 measured("session", 10.0, Some(NOW + HOUR)),
579 measured("weekly_all", 100.0, Some(NOW + 50 * HOUR)),
580 ],
581 NOW - 60,
582 None,
583 );
584 let given = answer(
585 vec![
586 measured("session", 20.0, Some(NOW + HOUR)),
587 Window {
588 scope: Some("Fable".into()),
589 ..measured("weekly_scoped", 0.0, Some(NOW + 50 * HOUR))
590 },
591 ],
592 NOW,
593 );
594 let reading = answered(Some(&known), &given, NOW);
595 assert_eq!(
596 shares(&reading),
597 [("session", 20.0), ("weekly_scoped", 0.0)]
598 );
599 assert_eq!(reading.answered_at, Some(NOW));
600 assert_eq!(reading.observed_at, Some(NOW));
601 }
602
603 #[test]
607 fn a_limit_whose_row_could_not_be_read_is_not_taken_away() {
608 let mut spent = real_answer();
609 spent["limits"][1]["percent"] = serde_json::json!(100);
610 let known = from_usage_object(&spent, NOW - 600);
611 let mut unread = spent.clone();
612 unread["limits"][0]["percent"] = serde_json::json!(70);
613 unread["limits"][1]["percent"] = Value::Null;
614 let reading = answered(Some(&known), &from_usage_object(&unread, NOW), NOW);
615 assert_eq!(
616 shares(&reading),
617 [
618 ("session", 70.0),
619 ("weekly_all", 100.0),
620 ("weekly_scoped", 0.0)
621 ]
622 );
623 }
624
625 #[test]
628 fn an_answer_in_the_older_shape_takes_no_limit_away() {
629 let mut spent = real_answer();
630 spent["limits"][2]["percent"] = serde_json::json!(100);
631 let known = from_usage_object(&spent, NOW - 600);
632 let mut older = real_answer();
633 older.as_object_mut().unwrap().remove("limits");
634 let reading = answered(Some(&known), &from_usage_object(&older, NOW), NOW);
635 assert_eq!(
636 shares(&reading),
637 [
638 ("five_hour", 62.0),
639 ("seven_day", 48.0),
640 ("weekly_scoped", 100.0)
641 ]
642 );
643 }
644
645 #[test]
648 fn a_reading_stamped_ahead_of_the_clock_holds_no_answer_back() {
649 let known = held(
650 vec![measured("session", 80.0, Some(NOW + 2 * HOUR))],
651 NOW + HOUR,
652 Some(NOW + HOUR),
653 );
654 let given = answer(vec![measured("session", 20.0, Some(NOW + 2 * HOUR))], NOW);
655 let reading = answered(Some(&known), &given, NOW);
656 assert_eq!(shares(&reading), [("session", 20.0)]);
657 assert_eq!(
658 (reading.observed_at, reading.answered_at),
659 (Some(NOW), Some(NOW))
660 );
661 }
662
663 #[test]
668 fn a_window_openai_leaves_out_stands_until_its_reset() {
669 let known = held(
670 vec![
671 measured("five_hour", 30.0, Some(NOW + HOUR)),
672 measured("1_day", 60.0, Some(NOW - 60)),
673 measured("2_day", 20.0, None),
674 measured("seven_day", 40.0, Some(NOW + 50 * HOUR)),
675 ],
676 NOW - 600,
677 Some(NOW - 600),
678 );
679 let whole = answer(
680 vec![measured("seven_day", 45.0, Some(NOW + 50 * HOUR))],
681 NOW,
682 );
683 let given = Snapshot {
684 lists_every_limit: false,
685 ..whole.clone()
686 };
687 let reading = answered(Some(&known), &given, NOW);
688 assert_eq!(shares(&reading), [("five_hour", 30.0), ("seven_day", 45.0)]);
689 assert!(!reading.lists_every_limit);
690 assert_eq!(
691 shares(&answered(Some(&known), &whole, NOW)),
692 [("seven_day", 45.0)],
693 "where the answer lists every limit"
694 );
695 }
696
697 #[test]
700 fn a_session_moves_a_share_only_inside_a_window_it_was_given() {
701 let known = held(
702 vec![measured("session", 22.0, Some(NOW + HOUR))],
703 NOW - 60,
704 Some(NOW - 60),
705 );
706 let higher = moved(
707 &known,
708 &[measured("five_hour", 25.0, Some(NOW + HOUR))],
709 NOW,
710 )
711 .expect("a share moved");
712 assert_eq!(
713 shares(&higher),
714 [("session", 25.0)],
715 "under the name the service gave it"
716 );
717 assert_eq!(higher.answered_at, Some(NOW - 60), "and no answer since");
718 assert_eq!(
719 moved(
720 &known,
721 &[measured("five_hour", 5.0, Some(NOW + 6 * HOUR))],
722 NOW
723 ),
724 None,
725 "another reset still ahead"
726 );
727 assert_eq!(
728 moved(
729 &known,
730 &[measured("seven_day", 40.0, Some(NOW + 50 * HOUR))],
731 NOW
732 ),
733 None,
734 "a limit the service did not give"
735 );
736 }
737
738 #[test]
742 fn a_session_takes_the_next_window_of_a_limit_whose_reset_has_passed() {
743 let mut over = measured("session", 90.0, Some(NOW - 60));
744 over.severity = Some("warning".into());
745 let not_running = measured("session", 0.0, None);
746 for had in [over, not_running] {
747 let known = held(vec![had.clone()], NOW - HOUR, Some(NOW - HOUR));
748 let next = moved(
749 &known,
750 &[measured("five_hour", 3.0, Some(NOW + 5 * HOUR))],
751 NOW,
752 )
753 .expect("the next window");
754 let window = &next.windows[0];
755 assert_eq!(window.kind, "session", "{had:?}");
756 assert_eq!(
757 (window.percent, window.resets_at),
758 (3.0, Some(NOW + 5 * HOUR))
759 );
760 assert_eq!(window.severity, None, "a share the service has not graded");
761 assert!(window.is_active, "working against it as the service said");
762 assert_eq!(next.observed_at, Some(NOW));
763 assert_eq!(next.answered_at, Some(NOW - HOUR));
764 }
765 }
766
767 #[test]
771 fn a_session_never_moves_a_share_back() {
772 let known = held(
773 vec![measured("session", 22.0, Some(NOW + HOUR))],
774 NOW - 60,
775 Some(NOW - 60),
776 );
777 for behind in [
778 measured("five_hour", 20.0, Some(NOW + HOUR)),
779 measured("five_hour", 22.0, Some(NOW + HOUR)),
780 measured("five_hour", 95.0, Some(NOW - 4 * HOUR)),
781 ] {
782 assert_eq!(
783 moved(&known, std::slice::from_ref(&behind), NOW),
784 None,
785 "{behind:?}"
786 );
787 }
788 }
789
790 #[test]
794 fn resets_a_second_apart_are_one_window() {
795 let known = held(
796 vec![measured("session", 22.0, Some(NOW + HOUR))],
797 NOW - 60,
798 Some(NOW - 60),
799 );
800 let ahead = moved(
801 &known,
802 &[measured("five_hour", 25.0, Some(NOW + HOUR + 1))],
803 NOW,
804 )
805 .expect("a share moved");
806 assert_eq!(shares(&ahead), [("session", 25.0)]);
807 assert_eq!(
808 ahead.windows[0].resets_at,
809 Some(NOW + HOUR),
810 "the reset is the service's"
811 );
812 }
813
814 #[test]
820 fn an_answer_taken_after_everything_known_is_what_the_limits_are_now() {
821 let known = held(
822 vec![
823 measured("session", 0.0, None),
824 measured("weekly_all", 100.0, Some(NOW + 33 * HOUR)),
825 ],
826 NOW - HOUR,
827 Some(NOW - HOUR),
828 );
829 let given = answer(
830 vec![
831 measured("session", 5.0, Some(NOW + 5 * HOUR)),
832 measured("weekly_all", 14.0, Some(NOW + 33 * HOUR)),
833 ],
834 NOW,
835 );
836 let reading = answered(Some(&known), &given, NOW);
837 assert_eq!(shares(&reading), [("session", 5.0), ("weekly_all", 14.0)]);
838 assert_eq!(reading.observed_at, Some(NOW));
839 assert_eq!(reading.source, Source::Live);
840
841 let unused = answer(vec![measured("weekly_all", 0.0, None)], NOW);
842 assert_eq!(
843 shares(&answered(Some(&known), &unused, NOW)),
844 [("weekly_all", 0.0)],
845 "and one that finds nothing used and no window running"
846 );
847 }
848
849 #[test]
852 fn an_answer_taken_before_a_session_moved_a_share_does_not_lower_it() {
853 let known = held(
854 vec![measured("weekly_all", 100.0, Some(NOW + 33 * HOUR))],
855 NOW - 60,
856 Some(NOW - 2 * HOUR),
857 );
858 for taken in [NOW - 60, NOW - HOUR] {
859 let given = answer(
860 vec![measured("weekly_all", 14.0, Some(NOW + 33 * HOUR))],
861 taken,
862 );
863 let reading = answered(Some(&known), &given, NOW);
864 assert_eq!(
865 shares(&reading),
866 [("weekly_all", 100.0)],
867 "taken at {taken}"
868 );
869 assert_eq!(reading.observed_at, Some(NOW - 60));
870 assert_eq!(reading.answered_at, Some(taken));
871 }
872 }
873
874 #[test]
876 fn an_answer_older_than_the_last_changes_nothing() {
877 let known = held(
878 vec![measured("session", 40.0, Some(NOW + HOUR))],
879 NOW,
880 Some(NOW),
881 );
882 let late = answer(vec![measured("session", 30.0, Some(NOW + HOUR))], NOW - 60);
883 assert_eq!(answered(Some(&known), &late, NOW), known);
884 }
885
886 #[test]
888 fn a_session_leaves_the_limits_it_does_not_pass_as_they_were() {
889 let scoped = Window {
890 scope: Some("Fable".into()),
891 ..measured("weekly_scoped", 5.0, Some(NOW + 50 * HOUR))
892 };
893 let known = held(
894 vec![measured("session", 22.0, Some(NOW + HOUR)), scoped],
895 NOW - 60,
896 Some(NOW - 60),
897 );
898 let reading = moved(
899 &known,
900 &[measured("five_hour", 25.0, Some(NOW + HOUR))],
901 NOW,
902 )
903 .expect("a share moved");
904 assert_eq!(
905 shares(&reading),
906 [("session", 25.0), ("weekly_scoped", 5.0)]
907 );
908 }
909
910 #[test]
914 fn a_session_never_takes_a_limit_away() {
915 let known = held(
916 vec![
917 measured("session", 40.0, Some(NOW - 10)),
918 measured("weekly_all", 30.0, Some(NOW + 50 * HOUR)),
919 ],
920 NOW - HOUR,
921 Some(NOW - HOUR),
922 );
923 let reading = moved(
924 &known,
925 &[measured("seven_day", 31.0, Some(NOW + 50 * HOUR))],
926 NOW,
927 )
928 .expect("a share moved");
929 assert_eq!(shares(&reading), [("session", 40.0), ("weekly_all", 31.0)]);
930 assert_eq!(reading.windows[0].used(NOW), 0.0);
931 }
932
933 #[test]
935 fn a_first_answer_is_the_reading() {
936 let given = answer(vec![measured("session", 22.0, Some(NOW + HOUR))], NOW);
937 assert_eq!(answered(None, &given, NOW), given);
938 assert_eq!(
939 moved(&held(Vec::new(), NOW, None), &given.windows, NOW),
940 None
941 );
942 }
943
944 #[test]
948 fn a_reading_is_stamped_when_something_moves_or_the_service_answers() {
949 let known = held(
950 vec![measured("session", 22.0, Some(NOW + HOUR))],
951 NOW - 600,
952 Some(NOW - 600),
953 );
954 let repeated = measured("five_hour", 22.0, Some(NOW + HOUR));
955 assert_eq!(moved(&known, &[repeated], NOW), None);
956
957 let ahead = measured("five_hour", 23.0, Some(NOW + HOUR));
958 assert_eq!(moved(&known, &[ahead], NOW).unwrap().observed_at, Some(NOW));
959
960 let confirmed = answer(known.windows.clone(), NOW - 5);
961 let reading = answered(Some(&known), &confirmed, NOW);
962 assert_eq!(reading.windows, known.windows);
963 assert_eq!(
964 (reading.observed_at, reading.answered_at),
965 (Some(NOW - 5), Some(NOW - 5)),
966 "an answer that finds the same confirms it"
967 );
968 assert_eq!(reading.source, Source::Live);
969 }
970
971 #[test]
975 fn a_window_past_its_reset_is_reset_by_an_answer_that_finds_nothing_used() {
976 let known = held(
977 vec![measured("session", 100.0, Some(NOW - HOUR))],
978 NOW - 2 * HOUR,
979 Some(NOW - 2 * HOUR),
980 );
981 for said in [
982 measured("session", 0.0, None),
983 measured("session", 0.0, Some(NOW - HOUR)),
984 measured("five_hour", 0.0, None),
985 ] {
986 let given = answer(vec![said.clone()], NOW - 5);
987 let reading = answered(Some(&known), &given, NOW);
988 assert_eq!(reading.windows, std::slice::from_ref(&said), "{said:?}");
989 assert_eq!(
990 answered(Some(&reading), &given, NOW),
991 reading,
992 "a repeat changes nothing"
993 );
994 }
995 assert_eq!(
996 moved(&known, &[measured("five_hour", 0.0, None)], NOW),
997 None,
998 "a session's window with no reset says nothing about now"
999 );
1000 }
1001}