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