1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
6pub enum RateLimitStatus {
7 Allowed,
9 AllowedWarning,
11 Rejected,
13 Unknown(String),
15}
16
17impl RateLimitStatus {
18 pub fn as_str(&self) -> &str {
19 match self {
20 Self::Allowed => "allowed",
21 Self::AllowedWarning => "allowed_warning",
22 Self::Rejected => "rejected",
23 Self::Unknown(s) => s.as_str(),
24 }
25 }
26}
27
28impl fmt::Display for RateLimitStatus {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.write_str(self.as_str())
31 }
32}
33
34impl From<&str> for RateLimitStatus {
35 fn from(s: &str) -> Self {
36 match s {
37 "allowed" => Self::Allowed,
38 "allowed_warning" => Self::AllowedWarning,
39 "rejected" => Self::Rejected,
40 other => Self::Unknown(other.to_string()),
41 }
42 }
43}
44
45impl Serialize for RateLimitStatus {
46 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
47 serializer.serialize_str(self.as_str())
48 }
49}
50
51impl<'de> Deserialize<'de> for RateLimitStatus {
52 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
53 let s = String::deserialize(deserializer)?;
54 Ok(Self::from(s.as_str()))
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub enum RateLimitWindow {
61 FiveHour,
63 SevenDay,
65 SevenDayOpus,
67 SevenDaySonnet,
69 SevenDayOverageIncluded,
71 Overage,
73 Unknown(String),
75}
76
77impl RateLimitWindow {
78 pub fn as_str(&self) -> &str {
79 match self {
80 Self::FiveHour => "five_hour",
81 Self::SevenDay => "seven_day",
82 Self::SevenDayOpus => "seven_day_opus",
83 Self::SevenDaySonnet => "seven_day_sonnet",
84 Self::SevenDayOverageIncluded => "seven_day_overage_included",
85 Self::Overage => "overage",
86 Self::Unknown(s) => s.as_str(),
87 }
88 }
89}
90
91impl fmt::Display for RateLimitWindow {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str(self.as_str())
94 }
95}
96
97impl From<&str> for RateLimitWindow {
98 fn from(s: &str) -> Self {
99 match s {
100 "five_hour" => Self::FiveHour,
101 "seven_day" => Self::SevenDay,
102 "seven_day_opus" => Self::SevenDayOpus,
103 "seven_day_sonnet" => Self::SevenDaySonnet,
104 "seven_day_overage_included" => Self::SevenDayOverageIncluded,
105 "overage" => Self::Overage,
106 other => Self::Unknown(other.to_string()),
107 }
108 }
109}
110
111impl Serialize for RateLimitWindow {
112 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
113 serializer.serialize_str(self.as_str())
114 }
115}
116
117impl<'de> Deserialize<'de> for RateLimitWindow {
118 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
119 let s = String::deserialize(deserializer)?;
120 Ok(Self::from(s.as_str()))
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq, Hash)]
126pub enum OverageStatus {
127 Allowed,
129 AllowedWarning,
131 Rejected,
133 Unknown(String),
135}
136
137impl OverageStatus {
138 pub fn as_str(&self) -> &str {
139 match self {
140 Self::Allowed => "allowed",
141 Self::AllowedWarning => "allowed_warning",
142 Self::Rejected => "rejected",
143 Self::Unknown(s) => s.as_str(),
144 }
145 }
146}
147
148impl fmt::Display for OverageStatus {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(self.as_str())
151 }
152}
153
154impl From<&str> for OverageStatus {
155 fn from(s: &str) -> Self {
156 match s {
157 "allowed" => Self::Allowed,
158 "allowed_warning" => Self::AllowedWarning,
159 "rejected" => Self::Rejected,
160 other => Self::Unknown(other.to_string()),
161 }
162 }
163}
164
165impl Serialize for OverageStatus {
166 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
167 serializer.serialize_str(self.as_str())
168 }
169}
170
171impl<'de> Deserialize<'de> for OverageStatus {
172 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
173 let s = String::deserialize(deserializer)?;
174 Ok(Self::from(s.as_str()))
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Hash)]
181pub enum RateLimitScope {
182 Service,
183 Channel,
184 GroupPool,
186 Unknown(String),
188}
189
190impl RateLimitScope {
191 pub fn as_str(&self) -> &str {
192 match self {
193 Self::Service => "service",
194 Self::Channel => "channel",
195 Self::GroupPool => "group_pool",
196 Self::Unknown(s) => s.as_str(),
197 }
198 }
199}
200
201impl fmt::Display for RateLimitScope {
202 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203 f.write_str(self.as_str())
204 }
205}
206
207impl From<&str> for RateLimitScope {
208 fn from(s: &str) -> Self {
209 match s {
210 "service" => Self::Service,
211 "channel" => Self::Channel,
212 "group_pool" => Self::GroupPool,
213 other => Self::Unknown(other.to_string()),
214 }
215 }
216}
217
218impl Serialize for RateLimitScope {
219 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
220 serializer.serialize_str(self.as_str())
221 }
222}
223
224impl<'de> Deserialize<'de> for RateLimitScope {
225 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
226 let s = String::deserialize(deserializer)?;
227 Ok(Self::from(s.as_str()))
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash)]
233pub enum OverageDisabledReason {
234 OverageNotProvisioned,
236 OrgLevelDisabled,
238 OrgLevelDisabledUntil,
240 OutOfCredits,
242 SeatTierLevelDisabled,
244 MemberLevelDisabled,
246 SeatTierZeroCreditLimit,
248 GroupZeroCreditLimit,
250 MemberZeroCreditLimit,
252 OrgServiceLevelDisabled,
254 NoLimitsConfigured,
256 FetchError,
258 Unknown(String),
261}
262
263impl OverageDisabledReason {
264 pub fn as_str(&self) -> &str {
265 match self {
266 Self::OverageNotProvisioned => "overage_not_provisioned",
267 Self::OrgLevelDisabled => "org_level_disabled",
268 Self::OrgLevelDisabledUntil => "org_level_disabled_until",
269 Self::OutOfCredits => "out_of_credits",
270 Self::SeatTierLevelDisabled => "seat_tier_level_disabled",
271 Self::MemberLevelDisabled => "member_level_disabled",
272 Self::SeatTierZeroCreditLimit => "seat_tier_zero_credit_limit",
273 Self::GroupZeroCreditLimit => "group_zero_credit_limit",
274 Self::MemberZeroCreditLimit => "member_zero_credit_limit",
275 Self::OrgServiceLevelDisabled => "org_service_level_disabled",
276 Self::NoLimitsConfigured => "no_limits_configured",
277 Self::FetchError => "fetch_error",
278 Self::Unknown(s) => s.as_str(),
279 }
280 }
281}
282
283impl fmt::Display for OverageDisabledReason {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 f.write_str(self.as_str())
286 }
287}
288
289impl From<&str> for OverageDisabledReason {
290 fn from(s: &str) -> Self {
291 match s {
292 "overage_not_provisioned" => Self::OverageNotProvisioned,
293 "org_level_disabled" => Self::OrgLevelDisabled,
294 "org_level_disabled_until" => Self::OrgLevelDisabledUntil,
295 "out_of_credits" => Self::OutOfCredits,
296 "seat_tier_level_disabled" => Self::SeatTierLevelDisabled,
297 "member_level_disabled" => Self::MemberLevelDisabled,
298 "seat_tier_zero_credit_limit" => Self::SeatTierZeroCreditLimit,
299 "group_zero_credit_limit" => Self::GroupZeroCreditLimit,
300 "member_zero_credit_limit" => Self::MemberZeroCreditLimit,
301 "org_service_level_disabled" => Self::OrgServiceLevelDisabled,
302 "no_limits_configured" => Self::NoLimitsConfigured,
303 "fetch_error" => Self::FetchError,
304 other => Self::Unknown(other.to_string()),
305 }
306 }
307}
308
309impl Serialize for OverageDisabledReason {
310 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
311 serializer.serialize_str(self.as_str())
312 }
313}
314
315impl<'de> Deserialize<'de> for OverageDisabledReason {
316 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
317 let s = String::deserialize(deserializer)?;
318 Ok(Self::from(s.as_str()))
319 }
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct RateLimitEvent {
362 pub rate_limit_info: RateLimitInfo,
364 #[serde(alias = "sessionId")]
366 pub session_id: String,
367 #[serde(skip_serializing_if = "Option::is_none")]
369 pub uuid: Option<String>,
370}
371
372#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
374pub struct RateLimitInfo {
375 pub status: RateLimitStatus,
377 #[serde(rename = "resetsAt", skip_serializing_if = "Option::is_none")]
379 pub resets_at: Option<u64>,
380 #[serde(rename = "rateLimitType", skip_serializing_if = "Option::is_none")]
382 pub rate_limit_type: Option<RateLimitWindow>,
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub utilization: Option<f64>,
386 #[serde(rename = "unifiedWindows", skip_serializing_if = "Option::is_none")]
394 pub unified_windows: Option<UnifiedRateLimitWindows>,
395 #[serde(skip_serializing_if = "Option::is_none", rename = "overageStatus")]
397 pub overage_status: Option<OverageStatus>,
398 #[serde(rename = "overageResetsAt", skip_serializing_if = "Option::is_none")]
400 pub overage_resets_at: Option<u64>,
401 #[serde(
403 rename = "overageDisabledReason",
404 skip_serializing_if = "Option::is_none"
405 )]
406 pub overage_disabled_reason: Option<OverageDisabledReason>,
407 #[serde(rename = "isUsingOverage", skip_serializing_if = "Option::is_none")]
409 pub is_using_overage: Option<bool>,
410 #[serde(rename = "overageInUse", skip_serializing_if = "Option::is_none")]
412 pub overage_in_use: Option<bool>,
413 #[serde(rename = "surpassedThreshold", skip_serializing_if = "Option::is_none")]
415 pub surpassed_threshold: Option<f64>,
416 #[serde(
421 rename = "dailyCapPaused",
422 default,
423 skip_serializing_if = "Option::is_none"
424 )]
425 pub daily_cap_paused: Option<bool>,
426 #[serde(
432 rename = "rateLimitGraceActive",
433 skip_serializing_if = "Option::is_none"
434 )]
435 pub rate_limit_grace_active: Option<bool>,
436 #[serde(
438 rename = "overagePeriodMonthly",
439 skip_serializing_if = "Option::is_none"
440 )]
441 pub overage_period_monthly: Option<OveragePeriodUtilization>,
442 #[serde(
444 rename = "overagePeriodChannel",
445 skip_serializing_if = "Option::is_none"
446 )]
447 pub overage_period_channel: Option<OveragePeriodUtilization>,
448 #[serde(rename = "limitScope", skip_serializing_if = "Option::is_none")]
451 pub limit_scope: Option<RateLimitScope>,
452 #[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
454 pub error_code: Option<RateLimitErrorCode>,
455 #[serde(
457 rename = "canUserPurchaseCredits",
458 skip_serializing_if = "Option::is_none"
459 )]
460 pub can_user_purchase_credits: Option<bool>,
461 #[serde(
463 rename = "hasChargeableSavedPaymentMethod",
464 skip_serializing_if = "Option::is_none"
465 )]
466 pub has_chargeable_saved_payment_method: Option<bool>,
467 #[serde(rename = "usageCreditOffer", skip_serializing_if = "Option::is_none")]
471 pub usage_credit_offer: Option<UsageCreditOffer>,
472 #[serde(rename = "adminRequestOffer", skip_serializing_if = "Option::is_none")]
477 pub admin_request_offer: Option<AdminRequestOffer>,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
483pub struct AdminRequestOffer {
484 pub state: AdminRequestOfferState,
486 pub label: String,
488 #[serde(rename = "doneLabel")]
490 pub done_label: String,
491 pub confirm: bool,
493 #[serde(rename = "confirmPrompt")]
495 pub confirm_prompt: String,
496 #[serde(
498 rename = "lastFileFailed",
499 default,
500 skip_serializing_if = "Option::is_none"
501 )]
502 pub last_file_failed: Option<bool>,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
505 pub revision: Option<i64>,
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, Hash)]
510pub enum AdminRequestOfferState {
511 CanAsk,
513 Pending,
515 Sending,
517 Sent,
519 Unknown(String),
521}
522
523impl AdminRequestOfferState {
524 pub fn as_str(&self) -> &str {
525 match self {
526 Self::CanAsk => "can_ask",
527 Self::Pending => "pending",
528 Self::Sending => "sending",
529 Self::Sent => "sent",
530 Self::Unknown(s) => s.as_str(),
531 }
532 }
533}
534
535impl fmt::Display for AdminRequestOfferState {
536 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537 f.write_str(self.as_str())
538 }
539}
540
541impl From<&str> for AdminRequestOfferState {
542 fn from(s: &str) -> Self {
543 match s {
544 "can_ask" => Self::CanAsk,
545 "pending" => Self::Pending,
546 "sending" => Self::Sending,
547 "sent" => Self::Sent,
548 other => Self::Unknown(other.to_string()),
549 }
550 }
551}
552
553impl Serialize for AdminRequestOfferState {
554 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
555 serializer.serialize_str(self.as_str())
556 }
557}
558
559impl<'de> Deserialize<'de> for AdminRequestOfferState {
560 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
561 let s = String::deserialize(deserializer)?;
562 Ok(Self::from(s.as_str()))
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
569pub struct UsageCreditOffer {
570 pub kind: UsageCreditOfferKind,
572 #[serde(rename = "headlinePct")]
574 pub headline_pct: i64,
575 #[serde(rename = "daysLeft")]
577 pub days_left: i64,
578 pub label: String,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq, Hash)]
584pub enum UsageCreditOfferKind {
585 PctOff,
587 ExtraCredit,
589 Unknown(String),
591}
592
593impl UsageCreditOfferKind {
594 pub fn as_str(&self) -> &str {
595 match self {
596 Self::PctOff => "pct_off",
597 Self::ExtraCredit => "extra_credit",
598 Self::Unknown(s) => s.as_str(),
599 }
600 }
601}
602
603impl fmt::Display for UsageCreditOfferKind {
604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605 f.write_str(self.as_str())
606 }
607}
608
609impl From<&str> for UsageCreditOfferKind {
610 fn from(s: &str) -> Self {
611 match s {
612 "pct_off" => Self::PctOff,
613 "extra_credit" => Self::ExtraCredit,
614 other => Self::Unknown(other.to_string()),
615 }
616 }
617}
618
619impl Serialize for UsageCreditOfferKind {
620 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
621 serializer.serialize_str(self.as_str())
622 }
623}
624
625impl<'de> Deserialize<'de> for UsageCreditOfferKind {
626 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
627 let s = String::deserialize(deserializer)?;
628 Ok(Self::from(s.as_str()))
629 }
630}
631
632#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
634pub struct OveragePeriodUtilization {
635 pub utilization: f64,
637}
638
639#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
643pub struct UnifiedRateLimitWindows {
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub five_hour: Option<UnifiedWindowUsage>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub seven_day: Option<UnifiedWindowUsage>,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
653 pub seven_day_overage_included: Option<UnifiedWindowUsage>,
654}
655
656#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
659pub struct UnifiedWindowUsage {
660 pub utilization: f64,
663 #[serde(rename = "resetsAt")]
665 pub resets_at: u64,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
672 pub status: Option<RateLimitStatus>,
673 #[serde(
676 rename = "surpassedThreshold",
677 default,
678 skip_serializing_if = "Option::is_none"
679 )]
680 pub surpassed_threshold: Option<f64>,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Hash)]
685pub enum RateLimitErrorCode {
686 CreditsRequired,
688 Unknown(String),
690}
691
692impl RateLimitErrorCode {
693 pub fn as_str(&self) -> &str {
694 match self {
695 Self::CreditsRequired => "credits_required",
696 Self::Unknown(s) => s.as_str(),
697 }
698 }
699}
700
701impl fmt::Display for RateLimitErrorCode {
702 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
703 f.write_str(self.as_str())
704 }
705}
706
707impl From<&str> for RateLimitErrorCode {
708 fn from(s: &str) -> Self {
709 match s {
710 "credits_required" => Self::CreditsRequired,
711 other => Self::Unknown(other.to_string()),
712 }
713 }
714}
715
716impl Serialize for RateLimitErrorCode {
717 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
718 serializer.serialize_str(self.as_str())
719 }
720}
721
722impl<'de> Deserialize<'de> for RateLimitErrorCode {
723 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
724 let s = String::deserialize(deserializer)?;
725 Ok(Self::from(s.as_str()))
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::{
732 OverageDisabledReason, OveragePeriodUtilization, OverageStatus, RateLimitErrorCode,
733 RateLimitStatus, RateLimitWindow, UsageCreditOffer, UsageCreditOfferKind,
734 };
735 use crate::io::ClaudeOutput;
736
737 #[test]
738 fn test_deserialize_rate_limit_event() {
739 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1771390800,"rateLimitType":"five_hour","overageStatus":"rejected","overageDisabledReason":"org_level_disabled","isUsingOverage":false},"uuid":"76258cfb-0dc8-4d4b-8682-77082b59c03f","session_id":"1ae0af5b-89fa-4075-8156-d5d3702f6505"}"#;
740
741 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
742 assert!(output.is_rate_limit_event());
743 assert_eq!(output.message_type(), "rate_limit_event");
744 assert_eq!(
745 output.session_id(),
746 Some("1ae0af5b-89fa-4075-8156-d5d3702f6505")
747 );
748
749 let evt = output.as_rate_limit_event().unwrap();
750 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
751 assert_eq!(evt.rate_limit_info.resets_at, Some(1771390800));
752 assert_eq!(
753 evt.rate_limit_info.rate_limit_type,
754 Some(RateLimitWindow::FiveHour)
755 );
756 assert_eq!(evt.rate_limit_info.utilization, None);
757 assert_eq!(
758 evt.rate_limit_info.overage_status,
759 Some(OverageStatus::Rejected)
760 );
761 assert_eq!(
762 evt.rate_limit_info.overage_disabled_reason,
763 Some(OverageDisabledReason::OrgLevelDisabled)
764 );
765 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
766 assert_eq!(
767 evt.uuid,
768 Some("76258cfb-0dc8-4d4b-8682-77082b59c03f".to_string())
769 );
770 }
771
772 #[test]
773 fn test_deserialize_rate_limit_event_minimal() {
774 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed"},"session_id":"abc"}"#;
775
776 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
777 let evt = output.as_rate_limit_event().unwrap();
778 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
779 assert_eq!(evt.rate_limit_info.is_using_overage, None);
780 assert!(evt.uuid.is_none());
781 }
782
783 #[test]
784 fn test_deserialize_rate_limit_event_allowed_warning() {
785 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed_warning","resetsAt":1700000000,"rateLimitType":"five_hour","utilization":0.85,"isUsingOverage":false},"uuid":"550e8400-e29b-41d4-a716-446655440000","session_id":"test-session-id"}"#;
786
787 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
788 let evt = output.as_rate_limit_event().unwrap();
789 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::AllowedWarning);
790 assert_eq!(evt.rate_limit_info.utilization, Some(0.85));
791 assert_eq!(evt.rate_limit_info.overage_status, None);
792 assert_eq!(evt.rate_limit_info.overage_disabled_reason, None);
793 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
794 }
795
796 #[test]
797 fn test_deserialize_rate_limit_event_no_resets_at() {
798 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"allowed","isUsingOverage":false},"uuid":"4269273d-3b5e-40ae-9765-cb3c12284c44","session_id":"f9626cf7-4d88-4844-9bb1-cab96909fc7b"}"#;
799
800 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
801 let evt = output.as_rate_limit_event().unwrap();
802 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Allowed);
803 assert_eq!(evt.rate_limit_info.resets_at, None);
804 assert_eq!(evt.rate_limit_info.rate_limit_type, None);
805 assert_eq!(evt.rate_limit_info.is_using_overage, Some(false));
806 }
807
808 #[test]
809 fn test_deserialize_rate_limit_event_rejected() {
810 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","resetsAt":1700003600,"rateLimitType":"seven_day","isUsingOverage":false,"overageStatus":"rejected","overageDisabledReason":"out_of_credits"},"uuid":"660e8400-e29b-41d4-a716-446655440001","session_id":"test-session-id"}"#;
811
812 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
813 let evt = output.as_rate_limit_event().unwrap();
814 assert_eq!(evt.rate_limit_info.status, RateLimitStatus::Rejected);
815 assert_eq!(
816 evt.rate_limit_info.rate_limit_type,
817 Some(RateLimitWindow::SevenDay)
818 );
819 assert_eq!(
820 evt.rate_limit_info.overage_status,
821 Some(OverageStatus::Rejected)
822 );
823 assert_eq!(
824 evt.rate_limit_info.overage_disabled_reason,
825 Some(OverageDisabledReason::OutOfCredits)
826 );
827 }
828
829 #[test]
830 fn test_deserialize_rate_limit_event_full_2_1_205() {
831 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","resetsAt":1700003600,"rateLimitType":"seven_day_overage_included","utilization":0.97,"overageStatus":"allowed_warning","overageResetsAt":1700007200,"overageDisabledReason":"seat_tier_zero_credit_limit","isUsingOverage":true,"overageInUse":true,"surpassedThreshold":0.8,"overagePeriodMonthly":{"utilization":0.5},"overagePeriodChannel":{"utilization":0.25},"errorCode":"credits_required","canUserPurchaseCredits":true,"hasChargeableSavedPaymentMethod":false},"session_id":"abc"}"#;
832
833 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
834 let evt = output.as_rate_limit_event().unwrap();
835 let info = &evt.rate_limit_info;
836 assert_eq!(
837 info.rate_limit_type,
838 Some(RateLimitWindow::SevenDayOverageIncluded)
839 );
840 assert_eq!(info.overage_status, Some(OverageStatus::AllowedWarning));
841 assert_eq!(info.overage_resets_at, Some(1700007200));
842 assert_eq!(
843 info.overage_disabled_reason,
844 Some(OverageDisabledReason::SeatTierZeroCreditLimit)
845 );
846 assert_eq!(info.is_using_overage, Some(true));
847 assert_eq!(info.overage_in_use, Some(true));
848 assert_eq!(info.surpassed_threshold, Some(0.8));
849 assert_eq!(
850 info.overage_period_monthly,
851 Some(OveragePeriodUtilization { utilization: 0.5 })
852 );
853 assert_eq!(
854 info.overage_period_channel,
855 Some(OveragePeriodUtilization { utilization: 0.25 })
856 );
857 assert_eq!(info.error_code, Some(RateLimitErrorCode::CreditsRequired));
858 assert_eq!(info.can_user_purchase_credits, Some(true));
859 assert_eq!(info.has_chargeable_saved_payment_method, Some(false));
860
861 let round_trip = serde_json::to_value(output).unwrap();
862 let original: serde_json::Value = serde_json::from_str(json).unwrap();
863 assert_eq!(round_trip, original);
864 }
865
866 #[test]
867 fn test_rate_limit_usage_credit_offer_round_trip() {
868 let json = r#"{"type":"rate_limit_event","rate_limit_info":{"status":"rejected","errorCode":"credits_required","usageCreditOffer":{"kind":"pct_off","headlinePct":20,"daysLeft":3,"label":"20% off usage credits for 3 more days"}},"session_id":"abc"}"#;
869
870 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
871 let evt = output.as_rate_limit_event().unwrap();
872 assert_eq!(
873 evt.rate_limit_info.usage_credit_offer,
874 Some(UsageCreditOffer {
875 kind: UsageCreditOfferKind::PctOff,
876 headline_pct: 20,
877 days_left: 3,
878 label: "20% off usage credits for 3 more days".to_string(),
879 })
880 );
881
882 let round_trip = serde_json::to_value(output).unwrap();
883 let original: serde_json::Value = serde_json::from_str(json).unwrap();
884 assert_eq!(round_trip, original);
885 }
886
887 #[test]
888 fn test_usage_credit_offer_kind_round_trip() {
889 for s in ["pct_off", "extra_credit", "future_kind"] {
890 let kind: UsageCreditOfferKind = serde_json::from_value(serde_json::json!(s)).unwrap();
891 assert_eq!(kind.as_str(), s);
892 assert_eq!(serde_json::to_value(&kind).unwrap(), serde_json::json!(s));
893 }
894 assert_eq!(
895 UsageCreditOfferKind::from("future_kind"),
896 UsageCreditOfferKind::Unknown("future_kind".to_string())
897 );
898 }
899
900 #[test]
901 fn test_rate_limit_window_round_trip() {
902 for s in [
903 "five_hour",
904 "seven_day",
905 "seven_day_opus",
906 "seven_day_sonnet",
907 "seven_day_overage_included",
908 "overage",
909 "some_future_window",
910 ] {
911 assert_eq!(RateLimitWindow::from(s).as_str(), s);
912 }
913 }
914
915 #[test]
916 fn test_overage_disabled_reason_round_trip() {
917 for s in [
918 "overage_not_provisioned",
919 "org_level_disabled",
920 "org_level_disabled_until",
921 "out_of_credits",
922 "seat_tier_level_disabled",
923 "member_level_disabled",
924 "seat_tier_zero_credit_limit",
925 "group_zero_credit_limit",
926 "member_zero_credit_limit",
927 "org_service_level_disabled",
928 "no_limits_configured",
929 "fetch_error",
930 "unknown",
931 ] {
932 assert_eq!(OverageDisabledReason::from(s).as_str(), s);
933 }
934 }
935}