1#![allow(clippy::missing_safety_doc)]
12#![expect(
13 clippy::undocumented_unsafe_blocks,
14 reason = "module-wide FFI safety contract documented in ffi::mod.rs preamble"
15)]
16
17use std::ffi::{c_char, c_int, CStr, CString};
18use std::sync::Arc;
19use std::time::Duration;
20
21use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};
22
23use crate::adapter::net::behavior::aggregator::{
24 FoldQueryClient, FoldQueryClientError, FoldQueryError, RegistryClient, RegistryClientError,
25 RegistryGroupSummary, RegistryRpcError, SummaryAnnouncement, DEFAULT_QUERY_DEADLINE,
26 DEFAULT_REGISTRY_DEADLINE,
27};
28use crate::adapter::net::{ChannelConfig, ChannelId, ChannelName, Visibility};
29
30use super::mesh::MeshNodeHandle;
31
32pub const NET_REGISTRY_ERR_UNKNOWN_KIND: i32 = 7;
38
39pub const NET_REGISTRY_OK: i32 = 0;
41pub const NET_REGISTRY_ERR_TRANSPORT: i32 = 1;
44pub const NET_REGISTRY_ERR_CODEC: i32 = 2;
46pub const NET_REGISTRY_ERR_UNKNOWN_TEMPLATE: i32 = 3;
48pub const NET_REGISTRY_ERR_DUPLICATE_GROUP_NAME: i32 = 4;
51pub const NET_REGISTRY_ERR_SPAWN_REJECTED: i32 = 5;
54pub const NET_REGISTRY_ERR_SPAWN_NOT_SUPPORTED: i32 = 6;
56pub const NET_REGISTRY_ERR_UNKNOWN_GROUP: i32 = 8;
59pub const NET_REGISTRY_ERR_SCALE_REJECTED: i32 = 9;
62pub const NET_REGISTRY_ERR_SCALE_NOT_SUPPORTED: i32 = 10;
65pub const NET_REGISTRY_ERR_INVALID_ARGS: i32 = 99;
68
69#[repr(i32)]
77#[derive(Copy, Clone)]
78pub enum NetVisibility {
79 Global = 0,
81 ParentVisible = 1,
83 Exported = 2,
85 SubnetLocal = 3,
87}
88
89impl NetVisibility {
90 fn from_raw(raw: i32) -> Option<Visibility> {
91 match raw {
92 0 => Some(Visibility::Global),
93 1 => Some(Visibility::ParentVisible),
94 2 => Some(Visibility::Exported),
95 3 => Some(Visibility::SubnetLocal),
96 _ => None,
97 }
98 }
99
100 #[allow(dead_code)] fn to_raw(v: Visibility) -> NetVisibility {
112 match v {
113 Visibility::Global => NetVisibility::Global,
114 Visibility::ParentVisible => NetVisibility::ParentVisible,
115 Visibility::Exported => NetVisibility::Exported,
116 Visibility::SubnetLocal => NetVisibility::SubnetLocal,
117 }
118 }
119}
120
121pub struct RegistryClientHandle {
133 client: ParkingRwLock<RegistryClient>,
134 last_error_detail: ParkingMutex<Option<CString>>,
135}
136
137#[unsafe(no_mangle)]
143pub unsafe extern "C" fn net_registry_client_new(
144 mesh_handle: *mut MeshNodeHandle,
145) -> *mut RegistryClientHandle {
146 if mesh_handle.is_null() {
147 return std::ptr::null_mut();
148 }
149 let mesh_arc = unsafe { super::mesh::mesh_node_arc(&*mesh_handle) };
150 let boxed = Box::new(RegistryClientHandle {
151 client: ParkingRwLock::new(RegistryClient::new(mesh_arc)),
152 last_error_detail: ParkingMutex::new(None),
153 });
154 Box::into_raw(boxed)
155}
156
157#[unsafe(no_mangle)]
160pub unsafe extern "C" fn net_registry_client_free(handle: *mut RegistryClientHandle) {
161 if handle.is_null() {
162 return;
163 }
164 drop(unsafe { Box::from_raw(handle) });
165}
166
167#[unsafe(no_mangle)]
173pub unsafe extern "C" fn net_registry_client_set_deadline(
174 handle: *mut RegistryClientHandle,
175 millis: u64,
176) {
177 if handle.is_null() {
178 return;
179 }
180 let h: &RegistryClientHandle = unsafe { &*handle };
181 let deadline = if millis == 0 {
182 DEFAULT_REGISTRY_DEADLINE
183 } else {
184 Duration::from_millis(millis)
185 };
186 h.client.write().set_deadline_mut(deadline);
187}
188
189#[inline]
201unsafe fn write_kind(out: *mut c_int, kind: c_int) {
202 if !out.is_null() {
203 unsafe { *out = kind };
204 }
205}
206
207#[inline]
212unsafe fn cstr_arg(ptr: *const c_char, out: *mut c_int) -> Option<String> {
213 if ptr.is_null() {
214 unsafe { write_kind(out, NET_REGISTRY_ERR_INVALID_ARGS) };
215 return None;
216 }
217 match unsafe { CStr::from_ptr(ptr).to_str() } {
218 Ok(s) => Some(s.to_owned()),
219 Err(_) => {
220 unsafe { write_kind(out, NET_REGISTRY_ERR_INVALID_ARGS) };
221 None
222 }
223 }
224}
225
226#[inline]
230unsafe fn json_to_raw(json: String, out: *mut c_int) -> *mut c_char {
231 match CString::new(json) {
232 Ok(s) => {
233 unsafe { write_kind(out, NET_REGISTRY_OK) };
234 s.into_raw()
235 }
236 Err(_) => {
237 unsafe { write_kind(out, NET_REGISTRY_ERR_CODEC) };
238 std::ptr::null_mut()
239 }
240 }
241}
242
243unsafe fn registry_op_json<F>(
248 handle: *mut RegistryClientHandle,
249 out_error_kind: *mut c_int,
250 op: F,
251) -> *mut c_char
252where
253 F: FnOnce(RegistryClient) -> Result<String, RegistryClientError>,
254{
255 if handle.is_null() {
256 unsafe { write_kind(out_error_kind, NET_REGISTRY_ERR_INVALID_ARGS) };
257 return std::ptr::null_mut();
258 }
259 let h: &RegistryClientHandle = unsafe { &*handle };
260 let client = h.client.read().clone();
261 match op(client) {
262 Ok(json) => unsafe { json_to_raw(json, out_error_kind) },
263 Err(e) => {
264 let (kind, detail) = classify(&e);
265 store_error_detail(h, detail);
266 unsafe { write_kind(out_error_kind, kind) };
267 std::ptr::null_mut()
268 }
269 }
270}
271
272#[unsafe(no_mangle)]
279pub unsafe extern "C" fn net_registry_client_list(
280 handle: *mut RegistryClientHandle,
281 target_node_id: u64,
282 out_error_kind: *mut c_int,
283) -> *mut c_char {
284 if out_error_kind.is_null() {
285 return std::ptr::null_mut();
286 }
287 unsafe {
288 registry_op_json(handle, out_error_kind, |client| {
289 block_on(client.list(target_node_id)).map(|groups| groups_to_json(&groups))
290 })
291 }
292}
293
294#[unsafe(no_mangle)]
297pub unsafe extern "C" fn net_registry_client_spawn(
298 handle: *mut RegistryClientHandle,
299 target_node_id: u64,
300 template_name: *const c_char,
301 group_name: *const c_char,
302 replica_count: u8,
303 out_error_kind: *mut c_int,
304) -> *mut c_char {
305 let Some(template) = (unsafe { cstr_arg(template_name, out_error_kind) }) else {
306 return std::ptr::null_mut();
307 };
308 let Some(group) = (unsafe { cstr_arg(group_name, out_error_kind) }) else {
309 return std::ptr::null_mut();
310 };
311 unsafe {
312 registry_op_json(handle, out_error_kind, |client| {
313 block_on(client.spawn(target_node_id, template, group, replica_count))
314 .map(|summary| group_to_json(&summary))
315 })
316 }
317}
318
319#[unsafe(no_mangle)]
324pub unsafe extern "C" fn net_registry_client_unregister(
325 handle: *mut RegistryClientHandle,
326 target_node_id: u64,
327 group_name: *const c_char,
328 out_error_kind: *mut c_int,
329) -> c_int {
330 if handle.is_null() {
331 unsafe { write_kind(out_error_kind, NET_REGISTRY_ERR_INVALID_ARGS) };
332 return -1;
333 }
334 let Some(group) = (unsafe { cstr_arg(group_name, out_error_kind) }) else {
335 return -1;
336 };
337 let h: &RegistryClientHandle = unsafe { &*handle };
338 let client = h.client.read().clone();
339 match block_on(client.unregister(target_node_id, group)) {
340 Ok(existed) => {
341 unsafe { write_kind(out_error_kind, NET_REGISTRY_OK) };
342 if existed {
343 1
344 } else {
345 0
346 }
347 }
348 Err(e) => {
349 let (kind, detail) = classify(&e);
350 store_error_detail(h, detail);
351 unsafe { write_kind(out_error_kind, kind) };
352 -1
353 }
354 }
355}
356
357#[unsafe(no_mangle)]
366pub unsafe extern "C" fn net_registry_last_error_detail(
367 handle: *mut RegistryClientHandle,
368) -> *const c_char {
369 if handle.is_null() {
370 return std::ptr::null();
371 }
372 let h: &RegistryClientHandle = unsafe { &*handle };
373 let guard = h.last_error_detail.lock();
374 match guard.as_ref() {
375 Some(c) => c.as_ptr(),
376 None => std::ptr::null(),
377 }
378}
379
380#[unsafe(no_mangle)]
392pub unsafe extern "C" fn net_register_channel(
393 mesh_handle: *mut MeshNodeHandle,
394 name: *const c_char,
395 visibility: c_int,
396) -> c_int {
397 if mesh_handle.is_null() || name.is_null() {
398 return NET_REGISTRY_ERR_INVALID_ARGS;
399 }
400 let vis = match NetVisibility::from_raw(visibility) {
401 Some(v) => v,
402 None => return NET_REGISTRY_ERR_INVALID_ARGS,
403 };
404 let name_str = match unsafe { CStr::from_ptr(name).to_str() } {
405 Ok(s) => s,
406 Err(_) => return NET_REGISTRY_ERR_INVALID_ARGS,
407 };
408 let channel = match ChannelName::new(name_str) {
409 Ok(c) => c,
410 Err(_) => return NET_REGISTRY_ERR_INVALID_ARGS,
411 };
412 let mesh_arc: Arc<crate::adapter::net::MeshNode> =
417 unsafe { super::mesh::mesh_node_arc(&*mesh_handle) };
418 let Some(configs) = mesh_arc.channel_configs() else {
419 return NET_REGISTRY_ERR_INVALID_ARGS;
420 };
421 let cfg = ChannelConfig::new(ChannelId::new(channel)).with_visibility(vis);
422 configs.insert(cfg);
423 NET_REGISTRY_OK
424}
425
426pub struct FoldQueryClientHandle {
434 client: ParkingRwLock<FoldQueryClient>,
435 last_error_detail: ParkingMutex<Option<CString>>,
436}
437
438#[unsafe(no_mangle)]
442pub unsafe extern "C" fn net_fold_query_client_new(
443 mesh_handle: *mut MeshNodeHandle,
444) -> *mut FoldQueryClientHandle {
445 if mesh_handle.is_null() {
446 return std::ptr::null_mut();
447 }
448 let mesh_arc = unsafe { super::mesh::mesh_node_arc(&*mesh_handle) };
449 let boxed = Box::new(FoldQueryClientHandle {
450 client: ParkingRwLock::new(FoldQueryClient::new(mesh_arc)),
451 last_error_detail: ParkingMutex::new(None),
452 });
453 Box::into_raw(boxed)
454}
455
456#[unsafe(no_mangle)]
458pub unsafe extern "C" fn net_fold_query_client_free(handle: *mut FoldQueryClientHandle) {
459 if handle.is_null() {
460 return;
461 }
462 drop(unsafe { Box::from_raw(handle) });
463}
464
465#[unsafe(no_mangle)]
469pub unsafe extern "C" fn net_fold_query_client_set_ttl(
470 handle: *mut FoldQueryClientHandle,
471 millis: u64,
472) {
473 if handle.is_null() {
474 return;
475 }
476 let h: &FoldQueryClientHandle = unsafe { &*handle };
477 h.client.write().set_ttl_mut(Duration::from_millis(millis));
478}
479
480#[unsafe(no_mangle)]
483pub unsafe extern "C" fn net_fold_query_client_set_deadline(
484 handle: *mut FoldQueryClientHandle,
485 millis: u64,
486) {
487 if handle.is_null() {
488 return;
489 }
490 let h: &FoldQueryClientHandle = unsafe { &*handle };
491 let deadline = if millis == 0 {
492 DEFAULT_QUERY_DEADLINE
493 } else {
494 Duration::from_millis(millis)
495 };
496 h.client.write().set_deadline_mut(deadline);
497}
498
499#[unsafe(no_mangle)]
505pub unsafe extern "C" fn net_fold_query_client_query_latest(
506 handle: *mut FoldQueryClientHandle,
507 target_node_id: u64,
508 kind: u16,
509 out_error_kind: *mut c_int,
510) -> *mut c_char {
511 if out_error_kind.is_null() {
512 return std::ptr::null_mut();
513 }
514 unsafe {
515 fold_query_op_json(handle, out_error_kind, |client| {
516 block_on(client.query_latest(target_node_id, kind))
517 .map(|summaries| summaries_to_json(&summaries))
518 })
519 }
520}
521
522#[unsafe(no_mangle)]
524pub unsafe extern "C" fn net_fold_query_client_query_summarize_now(
525 handle: *mut FoldQueryClientHandle,
526 target_node_id: u64,
527 kind: u16,
528 out_error_kind: *mut c_int,
529) -> *mut c_char {
530 if out_error_kind.is_null() {
531 return std::ptr::null_mut();
532 }
533 unsafe {
534 fold_query_op_json(handle, out_error_kind, |client| {
535 block_on(client.query_summarize_now(target_node_id, kind))
536 .map(|summaries| summaries_to_json(&summaries))
537 })
538 }
539}
540
541#[unsafe(no_mangle)]
543pub unsafe extern "C" fn net_fold_query_client_invalidate_cache(
544 handle: *mut FoldQueryClientHandle,
545) {
546 if handle.is_null() {
547 return;
548 }
549 let h: &FoldQueryClientHandle = unsafe { &*handle };
550 h.client.read().invalidate_cache();
551}
552
553#[unsafe(no_mangle)]
555pub unsafe extern "C" fn net_fold_query_client_invalidate_target(
556 handle: *mut FoldQueryClientHandle,
557 target_node_id: u64,
558) {
559 if handle.is_null() {
560 return;
561 }
562 let h: &FoldQueryClientHandle = unsafe { &*handle };
563 h.client.read().invalidate_target(target_node_id);
564}
565
566#[unsafe(no_mangle)]
570pub unsafe extern "C" fn net_fold_query_last_error_detail(
571 handle: *mut FoldQueryClientHandle,
572) -> *const c_char {
573 if handle.is_null() {
574 return std::ptr::null();
575 }
576 let h: &FoldQueryClientHandle = unsafe { &*handle };
577 let guard = h.last_error_detail.lock();
578 match guard.as_ref() {
579 Some(c) => c.as_ptr(),
580 None => std::ptr::null(),
581 }
582}
583
584fn block_on<F: std::future::Future>(future: F) -> F::Output {
590 super::mesh::block_on(future)
591}
592
593unsafe fn fold_query_op_json<F>(
596 handle: *mut FoldQueryClientHandle,
597 out_error_kind: *mut c_int,
598 op: F,
599) -> *mut c_char
600where
601 F: FnOnce(FoldQueryClient) -> Result<String, FoldQueryClientError>,
602{
603 if handle.is_null() {
604 unsafe { write_kind(out_error_kind, NET_REGISTRY_ERR_INVALID_ARGS) };
605 return std::ptr::null_mut();
606 }
607 let h: &FoldQueryClientHandle = unsafe { &*handle };
608 let client = h.client.read().clone();
609 match op(client) {
610 Ok(json) => unsafe { json_to_raw(json, out_error_kind) },
611 Err(e) => {
612 let (kind, detail) = classify_fold_query(&e);
613 store_fold_query_error_detail(h, detail);
614 unsafe { write_kind(out_error_kind, kind) };
615 std::ptr::null_mut()
616 }
617 }
618}
619
620fn classify_fold_query(err: &FoldQueryClientError) -> (i32, String) {
621 match err {
622 FoldQueryClientError::Transport(e) => (NET_REGISTRY_ERR_TRANSPORT, format!("{e}")),
623 FoldQueryClientError::Codec(c) => (NET_REGISTRY_ERR_CODEC, c.clone()),
624 FoldQueryClientError::Server(FoldQueryError::UnknownKind { kind }) => (
625 NET_REGISTRY_ERR_UNKNOWN_KIND,
626 format!("unknown fold kind: 0x{kind:04x}"),
627 ),
628 FoldQueryClientError::Server(FoldQueryError::DecodeFailed(s)) => {
629 (NET_REGISTRY_ERR_CODEC, format!("server decode: {s}"))
630 }
631 }
632}
633
634fn store_fold_query_error_detail(h: &FoldQueryClientHandle, detail: String) {
635 let c = match CString::new(detail) {
636 Ok(c) => c,
637 Err(_) => CString::new("invalid utf-8 in error detail").unwrap_or_default(),
638 };
639 *h.last_error_detail.lock() = Some(c);
640}
641
642fn summaries_to_json(summaries: &[SummaryAnnouncement]) -> String {
643 let wire: Vec<SummaryWire<'_>> = summaries.iter().map(SummaryWire::from).collect();
644 serde_json::to_string(&wire).unwrap_or_else(|_| "[]".to_string())
649}
650
651#[cfg(test)]
652fn summary_to_json(s: &SummaryAnnouncement) -> String {
653 serde_json::to_string(&SummaryWire::from(s)).unwrap_or_else(|_| "{}".to_string())
654}
655
656#[derive(serde::Serialize)]
657struct SummaryWire<'a> {
658 fold_kind: u16,
659 source_subnet: String,
660 generation: u64,
661 buckets: Vec<BucketWire<'a>>,
662}
663
664#[derive(serde::Serialize)]
665struct BucketWire<'a> {
666 name: &'a str,
667 count: u64,
668}
669
670impl<'a> From<&'a SummaryAnnouncement> for SummaryWire<'a> {
671 fn from(s: &'a SummaryAnnouncement) -> Self {
672 Self {
673 fold_kind: s.fold_kind,
674 source_subnet: format!("{}", s.source_subnet),
675 generation: s.generation,
676 buckets: s
677 .buckets
678 .iter()
679 .map(|(n, c)| BucketWire {
680 name: n.as_str(),
681 count: *c,
682 })
683 .collect(),
684 }
685 }
686}
687
688fn classify(err: &RegistryClientError) -> (i32, String) {
690 match err {
691 RegistryClientError::Transport(e) => (NET_REGISTRY_ERR_TRANSPORT, format!("{e}")),
692 RegistryClientError::Codec(c) => (NET_REGISTRY_ERR_CODEC, c.clone()),
693 RegistryClientError::Server(RegistryRpcError::DecodeFailed(s)) => {
694 (NET_REGISTRY_ERR_CODEC, format!("server decode: {s}"))
695 }
696 RegistryClientError::Server(RegistryRpcError::UnknownTemplate(t)) => (
697 NET_REGISTRY_ERR_UNKNOWN_TEMPLATE,
698 format!("unknown template: {t}"),
699 ),
700 RegistryClientError::Server(RegistryRpcError::DuplicateGroupName(n)) => (
701 NET_REGISTRY_ERR_DUPLICATE_GROUP_NAME,
702 format!("duplicate group name: {n}"),
703 ),
704 RegistryClientError::Server(RegistryRpcError::SpawnRejected(d)) => (
705 NET_REGISTRY_ERR_SPAWN_REJECTED,
706 format!("spawn rejected: {d}"),
707 ),
708 RegistryClientError::Server(RegistryRpcError::SpawnNotSupported) => (
709 NET_REGISTRY_ERR_SPAWN_NOT_SUPPORTED,
710 "daemon is read-only (no spawn handler installed)".to_string(),
711 ),
712 RegistryClientError::Server(RegistryRpcError::UnknownGroup(g)) => (
713 NET_REGISTRY_ERR_UNKNOWN_GROUP,
714 format!("unknown group: {g}"),
715 ),
716 RegistryClientError::Server(RegistryRpcError::ScaleRejected(d)) => (
717 NET_REGISTRY_ERR_SCALE_REJECTED,
718 format!("scale rejected: {d}"),
719 ),
720 RegistryClientError::Server(RegistryRpcError::ScaleNotSupported) => (
721 NET_REGISTRY_ERR_SCALE_NOT_SUPPORTED,
722 "daemon doesn't accept dynamic scale (no scaler installed)".to_string(),
723 ),
724 }
725}
726
727fn store_error_detail(h: &RegistryClientHandle, detail: String) {
728 let c = match CString::new(detail) {
729 Ok(c) => c,
730 Err(_) => CString::new("invalid utf-8 in error detail").unwrap_or_default(),
731 };
732 *h.last_error_detail.lock() = Some(c);
733}
734
735fn groups_to_json(groups: &[RegistryGroupSummary]) -> String {
743 let wire: Vec<GroupWire<'_>> = groups.iter().map(GroupWire::from).collect();
744 serde_json::to_string(&wire).unwrap_or_else(|_| "[]".to_string())
745}
746
747fn group_to_json(g: &RegistryGroupSummary) -> String {
748 serde_json::to_string(&GroupWire::from(g)).unwrap_or_else(|_| "{}".to_string())
749}
750
751#[derive(serde::Serialize)]
752struct GroupWire<'a> {
753 name: &'a str,
754 group_seed_hex: String,
755 replicas: Vec<ReplicaWire<'a>>,
756}
757
758#[derive(serde::Serialize)]
759struct ReplicaWire<'a> {
760 generation: u64,
761 healthy: bool,
762 diagnostic: Option<&'a str>,
763 placement_node_id: Option<u64>,
764}
765
766impl<'a> From<&'a RegistryGroupSummary> for GroupWire<'a> {
767 fn from(g: &'a RegistryGroupSummary) -> Self {
768 Self {
769 name: g.name.as_str(),
770 group_seed_hex: hex::encode(g.group_seed),
771 replicas: g
772 .replicas
773 .iter()
774 .map(|r| ReplicaWire {
775 generation: r.generation,
776 healthy: r.healthy,
777 diagnostic: r.diagnostic.as_deref(),
778 placement_node_id: r.placement_node_id,
779 })
780 .collect(),
781 }
782 }
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788
789 #[test]
790 fn visibility_round_trips_through_raw() {
791 for (raw, expected) in [
792 (0, Visibility::Global),
793 (1, Visibility::ParentVisible),
794 (2, Visibility::Exported),
795 (3, Visibility::SubnetLocal),
796 ] {
797 let back = NetVisibility::from_raw(raw).expect("known discriminant");
798 assert_eq!(format!("{back:?}"), format!("{expected:?}"));
799 }
800 assert!(NetVisibility::from_raw(99).is_none());
801 assert!(NetVisibility::from_raw(-1).is_none());
802 }
803
804 #[test]
805 fn group_to_json_includes_every_documented_field() {
806 let g = RegistryGroupSummary {
807 name: "alpha".into(),
808 group_seed: [0xABu8; 32],
809 source_subnet: crate::adapter::net::subnet::SubnetId::GLOBAL,
810 fold_kinds: vec![0x0001],
811 replicas: vec![
812 crate::adapter::net::behavior::aggregator::RegistryReplicaSummary {
813 generation: 42,
814 healthy: true,
815 diagnostic: None,
816 placement_node_id: Some(0xBEEF),
817 },
818 crate::adapter::net::behavior::aggregator::RegistryReplicaSummary {
819 generation: 0,
820 healthy: false,
821 diagnostic: Some("stuck".into()),
822 placement_node_id: None,
823 },
824 ],
825 };
826 let json = group_to_json(&g);
827 assert!(json.contains("\"name\":\"alpha\""));
828 assert!(json.contains("\"group_seed_hex\":\"abababababababababababababababababababababababababababababababab\""));
831 assert!(json.contains("\"generation\":42"));
832 assert!(json.contains("\"healthy\":true"));
833 assert!(json.contains("\"diagnostic\":null"));
834 assert!(json.contains("\"placement_node_id\":48879"));
835 assert!(json.contains("\"healthy\":false"));
836 assert!(json.contains("\"diagnostic\":\"stuck\""));
837 assert!(json.contains("\"placement_node_id\":null"));
838 }
839
840 #[test]
841 fn summary_to_json_includes_every_documented_field() {
842 let s = SummaryAnnouncement {
843 fold_kind: 0x42,
844 source_subnet: crate::adapter::net::subnet::SubnetId::GLOBAL,
845 generation: 7,
846 buckets: vec![("alpha".into(), 1), ("beta".into(), 2)],
847 };
848 let json = summary_to_json(&s);
849 assert!(json.contains("\"fold_kind\":66"));
850 assert!(json.contains("\"source_subnet\":\"global\""));
851 assert!(json.contains("\"generation\":7"));
852 assert!(json.contains("\"name\":\"alpha\""));
853 assert!(json.contains("\"count\":1"));
854 assert!(json.contains("\"name\":\"beta\""));
855 assert!(json.contains("\"count\":2"));
856 }
857
858 #[test]
859 fn classify_fold_query_maps_every_variant() {
860 use crate::adapter::net::mesh_rpc::RpcError;
861 let transport = FoldQueryClientError::Transport(RpcError::NoRoute {
864 target: 0,
865 reason: String::new(),
866 });
867 assert_eq!(
868 classify_fold_query(&transport).0,
869 NET_REGISTRY_ERR_TRANSPORT
870 );
871
872 let codec = FoldQueryClientError::Codec("bad".into());
873 assert_eq!(classify_fold_query(&codec).0, NET_REGISTRY_ERR_CODEC);
874
875 let unknown_kind = FoldQueryClientError::Server(FoldQueryError::UnknownKind { kind: 0x42 });
876 let (kind_code, detail) = classify_fold_query(&unknown_kind);
877 assert_eq!(kind_code, NET_REGISTRY_ERR_UNKNOWN_KIND);
878 assert!(detail.contains("0x0042"));
879
880 let decode_failed =
881 FoldQueryClientError::Server(FoldQueryError::DecodeFailed("boom".into()));
882 assert_eq!(
883 classify_fold_query(&decode_failed).0,
884 NET_REGISTRY_ERR_CODEC,
885 );
886 }
887}