1use serde::Serialize;
11
12use crate::client::GatewayApi;
13use crate::client::metrics::{CurrentGauges, PerformanceCharts, ThreadCounts};
14use crate::client::query::ListQuery;
15use crate::client::status::{DiskInfo, JavaInfo, ModuleInfo, OsInfo, OverviewLicense};
16use crate::client::version::LicenseInfo;
17use crate::error::CoreError;
18
19#[derive(Debug, Serialize)]
27pub struct StatusResult {
28 pub gateway: StatusGateway,
30 pub state: String,
33 pub overview: StatusOverview,
35}
36
37#[derive(Debug, Serialize)]
39pub struct StatusGateway {
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub name: Option<String>,
43 pub ignition_version: String,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub edition: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub license: Option<LicenseInfo>,
51}
52
53#[derive(Debug, Serialize)]
56pub struct StatusOverview {
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub java: Option<JavaInfo>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub os: Option<OsInfo>,
63 pub uptime_ms: i64,
65 #[serde(skip_serializing_if = "Vec::is_empty")]
67 pub memory: Vec<i64>,
68 pub cpu_fraction: f64,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub disk: Option<DiskInfo>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub license: Option<StatusLicense>,
78}
79
80#[derive(Debug, Serialize)]
83pub struct StatusLicense {
84 pub state: String,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub trial_remaining_s: Option<i64>,
89}
90
91#[derive(Debug, Serialize)]
93pub struct ModulesResult {
94 pub items: Vec<ModuleInfo>,
96 pub quarantined: bool,
98}
99
100#[derive(Debug, Serialize)]
103pub struct MetricsResult {
104 pub current: CurrentGauges,
106 pub threads: ThreadCounts,
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub history: Option<PerformanceCharts>,
111}
112
113pub async fn status(api: &dyn GatewayApi) -> Result<StatusResult, CoreError> {
117 let info = api.gateway_info().await?;
118 let overview = api.overview().await?;
119 let ping = api.status_ping().await?;
120 Ok(StatusResult {
121 gateway: StatusGateway {
122 name: info.name,
123 ignition_version: info.ignition_version,
124 edition: info.edition,
125 license: info.license,
126 },
127 state: ping.state,
128 overview: StatusOverview {
129 java: overview.java,
130 os: overview.os,
131 uptime_ms: overview.uptime,
132 memory: overview.memory,
133 cpu_fraction: overview.cpu,
134 disk: overview.disk,
135 license: overview.license.map(
136 |OverviewLicense {
137 state,
138 trial_remaining_s,
139 ..
140 }| {
141 StatusLicense {
142 state,
143 trial_remaining_s,
144 }
145 },
146 ),
147 },
148 })
149}
150
151pub async fn modules(api: &dyn GatewayApi, quarantined: bool) -> Result<ModulesResult, CoreError> {
154 let page = api.modules(quarantined, &ListQuery::default()).await?;
155 Ok(ModulesResult {
156 items: page.items,
157 quarantined,
158 })
159}
160
161pub async fn metrics(
164 api: &dyn GatewayApi,
165 include_history: bool,
166) -> Result<MetricsResult, CoreError> {
167 let current = api.metrics_current().await?;
168 let threads = api.metrics_threads().await?;
169 let history = if include_history {
170 Some(api.metrics_historic().await?)
171 } else {
172 None
173 };
174 Ok(MetricsResult {
175 current,
176 threads,
177 history,
178 })
179}
180
181#[cfg(test)]
182mod tests {
183 use super::{MetricsResult, ModulesResult, StatusResult, metrics, modules, status};
184 use crate::client::GatewayApi;
185 use crate::client::metrics::{CurrentGauges, Datapoint, PerformanceCharts, ThreadCounts};
186 use crate::client::query::{ListEnvelope, ListMetadata};
187 use crate::client::status::{ModuleInfo, Overview, StatusPing};
188 use crate::client::version::{GatewayInfo, LicenseInfo};
189 use crate::error::CoreError;
190
191 use std::sync::Mutex;
192
193 struct HealthyRig {
197 modules_flags: Mutex<Vec<bool>>,
198 }
199
200 fn overview_fixture() -> Overview {
201 serde_json::from_value(serde_json::json!({
202 "version": "8.3.6 (b2026042713)",
203 "java": {"version": "17.0.11", "vendor": "Azul Systems, Inc.", "name": "OpenJDK 64-Bit Server VM"},
204 "os": {"name": "Linux", "arch": "amd64", "version": "5.15.0"},
205 "uptime": 338137,
206 "memory": [338137088i64, 1073741824i64],
207 "cpu": 0.0031,
208 "disk": {"total": 62661259264i64, "used": 12272824320i64},
209 "license": {"state": "trial", "trialRemaining": 7017}
210 }))
211 .expect("fixture overview parses")
212 }
213
214 fn gauges_fixture() -> CurrentGauges {
215 serde_json::from_value(serde_json::json!({
216 "cpu": 4.88, "heapMemory": 240000000i64, "maxMemory": 1073741824i64
217 }))
218 .expect("fixture gauges parse")
219 }
220
221 fn charts_fixture() -> PerformanceCharts {
222 PerformanceCharts {
223 cpu_datapoints: vec![Datapoint {
224 hist_id: 1,
225 timestamp: 1787346747022,
226 value: 4.88,
227 }],
228 heap_memory_datapoints: vec![Datapoint {
229 hist_id: 2,
230 timestamp: 1787346747022,
231 value: 240000000.0,
232 }],
233 non_heap_memory_datapoints: Vec::new(),
234 }
235 }
236
237 #[async_trait::async_trait]
238 impl GatewayApi for HealthyRig {
239 async fn tag_provider_list(
240 &self,
241 _query: &crate::client::query::ListQuery,
242 ) -> Result<
243 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
244 CoreError,
245 > {
246 unreachable!("not part of this action")
247 }
248 async fn tag_provider_find(
249 &self,
250 _name: &str,
251 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
252 unreachable!("not part of this action")
253 }
254 async fn tag_provider_create(
255 &self,
256 _body: &[crate::client::tags::TagProviderCreate],
257 ) -> Result<(), CoreError> {
258 unreachable!("not part of this action")
259 }
260 async fn tag_provider_delete(
261 &self,
262 _name: &str,
263 _signature: &str,
264 ) -> Result<(), CoreError> {
265 unreachable!("not part of this action")
266 }
267 async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
268 unreachable!("not part of this action")
269 }
270 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
271 unreachable!("not part of this action")
272 }
273 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
274 unreachable!("not part of this action")
275 }
276 async fn backup_download(
277 &self,
278 _out: &std::path::Path,
279 _backup_type: crate::client::backup::BackupType,
280 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
281 unreachable!("not part of this action")
282 }
283 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
284 unreachable!("not part of this action")
285 }
286 async fn eam_task_history(
287 &self,
288 _limit: Option<u32>,
289 _search: Option<&str>,
290 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
291 {
292 unreachable!("not part of this action")
293 }
294 async fn eam_task_definitions(
295 &self,
296 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
297 {
298 unreachable!("not part of this action")
299 }
300 async fn eam_task_find(
301 &self,
302 _name: &str,
303 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
304 unreachable!("not part of this action")
305 }
306 async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
307 unreachable!("not part of this action")
308 }
309 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
310 unreachable!("not part of this action")
311 }
312 async fn gateway_info(&self) -> Result<GatewayInfo, CoreError> {
313 Ok(GatewayInfo {
314 name: Some("ign-mock".into()),
315 redundancy_role: Some("Independent".into()),
316 edition: Some("standard".into()),
317 ignition_version: "8.3.6 (b2026042713)".into(),
318 jvm_version: Some("17.0.11".into()),
319 license: Some(LicenseInfo {
320 mode: "Trial".into(),
321 expiration_date: Some("2026-08-24T19:00:00Z".into()),
322 }),
323 endpoint: None,
324 })
325 }
326 async fn overview(&self) -> Result<Overview, CoreError> {
327 Ok(overview_fixture())
328 }
329 async fn status_ping(&self) -> Result<StatusPing, CoreError> {
330 Ok(StatusPing {
331 state: "RUNNING".into(),
332 })
333 }
334 async fn modules(
335 &self,
336 quarantined: bool,
337 _query: &crate::client::query::ListQuery,
338 ) -> Result<ListEnvelope<ModuleInfo>, CoreError> {
339 self.modules_flags
340 .lock()
341 .expect("flags lock")
342 .push(quarantined);
343 let item = ModuleInfo {
344 id: "com.inductiveautomation.perspective".into(),
345 name: "Perspective".into(),
346 version: "8.3.6".into(),
347 state: Some("ACTIVE".into()),
348 license_state: Some("ACTIVATED".into()),
349 vendor_name: Some("Inductive Automation".into()),
350 startup_time: Some("2026-08-21 22:03:29".into()),
351 extra: Default::default(),
352 };
353 Ok(ListEnvelope {
354 items: vec![item],
355 metadata: ListMetadata {
356 total: 1,
357 matching: 1,
358 limit: -1,
359 offset: 0,
360 },
361 })
362 }
363 async fn metrics_current(&self) -> Result<CurrentGauges, CoreError> {
364 Ok(gauges_fixture())
365 }
366 async fn metrics_historic(&self) -> Result<PerformanceCharts, CoreError> {
367 Ok(charts_fixture())
368 }
369 async fn metrics_threads(&self) -> Result<ThreadCounts, CoreError> {
370 Ok(ThreadCounts {
371 running: 32,
372 waiting: 39,
373 timed_waiting: 51,
374 blocked: 0,
375 extra: Default::default(),
376 })
377 }
378 async fn designers(
379 &self,
380 _query: &crate::client::query::ListQuery,
381 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
382 unreachable!("not part of this double's actions")
383 }
384 async fn perspective_sessions(
385 &self,
386 _query: &crate::client::query::ListQuery,
387 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
388 unreachable!("not part of this double's actions")
389 }
390 async fn vision_clients(
391 &self,
392 _query: &crate::client::query::ListQuery,
393 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
394 unreachable!("not part of this double's actions")
395 }
396 async fn terminate_perspective_session(
397 &self,
398 _id: &str,
399 _message: Option<&str>,
400 ) -> Result<(), CoreError> {
401 unreachable!("not part of this double's actions")
402 }
403 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
404 unreachable!("not part of this double's actions")
405 }
406 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
407 unreachable!("not part of this double's actions")
408 }
409 async fn database_connections(
410 &self,
411 ) -> Result<
412 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
413 CoreError,
414 > {
415 unreachable!("not part of this double's actions")
416 }
417 async fn opc_connections(
418 &self,
419 ) -> Result<
420 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
421 CoreError,
422 > {
423 unreachable!("not part of this double's actions")
424 }
425
426 async fn logs(
427 &self,
428 _filter: &crate::client::logs::LogQuery,
429 ) -> Result<ListEnvelope<crate::client::logs::LogEntry>, CoreError> {
430 unreachable!("not part of this double's actions")
431 }
432 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
433 unreachable!("not part of this double's actions")
434 }
435 async fn loggers(
436 &self,
437 _query: &crate::client::query::ListQuery,
438 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
439 unreachable!("not part of this double's actions")
440 }
441 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
442 unreachable!("not part of this double's actions")
443 }
444 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
445 unreachable!("not part of this double's actions")
446 }
447 async fn restart(&self) -> Result<(), CoreError> {
448 unreachable!("not part of this action")
449 }
450 async fn scan_projects(&self) -> Result<(), CoreError> {
451 unreachable!("not part of this action")
452 }
453 async fn security_properties(
454 &self,
455 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
456 unreachable!("not part of this action")
457 }
458 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
459 unreachable!("not part of this action")
460 }
461 async fn webdev_route_call(
462 &self,
463 _project: &str,
464 _route: &str,
465 _body: &serde_json::Value,
466 _extra_headers: &[(&str, &str)],
467 ) -> Result<serde_json::Value, CoreError> {
468 unreachable!("not part of this action")
469 }
470 async fn webdev_route_probe(
471 &self,
472 _project: &str,
473 _route: &str,
474 _extra_headers: &[(&str, &str)],
475 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
476 unreachable!("not part of this action")
477 }
478 async fn projects(
479 &self,
480 _query: &crate::client::query::ListQuery,
481 ) -> Result<
482 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
483 CoreError,
484 > {
485 unreachable!("not part of this action")
486 }
487 async fn project_find(
488 &self,
489 _name: &str,
490 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
491 unreachable!("not part of this action")
492 }
493 async fn project_create(
494 &self,
495 _body: &crate::client::projects::ProjectCreate,
496 ) -> Result<(), CoreError> {
497 unreachable!("not part of this action")
498 }
499 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
500 unreachable!("not part of this action")
501 }
502 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
503 unreachable!("not part of this action")
504 }
505 async fn project_modify(
506 &self,
507 _name: &str,
508 _body: &crate::client::projects::ProjectModify,
509 ) -> Result<(), CoreError> {
510 unreachable!("not part of this action")
511 }
512 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
513 unreachable!("not part of this action")
514 }
515 async fn project_export_to_file(
516 &self,
517 _name: &str,
518 _out: &std::path::Path,
519 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
520 unreachable!("not part of this action")
521 }
522 async fn project_import(
523 &self,
524 _name: &str,
525 _zip: Vec<u8>,
526 _overwrite: bool,
527 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
528 unreachable!("not part of this action")
529 }
530 }
531
532 #[tokio::test]
535 async fn status_merges_gateway_overview_and_ping() {
536 let rig = HealthyRig {
537 modules_flags: Mutex::new(Vec::new()),
538 };
539 let result: StatusResult = status(&rig).await.expect("healthy rig merges");
540
541 let json = serde_json::to_string(&result).expect("serialize");
545 assert_eq!(
546 json,
547 concat!(
548 r#"{"gateway":{"name":"ign-mock","ignition_version":"8.3.6 (b2026042713)","edition":"standard","#,
549 r#""license":{"mode":"Trial","expirationDate":"2026-08-24T19:00:00Z"}},"state":"RUNNING","#,
550 r#""overview":{"java":{"version":"17.0.11","vendor":"Azul Systems, Inc.","name":"OpenJDK 64-Bit Server VM"},"#,
551 r#""os":{"name":"Linux","arch":"amd64","version":"5.15.0"},"uptime_ms":338137,"memory":[338137088,1073741824],"#,
552 r#""cpu_fraction":0.0031,"disk":{"total":62661259264,"used":12272824320},"#,
553 r#""license":{"state":"trial","trial_remaining_s":7017}}}"#
554 ),
555 "data keys are the documented contract"
556 );
557
558 assert_eq!(result.gateway.ignition_version, "8.3.6 (b2026042713)");
559 assert_eq!(result.gateway.name.as_deref(), Some("ign-mock"));
560 assert_eq!(result.state, "RUNNING");
561 assert_eq!(result.overview.uptime_ms, 338137);
562 assert!((result.overview.cpu_fraction - 0.0031).abs() < f64::EPSILON);
563 let license = result.overview.license.expect("license block");
564 assert_eq!(license.state, "trial");
565 assert_eq!(license.trial_remaining_s, Some(7017));
566 }
567
568 struct BrokenOverview;
571
572 #[async_trait::async_trait]
573 impl GatewayApi for BrokenOverview {
574 async fn tag_provider_list(
575 &self,
576 _query: &crate::client::query::ListQuery,
577 ) -> Result<
578 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
579 CoreError,
580 > {
581 unreachable!("not part of this action")
582 }
583 async fn tag_provider_find(
584 &self,
585 _name: &str,
586 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
587 unreachable!("not part of this action")
588 }
589 async fn tag_provider_create(
590 &self,
591 _body: &[crate::client::tags::TagProviderCreate],
592 ) -> Result<(), CoreError> {
593 unreachable!("not part of this action")
594 }
595 async fn tag_provider_delete(
596 &self,
597 _name: &str,
598 _signature: &str,
599 ) -> Result<(), CoreError> {
600 unreachable!("not part of this action")
601 }
602 async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
603 unreachable!("not part of this action")
604 }
605 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
606 unreachable!("not part of this action")
607 }
608 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
609 unreachable!("not part of this action")
610 }
611 async fn backup_download(
612 &self,
613 _out: &std::path::Path,
614 _backup_type: crate::client::backup::BackupType,
615 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
616 unreachable!("not part of this action")
617 }
618 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
619 unreachable!("not part of this action")
620 }
621 async fn eam_task_history(
622 &self,
623 _limit: Option<u32>,
624 _search: Option<&str>,
625 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
626 {
627 unreachable!("not part of this action")
628 }
629 async fn eam_task_definitions(
630 &self,
631 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
632 {
633 unreachable!("not part of this action")
634 }
635 async fn eam_task_find(
636 &self,
637 _name: &str,
638 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
639 unreachable!("not part of this action")
640 }
641 async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
642 unreachable!("not part of this action")
643 }
644 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
645 unreachable!("not part of this action")
646 }
647 async fn gateway_info(&self) -> Result<GatewayInfo, CoreError> {
648 Ok(GatewayInfo {
649 name: None,
650 redundancy_role: None,
651 edition: None,
652 ignition_version: "8.3.6 (b2026042713)".into(),
653 jvm_version: None,
654 license: None,
655 endpoint: None,
656 })
657 }
658 async fn overview(&self) -> Result<Overview, CoreError> {
659 Err(CoreError::Auth {
660 status: 401,
661 endpoint: Some("http://gw.example.com/data/api/v1/overview".into()),
662 })
663 }
664 async fn status_ping(&self) -> Result<StatusPing, CoreError> {
665 unreachable!("status() must fail at overview() before pinging")
666 }
667 async fn modules(
668 &self,
669 _quarantined: bool,
670 _query: &crate::client::query::ListQuery,
671 ) -> Result<ListEnvelope<ModuleInfo>, CoreError> {
672 unreachable!("not part of this action")
673 }
674 async fn metrics_current(&self) -> Result<CurrentGauges, CoreError> {
675 unreachable!("not part of this action")
676 }
677 async fn metrics_historic(&self) -> Result<PerformanceCharts, CoreError> {
678 unreachable!("not part of this action")
679 }
680 async fn metrics_threads(&self) -> Result<ThreadCounts, CoreError> {
681 unreachable!("not part of this action")
682 }
683 async fn designers(
684 &self,
685 _query: &crate::client::query::ListQuery,
686 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
687 unreachable!("not part of this action")
688 }
689 async fn perspective_sessions(
690 &self,
691 _query: &crate::client::query::ListQuery,
692 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
693 unreachable!("not part of this action")
694 }
695 async fn vision_clients(
696 &self,
697 _query: &crate::client::query::ListQuery,
698 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
699 unreachable!("not part of this action")
700 }
701 async fn terminate_perspective_session(
702 &self,
703 _id: &str,
704 _message: Option<&str>,
705 ) -> Result<(), CoreError> {
706 unreachable!("not part of this action")
707 }
708 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
709 unreachable!("not part of this action")
710 }
711 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
712 unreachable!("not part of this action")
713 }
714 async fn database_connections(
715 &self,
716 ) -> Result<
717 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
718 CoreError,
719 > {
720 unreachable!("not part of this action")
721 }
722 async fn opc_connections(
723 &self,
724 ) -> Result<
725 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
726 CoreError,
727 > {
728 unreachable!("not part of this action")
729 }
730
731 async fn logs(
732 &self,
733 _filter: &crate::client::logs::LogQuery,
734 ) -> Result<ListEnvelope<crate::client::logs::LogEntry>, CoreError> {
735 unreachable!("not part of this double's actions")
736 }
737 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
738 unreachable!("not part of this double's actions")
739 }
740 async fn loggers(
741 &self,
742 _query: &crate::client::query::ListQuery,
743 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
744 unreachable!("not part of this double's actions")
745 }
746 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
747 unreachable!("not part of this double's actions")
748 }
749 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
750 unreachable!("not part of this double's actions")
751 }
752 async fn restart(&self) -> Result<(), CoreError> {
753 unreachable!("not part of this action")
754 }
755 async fn scan_projects(&self) -> Result<(), CoreError> {
756 unreachable!("not part of this action")
757 }
758 async fn security_properties(
759 &self,
760 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
761 unreachable!("not part of this action")
762 }
763 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
764 unreachable!("not part of this action")
765 }
766 async fn webdev_route_call(
767 &self,
768 _project: &str,
769 _route: &str,
770 _body: &serde_json::Value,
771 _extra_headers: &[(&str, &str)],
772 ) -> Result<serde_json::Value, CoreError> {
773 unreachable!("not part of this action")
774 }
775 async fn webdev_route_probe(
776 &self,
777 _project: &str,
778 _route: &str,
779 _extra_headers: &[(&str, &str)],
780 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
781 unreachable!("not part of this action")
782 }
783 async fn projects(
784 &self,
785 _query: &crate::client::query::ListQuery,
786 ) -> Result<
787 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
788 CoreError,
789 > {
790 unreachable!("not part of this action")
791 }
792 async fn project_find(
793 &self,
794 _name: &str,
795 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
796 unreachable!("not part of this action")
797 }
798 async fn project_create(
799 &self,
800 _body: &crate::client::projects::ProjectCreate,
801 ) -> Result<(), CoreError> {
802 unreachable!("not part of this action")
803 }
804 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
805 unreachable!("not part of this action")
806 }
807 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
808 unreachable!("not part of this action")
809 }
810 async fn project_modify(
811 &self,
812 _name: &str,
813 _body: &crate::client::projects::ProjectModify,
814 ) -> Result<(), CoreError> {
815 unreachable!("not part of this action")
816 }
817 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
818 unreachable!("not part of this action")
819 }
820 async fn project_export_to_file(
821 &self,
822 _name: &str,
823 _out: &std::path::Path,
824 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
825 unreachable!("not part of this action")
826 }
827 async fn project_import(
828 &self,
829 _name: &str,
830 _zip: Vec<u8>,
831 _overwrite: bool,
832 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
833 unreachable!("not part of this action")
834 }
835 }
836
837 #[tokio::test]
838 async fn status_propagates_subcall_errors() {
839 let err = status(&BrokenOverview).await.expect_err("401 propagates");
840 assert!(matches!(&err, CoreError::Auth { status: 401, .. }));
841 assert_eq!(err.exit_code(), 5);
842 }
843
844 #[tokio::test]
846 async fn modules_forwards_the_quarantined_flag() {
847 let rig = HealthyRig {
848 modules_flags: Mutex::new(Vec::new()),
849 };
850 let healthy: ModulesResult = modules(&rig, false).await.expect("healthy list");
851 let quarantined: ModulesResult = modules(&rig, true).await.expect("quarantined list");
852 assert!(!healthy.quarantined && healthy.items.len() == 1);
853 assert!(quarantined.quarantined);
854 assert_eq!(
855 *rig.modules_flags.lock().expect("flags lock"),
856 vec![false, true],
857 "the flag reached the client seam in call order"
858 );
859 }
860
861 #[tokio::test]
864 async fn metrics_history_is_opt_in() {
865 let rig = HealthyRig {
866 modules_flags: Mutex::new(Vec::new()),
867 };
868 let lean: MetricsResult = metrics(&rig, false).await.expect("lean metrics");
869 assert!(lean.history.is_none());
870 assert_eq!(lean.threads.running, 32);
871 assert!((lean.current.cpu - 4.88).abs() < f64::EPSILON);
872
873 let full: MetricsResult = metrics(&rig, true).await.expect("full metrics");
874 let charts = full.history.expect("charts included");
875 assert_eq!(charts.cpu_datapoints.len(), 1);
876 }
877}