1use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12use time::OffsetDateTime;
13
14use crate::error::SailError;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum SailboxStatus {
22 Running,
24 Paused,
26 Sleeping,
28 Failed,
30 Terminated,
32 Other(String),
34}
35
36impl Default for SailboxStatus {
37 fn default() -> Self {
38 SailboxStatus::Other(String::new())
39 }
40}
41
42impl SailboxStatus {
43 pub fn as_str(&self) -> &str {
45 match self {
46 SailboxStatus::Running => "running",
47 SailboxStatus::Paused => "paused",
48 SailboxStatus::Sleeping => "sleeping",
49 SailboxStatus::Failed => "failed",
50 SailboxStatus::Terminated => "terminated",
51 SailboxStatus::Other(s) => s,
52 }
53 }
54}
55
56impl From<&str> for SailboxStatus {
57 fn from(s: &str) -> SailboxStatus {
58 match s {
59 "running" => SailboxStatus::Running,
60 "paused" => SailboxStatus::Paused,
61 "sleeping" => SailboxStatus::Sleeping,
62 "failed" => SailboxStatus::Failed,
63 "terminated" => SailboxStatus::Terminated,
64 other => SailboxStatus::Other(other.to_string()),
65 }
66 }
67}
68
69impl std::fmt::Display for SailboxStatus {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str(self.as_str())
72 }
73}
74
75impl Serialize for SailboxStatus {
76 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
77 serializer.serialize_str(self.as_str())
78 }
79}
80
81impl<'de> Deserialize<'de> for SailboxStatus {
82 fn deserialize<D: serde::Deserializer<'de>>(
83 deserializer: D,
84 ) -> Result<SailboxStatus, D::Error> {
85 Ok(SailboxStatus::from(
86 String::deserialize(deserializer)?.as_str(),
87 ))
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum SailboxStatusFilter {
96 Running,
98 Paused,
100 Sleeping,
102 Failed,
104 Terminated,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
110pub enum SailboxListOrder {
111 #[default]
113 NewestActive,
114 NewestCreated,
116}
117
118impl SailboxListOrder {
119 pub fn as_str(&self) -> &'static str {
121 match self {
122 SailboxListOrder::NewestActive => "newest_active",
123 SailboxListOrder::NewestCreated => "newest_created",
124 }
125 }
126
127 pub fn parse(raw: &str) -> Option<SailboxListOrder> {
129 match raw.trim() {
130 "newest_active" => Some(SailboxListOrder::NewestActive),
131 "newest_created" => Some(SailboxListOrder::NewestCreated),
132 _ => None,
133 }
134 }
135}
136
137impl std::str::FromStr for SailboxListOrder {
138 type Err = SailError;
139
140 fn from_str(s: &str) -> Result<Self, Self::Err> {
141 SailboxListOrder::parse(s).ok_or_else(|| SailError::InvalidArgument {
142 message: format!(
143 "unknown list order {s:?}; use \"newest_active\" or \"newest_created\""
144 ),
145 })
146 }
147}
148
149impl std::fmt::Display for SailboxListOrder {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.write_str(self.as_str())
152 }
153}
154
155impl SailboxStatusFilter {
156 pub fn as_str(&self) -> &'static str {
158 match self {
159 SailboxStatusFilter::Running => "running",
160 SailboxStatusFilter::Paused => "paused",
161 SailboxStatusFilter::Sleeping => "sleeping",
162 SailboxStatusFilter::Failed => "failed",
163 SailboxStatusFilter::Terminated => "terminated",
164 }
165 }
166
167 pub fn parse(s: &str) -> Option<SailboxStatusFilter> {
169 match s {
170 "running" => Some(SailboxStatusFilter::Running),
171 "paused" => Some(SailboxStatusFilter::Paused),
172 "sleeping" => Some(SailboxStatusFilter::Sleeping),
173 "failed" => Some(SailboxStatusFilter::Failed),
174 "terminated" => Some(SailboxStatusFilter::Terminated),
175 _ => None,
176 }
177 }
178}
179
180impl std::str::FromStr for SailboxStatusFilter {
181 type Err = SailError;
182
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 SailboxStatusFilter::parse(s).ok_or_else(|| SailError::InvalidArgument {
185 message: format!(
186 "unknown status filter {s:?}; use \"running\", \"paused\", \"sleeping\", \
187 \"failed\", or \"terminated\""
188 ),
189 })
190 }
191}
192
193impl std::fmt::Display for SailboxStatusFilter {
194 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195 f.write_str(self.as_str())
196 }
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
202pub enum ListenerProtocol {
203 Tcp,
205 Http,
207 Other(String),
209}
210
211impl ListenerProtocol {
212 pub fn as_str(&self) -> &str {
214 match self {
215 ListenerProtocol::Tcp => "tcp",
216 ListenerProtocol::Http => "http",
217 ListenerProtocol::Other(s) => s,
218 }
219 }
220}
221
222impl From<&str> for ListenerProtocol {
223 fn from(s: &str) -> ListenerProtocol {
224 match s {
225 "tcp" => ListenerProtocol::Tcp,
226 "http" => ListenerProtocol::Http,
227 other => ListenerProtocol::Other(other.to_string()),
228 }
229 }
230}
231
232impl std::fmt::Display for ListenerProtocol {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 f.write_str(self.as_str())
235 }
236}
237
238impl Serialize for ListenerProtocol {
239 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
240 serializer.serialize_str(self.as_str())
241 }
242}
243
244impl<'de> Deserialize<'de> for ListenerProtocol {
245 fn deserialize<D: serde::Deserializer<'de>>(
246 deserializer: D,
247 ) -> Result<ListenerProtocol, D::Error> {
248 Ok(ListenerProtocol::from(
249 String::deserialize(deserializer)?.as_str(),
250 ))
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Eq)]
257pub enum ListenerRouteStatus {
258 Unknown,
260 Pending,
262 Active,
264 Restoring,
266 Unavailable,
268 Other(String),
270}
271
272impl ListenerRouteStatus {
273 pub fn as_str(&self) -> &str {
277 match self {
278 ListenerRouteStatus::Unknown => "unknown",
279 ListenerRouteStatus::Pending => "pending",
280 ListenerRouteStatus::Active => "active",
281 ListenerRouteStatus::Restoring => "restoring",
282 ListenerRouteStatus::Unavailable => "unavailable",
283 ListenerRouteStatus::Other(s) => s,
284 }
285 }
286}
287
288impl From<&str> for ListenerRouteStatus {
289 fn from(s: &str) -> ListenerRouteStatus {
295 match s {
296 "LISTENER_ROUTE_STATUS_UNSPECIFIED" | "unknown" => ListenerRouteStatus::Unknown,
297 "LISTENER_ROUTE_STATUS_PENDING" | "pending" => ListenerRouteStatus::Pending,
298 "LISTENER_ROUTE_STATUS_ACTIVE" | "active" => ListenerRouteStatus::Active,
299 "LISTENER_ROUTE_STATUS_RESTORING" | "restoring" => ListenerRouteStatus::Restoring,
300 "LISTENER_ROUTE_STATUS_UNAVAILABLE" | "unavailable" => ListenerRouteStatus::Unavailable,
301 other => ListenerRouteStatus::Other(
302 other
303 .strip_prefix("LISTENER_ROUTE_STATUS_")
304 .map_or_else(|| other.to_string(), str::to_ascii_lowercase),
305 ),
306 }
307 }
308}
309
310impl std::fmt::Display for ListenerRouteStatus {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.write_str(self.as_str())
313 }
314}
315
316impl Serialize for ListenerRouteStatus {
317 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
318 serializer.serialize_str(self.as_str())
319 }
320}
321
322impl<'de> Deserialize<'de> for ListenerRouteStatus {
323 fn deserialize<D: serde::Deserializer<'de>>(
324 deserializer: D,
325 ) -> Result<ListenerRouteStatus, D::Error> {
326 Ok(ListenerRouteStatus::from(
327 String::deserialize(deserializer)?.as_str(),
328 ))
329 }
330}
331
332#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum IngressProtocol {
337 Tcp,
339 Http,
341}
342
343impl IngressProtocol {
344 pub fn as_str(&self) -> &'static str {
346 match self {
347 IngressProtocol::Tcp => "tcp",
348 IngressProtocol::Http => "http",
349 }
350 }
351
352 pub fn parse(s: &str) -> Option<IngressProtocol> {
355 match s {
356 "tcp" => Some(IngressProtocol::Tcp),
357 "http" => Some(IngressProtocol::Http),
358 _ => None,
359 }
360 }
361}
362
363impl std::str::FromStr for IngressProtocol {
364 type Err = SailError;
365
366 fn from_str(s: &str) -> Result<Self, Self::Err> {
367 IngressProtocol::parse(s).ok_or_else(|| SailError::InvalidArgument {
368 message: format!("unknown ingress protocol {s:?}; use \"tcp\" or \"http\""),
369 })
370 }
371}
372
373impl std::fmt::Display for IngressProtocol {
374 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375 f.write_str(self.as_str())
376 }
377}
378
379impl Serialize for IngressProtocol {
380 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
381 serializer.serialize_str(self.as_str())
382 }
383}
384
385#[derive(Debug, Clone, Default)]
387pub struct CheckpointOptions {
388 pub name: Option<String>,
390 pub ttl: Option<std::time::Duration>,
392}
393
394#[derive(Debug, Clone)]
398pub struct WaitForListenerOptions {
399 pub timeout: std::time::Duration,
402}
403
404impl Default for WaitForListenerOptions {
405 fn default() -> WaitForListenerOptions {
406 WaitForListenerOptions {
407 timeout: std::time::Duration::from_mins(1),
408 }
409 }
410}
411
412#[derive(Debug, Clone, Default)]
414pub struct ForkOptions {
415 pub name: Option<String>,
417 pub timeout: Option<std::time::Duration>,
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
427pub enum SailboxSize {
428 Small,
431 Medium,
433}
434
435impl SailboxSize {
436 pub fn as_str(&self) -> &'static str {
438 match self {
439 SailboxSize::Small => "s",
440 SailboxSize::Medium => "m",
441 }
442 }
443
444 pub fn parse(s: &str) -> Option<SailboxSize> {
446 match s {
447 "s" => Some(SailboxSize::Small),
448 "m" => Some(SailboxSize::Medium),
449 _ => None,
450 }
451 }
452
453 pub fn allowed() -> &'static [&'static str] {
455 &["s", "m"]
456 }
457}
458
459impl std::str::FromStr for SailboxSize {
460 type Err = SailError;
461
462 fn from_str(s: &str) -> Result<Self, Self::Err> {
463 SailboxSize::parse(s).ok_or_else(|| SailError::InvalidArgument {
464 message: format!("unknown size {s:?}; use \"s\" or \"m\""),
465 })
466 }
467}
468
469impl std::fmt::Display for SailboxSize {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 f.write_str(self.as_str())
472 }
473}
474
475impl Serialize for SailboxSize {
476 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
477 serializer.serialize_str(self.as_str())
478 }
479}
480
481#[derive(Debug, Clone, Default, Serialize)]
484#[non_exhaustive]
485pub struct SailboxHandle {
486 pub sailbox_id: String,
488 pub name: String,
490 pub status: SailboxStatus,
492 #[doc(hidden)]
494 pub worker_address: String,
495 #[doc(hidden)]
497 pub exec_endpoint: String,
498}
499
500#[derive(Debug, Clone, Serialize, Deserialize)]
502#[non_exhaustive]
503pub struct SailboxDeprecation {
504 pub deadline: String,
507 pub message: String,
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
514#[non_exhaustive]
515pub struct SailboxInfo {
516 pub sailbox_id: String,
518 pub app_id: String,
520 pub app_name: String,
522 pub image_id: String,
524 pub name: String,
526 pub status: SailboxStatus,
528 pub memory_mib: i64,
530 pub vcpu_count: i64,
532 pub state_disk_size_gib: i64,
534 pub cpu_requested_vcpu: i64,
536 pub cpu_used_vcpu: f64,
538 pub memory_requested_bytes: i64,
540 pub memory_used_bytes: i64,
542 pub disk_requested_bytes: i64,
544 pub disk_used_bytes: i64,
546 #[serde(default)]
549 pub architecture: String,
550 #[serde(default)]
552 pub guest_schema_version: Option<i64>,
553 #[serde(default)]
555 pub deprecation: Option<SailboxDeprecation>,
556 #[serde(default)]
558 pub error_message: Option<String>,
559 pub checkpoint_generation: i64,
561 #[serde(with = "time::serde::rfc3339::option", default)]
563 pub started_at: Option<OffsetDateTime>,
564 #[serde(with = "time::serde::rfc3339::option", default)]
566 pub last_checkpointed_at: Option<OffsetDateTime>,
567 #[serde(with = "time::serde::rfc3339")]
569 pub created_at: OffsetDateTime,
570 #[serde(with = "time::serde::rfc3339")]
572 pub updated_at: OffsetDateTime,
573 #[serde(default)]
576 pub created_by_user_id: Option<String>,
577 #[serde(default)]
580 pub visibility: Option<String>,
581}
582
583#[doc(hidden)]
585#[derive(Debug, Clone, Deserialize)]
586#[non_exhaustive]
587pub struct WhoAmI {
588 pub org_id: String,
590 #[serde(default)]
592 pub user_id: Option<String>,
593}
594
595#[derive(Debug, Clone, Serialize)]
597#[non_exhaustive]
598pub struct SailboxPage {
599 pub items: Vec<SailboxInfo>,
601 pub limit: i64,
603 pub offset: i64,
605 pub total: i64,
607 pub has_more: bool,
609}
610
611#[derive(Debug, Clone, Default)]
613pub struct SailboxSpendQuery {
614 pub app_id: Option<String>,
616 pub sailbox_id: Option<String>,
618 pub from: Option<OffsetDateTime>,
621 pub to: Option<OffsetDateTime>,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize)]
628#[non_exhaustive]
629pub struct SailboxSpendResponse {
630 #[serde(with = "time::serde::rfc3339")]
632 pub start_at: OffsetDateTime,
633 #[serde(with = "time::serde::rfc3339")]
635 pub end_at: OffsetDateTime,
636 pub finalized_cost_usd_nanos: i64,
638 pub estimated_active_cost_usd_nanos: i64,
640 pub estimated_total_cost_usd_nanos: i64,
642 pub duration_seconds: i64,
644 pub vcpu_seconds: f64,
646 pub memory_gib_seconds: f64,
648 pub state_disk_gib_seconds: f64,
650 pub pricing_configured: bool,
652 pub rates: SailboxSpendRates,
654 pub sailboxes: Vec<SailboxSpendItem>,
656}
657
658#[derive(Debug, Clone, Default, Serialize, Deserialize)]
660#[non_exhaustive]
661pub struct SailboxSpendRates {
662 pub vcpu_second_usd_nanos: i64,
664 pub memory_gib_second_usd_nanos: i64,
666 pub state_disk_gib_second_usd_nanos: i64,
668 pub creation_usd_nanos: i64,
670 #[serde(default)]
672 pub amd64_state_disk_gib_second_usd_nanos: i64,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize)]
677#[non_exhaustive]
678pub struct SailboxSpendItem {
679 pub sailbox_id: String,
681 pub app_id: String,
683 pub finalized_cost_usd_nanos: i64,
685 pub estimated_active_cost_usd_nanos: i64,
687 pub estimated_total_cost_usd_nanos: i64,
689 pub duration_seconds: i64,
691 pub vcpu_seconds: f64,
693 pub memory_gib_seconds: f64,
695 pub state_disk_gib_seconds: f64,
697 pub active: bool,
699}
700
701#[derive(Debug, Clone)]
703pub struct SailboxMetricsQuery {
704 pub range: String,
706}
707
708impl Default for SailboxMetricsQuery {
709 fn default() -> SailboxMetricsQuery {
710 SailboxMetricsQuery {
711 range: "24h".to_string(),
712 }
713 }
714}
715
716#[derive(Debug, Clone, Serialize, Deserialize)]
718#[non_exhaustive]
719pub struct SailboxMetricsResponse {
720 pub range: String,
722 pub data: Vec<SailboxMetricPoint>,
724}
725
726#[derive(Debug, Clone, Serialize, Deserialize)]
728#[non_exhaustive]
729pub struct SailboxMetricPoint {
730 #[serde(with = "time::serde::rfc3339")]
732 pub timestamp: OffsetDateTime,
733 pub cpu_used_vcpu: f64,
735 pub cpu_requested_vcpu: i64,
737 pub memory_used_bytes: i64,
739 pub memory_requested_bytes: i64,
741 pub disk_used_bytes: i64,
743 pub disk_requested_bytes: i64,
745}
746
747#[derive(Debug, Clone, Serialize)]
751#[non_exhaustive]
752pub struct SailboxCheckpoint {
753 pub checkpoint_id: String,
755 pub sailbox_id: String,
757 pub checkpoint_generation: i64,
759 #[serde(with = "time::serde::rfc3339::option")]
763 pub expires_at: Option<OffsetDateTime>,
764 pub status: SailboxStatus,
766}
767
768#[derive(Debug, Clone)]
770pub struct ListSailboxesQuery {
771 pub app_id: Option<String>,
773 pub status: Option<SailboxStatusFilter>,
775 pub search: Option<String>,
777 pub order: SailboxListOrder,
779 pub limit: i64,
781 pub offset: i64,
783}
784
785#[doc(hidden)]
788pub const DEFAULT_LIST_LIMIT: i64 = 50;
789
790impl Default for ListSailboxesQuery {
791 fn default() -> ListSailboxesQuery {
792 ListSailboxesQuery {
793 app_id: None,
794 status: None,
795 search: None,
796 order: SailboxListOrder::NewestActive,
797 limit: DEFAULT_LIST_LIMIT,
798 offset: 0,
799 }
800 }
801}
802
803#[derive(Debug, Clone, Serialize)]
805pub struct IngressPort {
806 pub guest_port: u32,
808 pub protocol: IngressProtocol,
810 pub allowlist: Vec<String>,
814}
815
816#[derive(Debug, Clone, Serialize)]
818pub struct VolumeMount {
819 pub volume_id: String,
821 pub mount_path: String,
823}
824
825#[derive(Debug, Clone, Serialize, Deserialize)]
828#[non_exhaustive]
829pub struct VolumeInfo {
830 pub volume_id: String,
832 pub name: String,
834 pub backend: String,
836 pub status: String,
838 #[serde(with = "time::serde::rfc3339::option", default)]
840 pub created_at: Option<OffsetDateTime>,
841 #[serde(with = "time::serde::rfc3339::option", default)]
843 pub updated_at: Option<OffsetDateTime>,
844}
845
846#[derive(Debug, Clone, Deserialize)]
851pub(crate) struct AddListenerWire {
852 pub(crate) guest_port: u32,
853 pub(crate) protocol: ListenerProtocol,
854 #[serde(default)]
855 pub(crate) public_url: String,
856 #[serde(default, rename = "tcp_public_host")]
857 pub(crate) public_host: String,
858 #[serde(default, rename = "tcp_public_port")]
859 pub(crate) public_port: u32,
860}
861
862impl From<AddListenerWire> for crate::worker::Listener {
863 fn from(wire: AddListenerWire) -> crate::worker::Listener {
864 crate::worker::Listener {
865 guest_port: wire.guest_port,
866 protocol: wire.protocol,
867 route_status: ListenerRouteStatus::Unknown,
870 public_url: wire.public_url,
871 public_host: wire.public_host,
872 public_port: wire.public_port,
873 }
874 }
875}
876
877#[derive(Debug, Clone)]
880#[non_exhaustive]
881pub struct IssuedUserCert {
882 pub certificate: String,
884 pub key_id: String,
886}
887
888#[derive(Debug, Clone, PartialEq, Eq)]
891pub enum ListenerEndpoint {
892 Http {
894 url: String,
896 },
897 Tcp {
899 host: String,
901 port: u32,
903 },
904}
905
906impl std::fmt::Display for ListenerEndpoint {
907 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
908 match self {
909 ListenerEndpoint::Http { url } => f.write_str(url),
910 ListenerEndpoint::Tcp { host, port } => write!(f, "{host}:{port}"),
911 }
912 }
913}
914
915#[derive(Debug, Clone)]
921pub struct CreateSailboxRequest {
922 pub app_id: String,
924 pub name: String,
926 pub ingress_ports: Vec<IngressPort>,
928 pub volume_mounts: Vec<VolumeMount>,
930 pub image: crate::image::ImageSpec,
933 pub size: Option<SailboxSize>,
935 pub memory_gib: Option<u32>,
938 pub disk_gib: Option<u32>,
941 pub ssh: bool,
945 pub private: bool,
952 pub image_build_timeout: Option<Duration>,
955}
956
957impl Default for CreateSailboxRequest {
958 fn default() -> CreateSailboxRequest {
961 CreateSailboxRequest {
962 app_id: String::new(),
963 name: String::new(),
964 ingress_ports: Vec::new(),
965 volume_mounts: Vec::new(),
966 image: crate::image::ImageSpec {
967 base: Some(crate::image::BaseImage::Debian),
968 ..crate::image::ImageSpec::default()
969 },
970 size: None,
971 memory_gib: None,
972 disk_gib: None,
973 ssh: false,
974 private: false,
975 image_build_timeout: None,
976 }
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983
984 #[test]
985 fn closed_enums_round_trip_through_fromstr_and_display() {
986 for (text, value) in [
987 ("newest_active", SailboxListOrder::NewestActive),
988 ("newest_created", SailboxListOrder::NewestCreated),
989 ] {
990 assert_eq!(text.parse::<SailboxListOrder>().unwrap(), value);
991 assert_eq!(value.to_string(), text);
992 }
993 for (text, value) in [
994 ("running", SailboxStatusFilter::Running),
995 ("terminated", SailboxStatusFilter::Terminated),
996 ] {
997 assert_eq!(text.parse::<SailboxStatusFilter>().unwrap(), value);
998 assert_eq!(value.to_string(), text);
999 }
1000 for (text, value) in [
1001 ("tcp", IngressProtocol::Tcp),
1002 ("http", IngressProtocol::Http),
1003 ] {
1004 assert_eq!(text.parse::<IngressProtocol>().unwrap(), value);
1005 assert_eq!(value.to_string(), text);
1006 }
1007 for (text, value) in [("s", SailboxSize::Small), ("m", SailboxSize::Medium)] {
1008 assert_eq!(text.parse::<SailboxSize>().unwrap(), value);
1009 assert_eq!(value.to_string(), text);
1010 }
1011 let err = "udp".parse::<IngressProtocol>().unwrap_err();
1012 assert!(matches!(err, SailError::InvalidArgument { .. }));
1013 assert!(err.to_string().contains("udp"));
1014 }
1015
1016 #[test]
1017 fn list_query_default_uses_a_valid_nonzero_limit() {
1018 let query = ListSailboxesQuery::default();
1021 assert_eq!(query.limit, DEFAULT_LIST_LIMIT);
1022 assert!(query.limit > 0);
1023 assert_eq!(query.offset, 0);
1024 }
1025
1026 #[test]
1027 fn route_status_round_trips_through_serde() {
1028 for status in [
1032 ListenerRouteStatus::Unknown,
1033 ListenerRouteStatus::Pending,
1034 ListenerRouteStatus::Active,
1035 ListenerRouteStatus::Restoring,
1036 ListenerRouteStatus::Unavailable,
1037 ListenerRouteStatus::Other("degraded".to_string()),
1038 ] {
1039 let json = serde_json::to_string(&status).unwrap();
1040 let back: ListenerRouteStatus = serde_json::from_str(&json).unwrap();
1041 assert_eq!(
1042 status, back,
1043 "round-trip failed for {status:?} (json {json})"
1044 );
1045 }
1046 assert_eq!(
1048 ListenerRouteStatus::from("LISTENER_ROUTE_STATUS_ACTIVE"),
1049 ListenerRouteStatus::Active,
1050 );
1051 }
1052
1053 #[test]
1054 fn info_deserializes_a_deprecation_notice() {
1055 let value = serde_json::json!({
1056 "sailbox_id": "sb-1", "app_id": "app-1", "app_name": "a", "name": "n",
1057 "image_id": "img-1",
1058 "status": "running", "memory_mib": 2048, "vcpu_count": 4,
1059 "state_disk_size_gib": 10,
1060 "cpu_requested_vcpu": 2, "cpu_used_vcpu": 1.5,
1061 "memory_requested_bytes": 1024, "memory_used_bytes": 512,
1062 "disk_requested_bytes": 4096, "disk_used_bytes": 2048,
1063 "architecture": "amd64", "checkpoint_generation": 7,
1064 "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z",
1065 "deprecation": {
1066 "deadline": "2026-08-01",
1067 "message": "Upgrade this sailbox before 2026-08-01."
1068 }
1069 });
1070 let info: SailboxInfo = serde_json::from_value(value).unwrap();
1071 let notice = info.deprecation.unwrap();
1072 assert_eq!(notice.deadline, "2026-08-01");
1073 assert!(notice.message.starts_with("Upgrade this sailbox"));
1074 }
1075}