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