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 bundle_generate(
240 &self,
241 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
242 unreachable!("not part of this action")
243 }
244 async fn bundle_status(
245 &self,
246 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
247 unreachable!("not part of this action")
248 }
249 async fn bundle_download(
250 &self,
251 _out: &std::path::Path,
252 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
253 unreachable!("not part of this action")
254 }
255 async fn tag_provider_list(
256 &self,
257 _query: &crate::client::query::ListQuery,
258 ) -> Result<
259 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
260 CoreError,
261 > {
262 unreachable!("not part of this action")
263 }
264 async fn tag_provider_find(
265 &self,
266 _name: &str,
267 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
268 unreachable!("not part of this action")
269 }
270 async fn tag_provider_create(
271 &self,
272 _body: &[crate::client::tags::TagProviderCreate],
273 ) -> Result<(), CoreError> {
274 unreachable!("not part of this action")
275 }
276 async fn tag_provider_delete(
277 &self,
278 _name: &str,
279 _signature: &str,
280 ) -> Result<(), CoreError> {
281 unreachable!("not part of this action")
282 }
283 async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
284 unreachable!("not part of this action")
285 }
286 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
287 unreachable!("not part of this action")
288 }
289 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
290 unreachable!("not part of this action")
291 }
292 async fn backup_download(
293 &self,
294 _out: &std::path::Path,
295 _backup_type: crate::client::backup::BackupType,
296 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
297 unreachable!("not part of this action")
298 }
299 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
300 unreachable!("not part of this action")
301 }
302 async fn eam_task_history(
303 &self,
304 _limit: Option<u32>,
305 _search: Option<&str>,
306 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
307 {
308 unreachable!("not part of this action")
309 }
310 async fn eam_task_definitions(
311 &self,
312 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
313 {
314 unreachable!("not part of this action")
315 }
316 async fn eam_task_find(
317 &self,
318 _name: &str,
319 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
320 unreachable!("not part of this action")
321 }
322 async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
323 unreachable!("not part of this action")
324 }
325 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
326 unreachable!("not part of this action")
327 }
328 async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
329 unreachable!("not part of this action")
330 }
331 async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
332 unreachable!("not part of this action")
333 }
334 async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
335 unreachable!("not part of this action")
336 }
337 async fn eam_tasks_scheduled(
338 &self,
339 _running: bool,
340 ) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
341 unreachable!("not part of this action")
342 }
343 async fn eam_task_modify(
344 &self,
345 _definition: &serde_json::Value,
346 ) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
347 unreachable!("not part of this action")
348 }
349 async fn eam_task_delete(
350 &self,
351 _name: &str,
352 _signature: &str,
353 _confirm: bool,
354 ) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
355 unreachable!("not part of this action")
356 }
357 async fn api_call(
358 &self,
359 _call: &crate::client::apicall::ApiCallRequest,
360 ) -> Result<crate::client::apicall::ApiCallData, CoreError> {
361 unreachable!("not part of this action")
362 }
363 async fn license_status(
364 &self,
365 ) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
366 unreachable!("not part of this action")
367 }
368 async fn redundancy_status(
369 &self,
370 ) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
371 unreachable!("not part of this action")
372 }
373 async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
374 unreachable!("not part of this action")
375 }
376 async fn gateway_info(&self) -> Result<GatewayInfo, CoreError> {
377 Ok(GatewayInfo {
378 name: Some("ign-mock".into()),
379 redundancy_role: Some("Independent".into()),
380 edition: Some("standard".into()),
381 ignition_version: "8.3.6 (b2026042713)".into(),
382 jvm_version: Some("17.0.11".into()),
383 license: Some(LicenseInfo {
384 mode: "Trial".into(),
385 expiration_date: Some("2026-08-24T19:00:00Z".into()),
386 }),
387 endpoint: None,
388 })
389 }
390 async fn overview(&self) -> Result<Overview, CoreError> {
391 Ok(overview_fixture())
392 }
393 async fn status_ping(&self) -> Result<StatusPing, CoreError> {
394 Ok(StatusPing {
395 state: "RUNNING".into(),
396 })
397 }
398 async fn modules(
399 &self,
400 quarantined: bool,
401 _query: &crate::client::query::ListQuery,
402 ) -> Result<ListEnvelope<ModuleInfo>, CoreError> {
403 self.modules_flags
404 .lock()
405 .expect("flags lock")
406 .push(quarantined);
407 let item = ModuleInfo {
408 id: "com.inductiveautomation.perspective".into(),
409 name: "Perspective".into(),
410 version: "8.3.6".into(),
411 state: Some("ACTIVE".into()),
412 license_state: Some("ACTIVATED".into()),
413 vendor_name: Some("Inductive Automation".into()),
414 startup_time: Some("2026-08-21 22:03:29".into()),
415 extra: Default::default(),
416 };
417 Ok(ListEnvelope {
418 items: vec![item],
419 metadata: ListMetadata {
420 total: 1,
421 matching: 1,
422 limit: -1,
423 offset: 0,
424 },
425 })
426 }
427 async fn metrics_current(&self) -> Result<CurrentGauges, CoreError> {
428 Ok(gauges_fixture())
429 }
430 async fn metrics_historic(&self) -> Result<PerformanceCharts, CoreError> {
431 Ok(charts_fixture())
432 }
433 async fn metrics_threads(&self) -> Result<ThreadCounts, CoreError> {
434 Ok(ThreadCounts {
435 running: 32,
436 waiting: 39,
437 timed_waiting: 51,
438 blocked: 0,
439 extra: Default::default(),
440 })
441 }
442 async fn designers(
443 &self,
444 _query: &crate::client::query::ListQuery,
445 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
446 unreachable!("not part of this double's actions")
447 }
448 async fn perspective_sessions(
449 &self,
450 _query: &crate::client::query::ListQuery,
451 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
452 unreachable!("not part of this double's actions")
453 }
454 async fn vision_clients(
455 &self,
456 _query: &crate::client::query::ListQuery,
457 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
458 unreachable!("not part of this double's actions")
459 }
460 async fn terminate_perspective_session(
461 &self,
462 _id: &str,
463 _message: Option<&str>,
464 ) -> Result<(), CoreError> {
465 unreachable!("not part of this double's actions")
466 }
467 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
468 unreachable!("not part of this double's actions")
469 }
470 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
471 unreachable!("not part of this double's actions")
472 }
473 async fn database_connections(
474 &self,
475 ) -> Result<
476 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
477 CoreError,
478 > {
479 unreachable!("not part of this double's actions")
480 }
481 async fn opc_connections(
482 &self,
483 ) -> Result<
484 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
485 CoreError,
486 > {
487 unreachable!("not part of this double's actions")
488 }
489
490 async fn logs(
491 &self,
492 _filter: &crate::client::logs::LogQuery,
493 ) -> Result<ListEnvelope<crate::client::logs::LogEntry>, CoreError> {
494 unreachable!("not part of this double's actions")
495 }
496 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
497 unreachable!("not part of this double's actions")
498 }
499 async fn loggers(
500 &self,
501 _query: &crate::client::query::ListQuery,
502 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
503 unreachable!("not part of this double's actions")
504 }
505 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
506 unreachable!("not part of this double's actions")
507 }
508 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
509 unreachable!("not part of this double's actions")
510 }
511 async fn restart(&self) -> Result<(), CoreError> {
512 unreachable!("not part of this action")
513 }
514 async fn scan_projects(&self) -> Result<(), CoreError> {
515 unreachable!("not part of this action")
516 }
517 async fn security_properties(
518 &self,
519 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
520 unreachable!("not part of this action")
521 }
522 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
523 unreachable!("not part of this action")
524 }
525 async fn webdev_route_call(
526 &self,
527 _project: &str,
528 _route: &str,
529 _body: &serde_json::Value,
530 _extra_headers: &[(&str, &str)],
531 ) -> Result<serde_json::Value, CoreError> {
532 unreachable!("not part of this action")
533 }
534 async fn webdev_route_probe(
535 &self,
536 _project: &str,
537 _route: &str,
538 _extra_headers: &[(&str, &str)],
539 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
540 unreachable!("not part of this action")
541 }
542 async fn projects(
543 &self,
544 _query: &crate::client::query::ListQuery,
545 ) -> Result<
546 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
547 CoreError,
548 > {
549 unreachable!("not part of this action")
550 }
551 async fn project_find(
552 &self,
553 _name: &str,
554 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
555 unreachable!("not part of this action")
556 }
557 async fn project_create(
558 &self,
559 _body: &crate::client::projects::ProjectCreate,
560 ) -> Result<(), CoreError> {
561 unreachable!("not part of this action")
562 }
563 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
564 unreachable!("not part of this action")
565 }
566 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
567 unreachable!("not part of this action")
568 }
569 async fn project_modify(
570 &self,
571 _name: &str,
572 _body: &crate::client::projects::ProjectModify,
573 ) -> Result<(), CoreError> {
574 unreachable!("not part of this action")
575 }
576 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
577 unreachable!("not part of this action")
578 }
579 async fn project_export_to_file(
580 &self,
581 _name: &str,
582 _out: &std::path::Path,
583 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
584 unreachable!("not part of this action")
585 }
586 async fn project_import(
587 &self,
588 _name: &str,
589 _zip: Vec<u8>,
590 _overwrite: bool,
591 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
592 unreachable!("not part of this action")
593 }
594 }
595
596 #[tokio::test]
599 async fn status_merges_gateway_overview_and_ping() {
600 let rig = HealthyRig {
601 modules_flags: Mutex::new(Vec::new()),
602 };
603 let result: StatusResult = status(&rig).await.expect("healthy rig merges");
604
605 let json = serde_json::to_string(&result).expect("serialize");
609 assert_eq!(
610 json,
611 concat!(
612 r#"{"gateway":{"name":"ign-mock","ignition_version":"8.3.6 (b2026042713)","edition":"standard","#,
613 r#""license":{"mode":"Trial","expirationDate":"2026-08-24T19:00:00Z"}},"state":"RUNNING","#,
614 r#""overview":{"java":{"version":"17.0.11","vendor":"Azul Systems, Inc.","name":"OpenJDK 64-Bit Server VM"},"#,
615 r#""os":{"name":"Linux","arch":"amd64","version":"5.15.0"},"uptime_ms":338137,"memory":[338137088,1073741824],"#,
616 r#""cpu_fraction":0.0031,"disk":{"total":62661259264,"used":12272824320},"#,
617 r#""license":{"state":"trial","trial_remaining_s":7017}}}"#
618 ),
619 "data keys are the documented contract"
620 );
621
622 assert_eq!(result.gateway.ignition_version, "8.3.6 (b2026042713)");
623 assert_eq!(result.gateway.name.as_deref(), Some("ign-mock"));
624 assert_eq!(result.state, "RUNNING");
625 assert_eq!(result.overview.uptime_ms, 338137);
626 assert!((result.overview.cpu_fraction - 0.0031).abs() < f64::EPSILON);
627 let license = result.overview.license.expect("license block");
628 assert_eq!(license.state, "trial");
629 assert_eq!(license.trial_remaining_s, Some(7017));
630 }
631
632 struct BrokenOverview;
635
636 #[async_trait::async_trait]
637 impl GatewayApi for BrokenOverview {
638 async fn bundle_generate(
639 &self,
640 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
641 unreachable!("not part of this action")
642 }
643 async fn bundle_status(
644 &self,
645 ) -> Result<crate::client::diagnostics::BundleStatusWire, CoreError> {
646 unreachable!("not part of this action")
647 }
648 async fn bundle_download(
649 &self,
650 _out: &std::path::Path,
651 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
652 unreachable!("not part of this action")
653 }
654 async fn tag_provider_list(
655 &self,
656 _query: &crate::client::query::ListQuery,
657 ) -> Result<
658 crate::client::query::ListEnvelope<crate::client::tags::TagProviderRecord>,
659 CoreError,
660 > {
661 unreachable!("not part of this action")
662 }
663 async fn tag_provider_find(
664 &self,
665 _name: &str,
666 ) -> Result<crate::client::tags::TagProviderRecord, CoreError> {
667 unreachable!("not part of this action")
668 }
669 async fn tag_provider_create(
670 &self,
671 _body: &[crate::client::tags::TagProviderCreate],
672 ) -> Result<(), CoreError> {
673 unreachable!("not part of this action")
674 }
675 async fn tag_provider_delete(
676 &self,
677 _name: &str,
678 _signature: &str,
679 ) -> Result<(), CoreError> {
680 unreachable!("not part of this action")
681 }
682 async fn trial_status_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
683 unreachable!("not part of this action")
684 }
685 async fn banners(&self) -> Result<crate::client::trial::BannerSet, CoreError> {
686 unreachable!("not part of this action")
687 }
688 async fn trial_reset_wire(&self) -> Result<crate::client::trial::TrialWire, CoreError> {
689 unreachable!("not part of this action")
690 }
691 async fn backup_download(
692 &self,
693 _out: &std::path::Path,
694 _backup_type: crate::client::backup::BackupType,
695 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
696 unreachable!("not part of this action")
697 }
698 async fn backup_restore(&self, _gwbk: &std::path::Path) -> Result<(), CoreError> {
699 unreachable!("not part of this action")
700 }
701 async fn eam_task_history(
702 &self,
703 _limit: Option<u32>,
704 _search: Option<&str>,
705 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamHistoryItem>, CoreError>
706 {
707 unreachable!("not part of this action")
708 }
709 async fn eam_task_definitions(
710 &self,
711 ) -> Result<crate::client::query::ListEnvelope<crate::client::eam::EamTaskRecord>, CoreError>
712 {
713 unreachable!("not part of this action")
714 }
715 async fn eam_task_find(
716 &self,
717 _name: &str,
718 ) -> Result<crate::client::eam::EamTaskRecord, CoreError> {
719 unreachable!("not part of this action")
720 }
721 async fn eam_task_create(&self, _definition: &serde_json::Value) -> Result<(), CoreError> {
722 unreachable!("not part of this action")
723 }
724 async fn eam_task_force(&self, _owner: &str, _name: &str) -> Result<(), CoreError> {
725 unreachable!("not part of this action")
726 }
727 async fn eam_task_suspend(&self, _name: &str) -> Result<(), CoreError> {
728 unreachable!("not part of this action")
729 }
730 async fn eam_task_resume(&self, _name: &str) -> Result<(), CoreError> {
731 unreachable!("not part of this action")
732 }
733 async fn eam_task_cancel(&self, _name: &str) -> Result<(), CoreError> {
734 unreachable!("not part of this action")
735 }
736 async fn eam_tasks_scheduled(
737 &self,
738 _running: bool,
739 ) -> Result<Vec<crate::client::eam::EamScheduledTask>, CoreError> {
740 unreachable!("not part of this action")
741 }
742 async fn eam_task_modify(
743 &self,
744 _definition: &serde_json::Value,
745 ) -> Result<Option<crate::client::eam::ModifyOutcome>, CoreError> {
746 unreachable!("not part of this action")
747 }
748 async fn eam_task_delete(
749 &self,
750 _name: &str,
751 _signature: &str,
752 _confirm: bool,
753 ) -> Result<crate::client::eam::DeleteOutcome, CoreError> {
754 unreachable!("not part of this action")
755 }
756 async fn api_call(
757 &self,
758 _call: &crate::client::apicall::ApiCallRequest,
759 ) -> Result<crate::client::apicall::ApiCallData, CoreError> {
760 unreachable!("not part of this action")
761 }
762 async fn license_status(
763 &self,
764 ) -> Result<crate::client::license::LicenseStatusWire, CoreError> {
765 unreachable!("not part of this action")
766 }
767 async fn redundancy_status(
768 &self,
769 ) -> Result<crate::client::redundancy::RedundancyStatusWire, CoreError> {
770 unreachable!("not part of this action")
771 }
772 async fn gan_status(&self) -> Result<crate::client::gan::GanStatusWire, CoreError> {
773 unreachable!("not part of this action")
774 }
775 async fn gateway_info(&self) -> Result<GatewayInfo, CoreError> {
776 Ok(GatewayInfo {
777 name: None,
778 redundancy_role: None,
779 edition: None,
780 ignition_version: "8.3.6 (b2026042713)".into(),
781 jvm_version: None,
782 license: None,
783 endpoint: None,
784 })
785 }
786 async fn overview(&self) -> Result<Overview, CoreError> {
787 Err(CoreError::Auth {
788 status: 401,
789 endpoint: Some("http://gw.example.com/data/api/v1/overview".into()),
790 })
791 }
792 async fn status_ping(&self) -> Result<StatusPing, CoreError> {
793 unreachable!("status() must fail at overview() before pinging")
794 }
795 async fn modules(
796 &self,
797 _quarantined: bool,
798 _query: &crate::client::query::ListQuery,
799 ) -> Result<ListEnvelope<ModuleInfo>, CoreError> {
800 unreachable!("not part of this action")
801 }
802 async fn metrics_current(&self) -> Result<CurrentGauges, CoreError> {
803 unreachable!("not part of this action")
804 }
805 async fn metrics_historic(&self) -> Result<PerformanceCharts, CoreError> {
806 unreachable!("not part of this action")
807 }
808 async fn metrics_threads(&self) -> Result<ThreadCounts, CoreError> {
809 unreachable!("not part of this action")
810 }
811 async fn designers(
812 &self,
813 _query: &crate::client::query::ListQuery,
814 ) -> Result<ListEnvelope<crate::client::sessions::DesignerInfo>, CoreError> {
815 unreachable!("not part of this action")
816 }
817 async fn perspective_sessions(
818 &self,
819 _query: &crate::client::query::ListQuery,
820 ) -> Result<ListEnvelope<crate::client::sessions::PerspectiveSession>, CoreError> {
821 unreachable!("not part of this action")
822 }
823 async fn vision_clients(
824 &self,
825 _query: &crate::client::query::ListQuery,
826 ) -> Result<ListEnvelope<crate::client::sessions::VisionClient>, CoreError> {
827 unreachable!("not part of this action")
828 }
829 async fn terminate_perspective_session(
830 &self,
831 _id: &str,
832 _message: Option<&str>,
833 ) -> Result<(), CoreError> {
834 unreachable!("not part of this action")
835 }
836 async fn terminate_vision_client(&self, _id: &str) -> Result<(), CoreError> {
837 unreachable!("not part of this action")
838 }
839 async fn prune_designer(&self, _id: &str) -> Result<(), CoreError> {
840 unreachable!("not part of this action")
841 }
842 async fn database_connections(
843 &self,
844 ) -> Result<
845 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
846 CoreError,
847 > {
848 unreachable!("not part of this action")
849 }
850 async fn opc_connections(
851 &self,
852 ) -> Result<
853 crate::client::query::ListEnvelope<crate::client::connections::GatewayConnection>,
854 CoreError,
855 > {
856 unreachable!("not part of this action")
857 }
858
859 async fn logs(
860 &self,
861 _filter: &crate::client::logs::LogQuery,
862 ) -> Result<ListEnvelope<crate::client::logs::LogEntry>, CoreError> {
863 unreachable!("not part of this double's actions")
864 }
865 async fn logs_download(&self) -> Result<crate::client::logs::LogDownload, CoreError> {
866 unreachable!("not part of this double's actions")
867 }
868 async fn loggers(
869 &self,
870 _query: &crate::client::query::ListQuery,
871 ) -> Result<ListEnvelope<crate::client::logs::LoggerInfo>, CoreError> {
872 unreachable!("not part of this double's actions")
873 }
874 async fn set_logger_level(&self, _logger: &str, _level: &str) -> Result<(), CoreError> {
875 unreachable!("not part of this double's actions")
876 }
877 async fn reset_logger_levels(&self) -> Result<(), CoreError> {
878 unreachable!("not part of this double's actions")
879 }
880 async fn restart(&self) -> Result<(), CoreError> {
881 unreachable!("not part of this action")
882 }
883 async fn scan_projects(&self) -> Result<(), CoreError> {
884 unreachable!("not part of this action")
885 }
886 async fn security_properties(
887 &self,
888 ) -> Result<crate::client::restart::SecurityProperties, CoreError> {
889 unreachable!("not part of this action")
890 }
891 async fn webdev_route_status(&self, _route: &str) -> Result<u16, CoreError> {
892 unreachable!("not part of this action")
893 }
894 async fn webdev_route_call(
895 &self,
896 _project: &str,
897 _route: &str,
898 _body: &serde_json::Value,
899 _extra_headers: &[(&str, &str)],
900 ) -> Result<serde_json::Value, CoreError> {
901 unreachable!("not part of this action")
902 }
903 async fn webdev_route_probe(
904 &self,
905 _project: &str,
906 _route: &str,
907 _extra_headers: &[(&str, &str)],
908 ) -> Result<crate::client::webdev::RouteProbe, CoreError> {
909 unreachable!("not part of this action")
910 }
911 async fn projects(
912 &self,
913 _query: &crate::client::query::ListQuery,
914 ) -> Result<
915 crate::client::query::ListEnvelope<crate::client::projects::ProjectRecord>,
916 CoreError,
917 > {
918 unreachable!("not part of this action")
919 }
920 async fn project_find(
921 &self,
922 _name: &str,
923 ) -> Result<crate::client::projects::ProjectRecord, CoreError> {
924 unreachable!("not part of this action")
925 }
926 async fn project_create(
927 &self,
928 _body: &crate::client::projects::ProjectCreate,
929 ) -> Result<(), CoreError> {
930 unreachable!("not part of this action")
931 }
932 async fn project_copy(&self, _from: &str, _to: &str) -> Result<(), CoreError> {
933 unreachable!("not part of this action")
934 }
935 async fn project_rename(&self, _name: &str, _new_name: &str) -> Result<(), CoreError> {
936 unreachable!("not part of this action")
937 }
938 async fn project_modify(
939 &self,
940 _name: &str,
941 _body: &crate::client::projects::ProjectModify,
942 ) -> Result<(), CoreError> {
943 unreachable!("not part of this action")
944 }
945 async fn project_delete(&self, _name: &str) -> Result<(), CoreError> {
946 unreachable!("not part of this action")
947 }
948 async fn project_export_to_file(
949 &self,
950 _name: &str,
951 _out: &std::path::Path,
952 ) -> Result<crate::client::projects::ExportMeta, CoreError> {
953 unreachable!("not part of this action")
954 }
955 async fn project_import(
956 &self,
957 _name: &str,
958 _zip: Vec<u8>,
959 _overwrite: bool,
960 ) -> Result<crate::client::projects::ImportOutcome, CoreError> {
961 unreachable!("not part of this action")
962 }
963 }
964
965 #[tokio::test]
966 async fn status_propagates_subcall_errors() {
967 let err = status(&BrokenOverview).await.expect_err("401 propagates");
968 assert!(matches!(&err, CoreError::Auth { status: 401, .. }));
969 assert_eq!(err.exit_code(), 5);
970 }
971
972 #[tokio::test]
974 async fn modules_forwards_the_quarantined_flag() {
975 let rig = HealthyRig {
976 modules_flags: Mutex::new(Vec::new()),
977 };
978 let healthy: ModulesResult = modules(&rig, false).await.expect("healthy list");
979 let quarantined: ModulesResult = modules(&rig, true).await.expect("quarantined list");
980 assert!(!healthy.quarantined && healthy.items.len() == 1);
981 assert!(quarantined.quarantined);
982 assert_eq!(
983 *rig.modules_flags.lock().expect("flags lock"),
984 vec![false, true],
985 "the flag reached the client seam in call order"
986 );
987 }
988
989 #[tokio::test]
992 async fn metrics_history_is_opt_in() {
993 let rig = HealthyRig {
994 modules_flags: Mutex::new(Vec::new()),
995 };
996 let lean: MetricsResult = metrics(&rig, false).await.expect("lean metrics");
997 assert!(lean.history.is_none());
998 assert_eq!(lean.threads.running, 32);
999 assert!((lean.current.cpu - 4.88).abs() < f64::EPSILON);
1000
1001 let full: MetricsResult = metrics(&rig, true).await.expect("full metrics");
1002 let charts = full.history.expect("charts included");
1003 assert_eq!(charts.cpu_datapoints.len(), 1);
1004 }
1005}