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::time::Duration;
19
20use parking_lot::{Mutex as ParkingMutex, RwLock as ParkingRwLock};
21
22use crate::adapter::net::behavior::aggregator::{
23 FoldQueryClient, FoldQueryClientError, FoldQueryError, RegistryClient, RegistryClientError,
24 RegistryGroupSummary, RegistryRpcError, SummaryAnnouncement, DEFAULT_QUERY_DEADLINE,
25 DEFAULT_REGISTRY_DEADLINE,
26};
27use crate::adapter::net::{ChannelConfig, ChannelId, ChannelName, Visibility};
28
29use super::mesh::MeshNodeHandle;
30
31pub const NET_REGISTRY_ERR_UNKNOWN_KIND: i32 = 7;
37
38pub const NET_REGISTRY_OK: i32 = 0;
40pub const NET_REGISTRY_ERR_TRANSPORT: i32 = 1;
43pub const NET_REGISTRY_ERR_CODEC: i32 = 2;
45pub const NET_REGISTRY_ERR_UNKNOWN_TEMPLATE: i32 = 3;
47pub const NET_REGISTRY_ERR_DUPLICATE_GROUP_NAME: i32 = 4;
50pub const NET_REGISTRY_ERR_SPAWN_REJECTED: i32 = 5;
53pub const NET_REGISTRY_ERR_SPAWN_NOT_SUPPORTED: i32 = 6;
55pub const NET_REGISTRY_ERR_UNKNOWN_GROUP: i32 = 8;
58pub const NET_REGISTRY_ERR_SCALE_REJECTED: i32 = 9;
61pub const NET_REGISTRY_ERR_SCALE_NOT_SUPPORTED: i32 = 10;
64pub const NET_REGISTRY_ERR_INVALID_ARGS: i32 = 99;
67
68#[repr(i32)]
76#[derive(Copy, Clone)]
77pub enum NetVisibility {
78 Global = 0,
80 ParentVisible = 1,
82 Exported = 2,
84 SubnetLocal = 3,
86}
87
88impl NetVisibility {
89 fn from_raw(raw: i32) -> Option<Visibility> {
90 match raw {
91 0 => Some(Visibility::Global),
92 1 => Some(Visibility::ParentVisible),
93 2 => Some(Visibility::Exported),
94 3 => Some(Visibility::SubnetLocal),
95 _ => None,
96 }
97 }
98
99 #[allow(dead_code)] fn to_raw(v: Visibility) -> NetVisibility {
111 match v {
112 Visibility::Global => NetVisibility::Global,
113 Visibility::ParentVisible => NetVisibility::ParentVisible,
114 Visibility::Exported => NetVisibility::Exported,
115 Visibility::SubnetLocal => NetVisibility::SubnetLocal,
116 }
117 }
118}
119
120pub struct RegistryClientHandle {
132 client: ParkingRwLock<RegistryClient>,
133 last_error_detail: ParkingMutex<Option<CString>>,
134}
135
136#[unsafe(no_mangle)]
142pub unsafe extern "C" fn net_registry_client_new(
143 mesh_handle: *mut MeshNodeHandle,
144) -> *mut RegistryClientHandle {
145 if mesh_handle.is_null() {
146 return std::ptr::null_mut();
147 }
148 let Some(mesh_arc) = (unsafe { super::mesh::mesh_node_arc(&*mesh_handle) }) else {
152 return std::ptr::null_mut();
153 };
154 let boxed = Box::new(RegistryClientHandle {
155 client: ParkingRwLock::new(RegistryClient::new(mesh_arc)),
156 last_error_detail: ParkingMutex::new(None),
157 });
158 Box::into_raw(boxed)
159}
160
161#[unsafe(no_mangle)]
164pub unsafe extern "C" fn net_registry_client_free(handle: *mut RegistryClientHandle) {
165 if handle.is_null() {
166 return;
167 }
168 drop(unsafe { Box::from_raw(handle) });
169}
170
171#[unsafe(no_mangle)]
177pub unsafe extern "C" fn net_registry_client_set_deadline(
178 handle: *mut RegistryClientHandle,
179 millis: u64,
180) {
181 if handle.is_null() {
182 return;
183 }
184 let h: &RegistryClientHandle = unsafe { &*handle };
185 let deadline = if millis == 0 {
186 DEFAULT_REGISTRY_DEADLINE
187 } else {
188 Duration::from_millis(millis)
189 };
190 h.client.write().set_deadline_mut(deadline);
191}
192
193#[inline]
205unsafe fn write_kind(out: *mut c_int, kind: c_int) {
206 if !out.is_null() {
207 unsafe { *out = kind };
208 }
209}
210
211#[inline]
216unsafe fn cstr_arg(ptr: *const c_char, out: *mut c_int) -> Option<String> {
217 if ptr.is_null() {
218 unsafe { write_kind(out, NET_REGISTRY_ERR_INVALID_ARGS) };
219 return None;
220 }
221 match unsafe { CStr::from_ptr(ptr).to_str() } {
222 Ok(s) => Some(s.to_owned()),
223 Err(_) => {
224 unsafe { write_kind(out, NET_REGISTRY_ERR_INVALID_ARGS) };
225 None
226 }
227 }
228}
229
230#[inline]
234unsafe fn json_to_raw(json: String, out: *mut c_int) -> *mut c_char {
235 match CString::new(json) {
236 Ok(s) => {
237 unsafe { write_kind(out, NET_REGISTRY_OK) };
238 s.into_raw()
239 }
240 Err(_) => {
241 unsafe { write_kind(out, NET_REGISTRY_ERR_CODEC) };
242 std::ptr::null_mut()
243 }
244 }
245}
246
247unsafe fn registry_op_json<F>(
252 handle: *mut RegistryClientHandle,
253 out_error_kind: *mut c_int,
254 op: F,
255) -> *mut c_char
256where
257 F: FnOnce(RegistryClient) -> Result<String, RegistryClientError>,
258{
259 if handle.is_null() {
260 unsafe { write_kind(out_error_kind, NET_REGISTRY_ERR_INVALID_ARGS) };
261 return std::ptr::null_mut();
262 }
263 let h: &RegistryClientHandle = unsafe { &*handle };
264 let client = h.client.read().clone();
265 match op(client) {
266 Ok(json) => unsafe { json_to_raw(json, out_error_kind) },
267 Err(e) => {
268 let (kind, detail) = classify(&e);
269 store_error_detail(h, detail);
270 unsafe { write_kind(out_error_kind, kind) };
271 std::ptr::null_mut()
272 }
273 }
274}
275
276#[unsafe(no_mangle)]
283pub unsafe extern "C" fn net_registry_client_list(
284 handle: *mut RegistryClientHandle,
285 target_node_id: u64,
286 out_error_kind: *mut c_int,
287) -> *mut c_char {
288 if out_error_kind.is_null() {
289 return std::ptr::null_mut();
290 }
291 unsafe {
292 registry_op_json(handle, out_error_kind, |client| {
293 block_on(client.list(target_node_id)).map(|groups| groups_to_json(&groups))
294 })
295 }
296}
297
298#[unsafe(no_mangle)]
301pub unsafe extern "C" fn net_registry_client_spawn(
302 handle: *mut RegistryClientHandle,
303 target_node_id: u64,
304 template_name: *const c_char,
305 group_name: *const c_char,
306 replica_count: u8,
307 out_error_kind: *mut c_int,
308) -> *mut c_char {
309 let Some(template) = (unsafe { cstr_arg(template_name, out_error_kind) }) else {
310 return std::ptr::null_mut();
311 };
312 let Some(group) = (unsafe { cstr_arg(group_name, out_error_kind) }) else {
313 return std::ptr::null_mut();
314 };
315 unsafe {
316 registry_op_json(handle, out_error_kind, |client| {
317 block_on(client.spawn(target_node_id, template, group, replica_count))
318 .map(|summary| group_to_json(&summary))
319 })
320 }
321}
322
323#[unsafe(no_mangle)]
328pub unsafe extern "C" fn net_registry_client_unregister(
329 handle: *mut RegistryClientHandle,
330 target_node_id: u64,
331 group_name: *const c_char,
332 out_error_kind: *mut c_int,
333) -> c_int {
334 if handle.is_null() {
335 unsafe { write_kind(out_error_kind, NET_REGISTRY_ERR_INVALID_ARGS) };
336 return -1;
337 }
338 let Some(group) = (unsafe { cstr_arg(group_name, out_error_kind) }) else {
339 return -1;
340 };
341 let h: &RegistryClientHandle = unsafe { &*handle };
342 let client = h.client.read().clone();
343 match block_on(client.unregister(target_node_id, group)) {
344 Ok(existed) => {
345 unsafe { write_kind(out_error_kind, NET_REGISTRY_OK) };
346 if existed {
347 1
348 } else {
349 0
350 }
351 }
352 Err(e) => {
353 let (kind, detail) = classify(&e);
354 store_error_detail(h, detail);
355 unsafe { write_kind(out_error_kind, kind) };
356 -1
357 }
358 }
359}
360
361#[unsafe(no_mangle)]
370pub unsafe extern "C" fn net_registry_last_error_detail(
371 handle: *mut RegistryClientHandle,
372) -> *const c_char {
373 if handle.is_null() {
374 return std::ptr::null();
375 }
376 let h: &RegistryClientHandle = unsafe { &*handle };
377 let guard = h.last_error_detail.lock();
378 match guard.as_ref() {
379 Some(c) => c.as_ptr(),
380 None => std::ptr::null(),
381 }
382}
383
384#[unsafe(no_mangle)]
396pub unsafe extern "C" fn net_register_channel(
397 mesh_handle: *mut MeshNodeHandle,
398 name: *const c_char,
399 visibility: c_int,
400) -> c_int {
401 if mesh_handle.is_null() || name.is_null() {
402 return NET_REGISTRY_ERR_INVALID_ARGS;
403 }
404 let vis = match NetVisibility::from_raw(visibility) {
405 Some(v) => v,
406 None => return NET_REGISTRY_ERR_INVALID_ARGS,
407 };
408 let name_str = match unsafe { CStr::from_ptr(name).to_str() } {
409 Ok(s) => s,
410 Err(_) => return NET_REGISTRY_ERR_INVALID_ARGS,
411 };
412 let channel = match ChannelName::new(name_str) {
413 Ok(c) => c,
414 Err(_) => return NET_REGISTRY_ERR_INVALID_ARGS,
415 };
416 let Some(mesh_arc) = (unsafe { super::mesh::mesh_node_arc(&*mesh_handle) }) else {
421 return NET_REGISTRY_ERR_INVALID_ARGS;
422 };
423 let Some(configs) = mesh_arc.channel_configs() else {
424 return NET_REGISTRY_ERR_INVALID_ARGS;
425 };
426 let cfg = ChannelConfig::new(ChannelId::new(channel)).with_visibility(vis);
427 configs.insert(cfg);
428 NET_REGISTRY_OK
429}
430
431pub struct FoldQueryClientHandle {
439 client: ParkingRwLock<FoldQueryClient>,
440 last_error_detail: ParkingMutex<Option<CString>>,
441}
442
443#[unsafe(no_mangle)]
447pub unsafe extern "C" fn net_fold_query_client_new(
448 mesh_handle: *mut MeshNodeHandle,
449) -> *mut FoldQueryClientHandle {
450 if mesh_handle.is_null() {
451 return std::ptr::null_mut();
452 }
453 let Some(mesh_arc) = (unsafe { super::mesh::mesh_node_arc(&*mesh_handle) }) else {
454 return std::ptr::null_mut();
455 };
456 let boxed = Box::new(FoldQueryClientHandle {
457 client: ParkingRwLock::new(FoldQueryClient::new(mesh_arc)),
458 last_error_detail: ParkingMutex::new(None),
459 });
460 Box::into_raw(boxed)
461}
462
463#[unsafe(no_mangle)]
465pub unsafe extern "C" fn net_fold_query_client_free(handle: *mut FoldQueryClientHandle) {
466 if handle.is_null() {
467 return;
468 }
469 drop(unsafe { Box::from_raw(handle) });
470}
471
472#[unsafe(no_mangle)]
476pub unsafe extern "C" fn net_fold_query_client_set_ttl(
477 handle: *mut FoldQueryClientHandle,
478 millis: u64,
479) {
480 if handle.is_null() {
481 return;
482 }
483 let h: &FoldQueryClientHandle = unsafe { &*handle };
484 h.client.write().set_ttl_mut(Duration::from_millis(millis));
485}
486
487#[unsafe(no_mangle)]
490pub unsafe extern "C" fn net_fold_query_client_set_deadline(
491 handle: *mut FoldQueryClientHandle,
492 millis: u64,
493) {
494 if handle.is_null() {
495 return;
496 }
497 let h: &FoldQueryClientHandle = unsafe { &*handle };
498 let deadline = if millis == 0 {
499 DEFAULT_QUERY_DEADLINE
500 } else {
501 Duration::from_millis(millis)
502 };
503 h.client.write().set_deadline_mut(deadline);
504}
505
506#[unsafe(no_mangle)]
512pub unsafe extern "C" fn net_fold_query_client_query_latest(
513 handle: *mut FoldQueryClientHandle,
514 target_node_id: u64,
515 kind: u16,
516 out_error_kind: *mut c_int,
517) -> *mut c_char {
518 if out_error_kind.is_null() {
519 return std::ptr::null_mut();
520 }
521 unsafe {
522 fold_query_op_json(handle, out_error_kind, |client| {
523 block_on(client.query_latest(target_node_id, kind))
524 .map(|summaries| summaries_to_json(&summaries))
525 })
526 }
527}
528
529#[unsafe(no_mangle)]
531pub unsafe extern "C" fn net_fold_query_client_query_summarize_now(
532 handle: *mut FoldQueryClientHandle,
533 target_node_id: u64,
534 kind: u16,
535 out_error_kind: *mut c_int,
536) -> *mut c_char {
537 if out_error_kind.is_null() {
538 return std::ptr::null_mut();
539 }
540 unsafe {
541 fold_query_op_json(handle, out_error_kind, |client| {
542 block_on(client.query_summarize_now(target_node_id, kind))
543 .map(|summaries| summaries_to_json(&summaries))
544 })
545 }
546}
547
548#[unsafe(no_mangle)]
550pub unsafe extern "C" fn net_fold_query_client_invalidate_cache(
551 handle: *mut FoldQueryClientHandle,
552) {
553 if handle.is_null() {
554 return;
555 }
556 let h: &FoldQueryClientHandle = unsafe { &*handle };
557 h.client.read().invalidate_cache();
558}
559
560#[unsafe(no_mangle)]
562pub unsafe extern "C" fn net_fold_query_client_invalidate_target(
563 handle: *mut FoldQueryClientHandle,
564 target_node_id: u64,
565) {
566 if handle.is_null() {
567 return;
568 }
569 let h: &FoldQueryClientHandle = unsafe { &*handle };
570 h.client.read().invalidate_target(target_node_id);
571}
572
573#[unsafe(no_mangle)]
577pub unsafe extern "C" fn net_fold_query_last_error_detail(
578 handle: *mut FoldQueryClientHandle,
579) -> *const c_char {
580 if handle.is_null() {
581 return std::ptr::null();
582 }
583 let h: &FoldQueryClientHandle = unsafe { &*handle };
584 let guard = h.last_error_detail.lock();
585 match guard.as_ref() {
586 Some(c) => c.as_ptr(),
587 None => std::ptr::null(),
588 }
589}
590
591fn block_on<F: std::future::Future>(future: F) -> F::Output {
597 super::mesh::block_on(future)
598}
599
600unsafe fn fold_query_op_json<F>(
603 handle: *mut FoldQueryClientHandle,
604 out_error_kind: *mut c_int,
605 op: F,
606) -> *mut c_char
607where
608 F: FnOnce(FoldQueryClient) -> Result<String, FoldQueryClientError>,
609{
610 if handle.is_null() {
611 unsafe { write_kind(out_error_kind, NET_REGISTRY_ERR_INVALID_ARGS) };
612 return std::ptr::null_mut();
613 }
614 let h: &FoldQueryClientHandle = unsafe { &*handle };
615 let client = h.client.read().clone();
616 match op(client) {
617 Ok(json) => unsafe { json_to_raw(json, out_error_kind) },
618 Err(e) => {
619 let (kind, detail) = classify_fold_query(&e);
620 store_fold_query_error_detail(h, detail);
621 unsafe { write_kind(out_error_kind, kind) };
622 std::ptr::null_mut()
623 }
624 }
625}
626
627fn classify_fold_query(err: &FoldQueryClientError) -> (i32, String) {
628 match err {
629 FoldQueryClientError::Transport(e) => (NET_REGISTRY_ERR_TRANSPORT, format!("{e}")),
630 FoldQueryClientError::Codec(c) => (NET_REGISTRY_ERR_CODEC, c.clone()),
631 FoldQueryClientError::Server(FoldQueryError::UnknownKind { kind }) => (
632 NET_REGISTRY_ERR_UNKNOWN_KIND,
633 format!("unknown fold kind: 0x{kind:04x}"),
634 ),
635 FoldQueryClientError::Server(FoldQueryError::DecodeFailed(s)) => {
636 (NET_REGISTRY_ERR_CODEC, format!("server decode: {s}"))
637 }
638 }
639}
640
641fn store_fold_query_error_detail(h: &FoldQueryClientHandle, detail: String) {
642 let c = match CString::new(detail) {
643 Ok(c) => c,
644 Err(_) => CString::new("invalid utf-8 in error detail").unwrap_or_default(),
645 };
646 *h.last_error_detail.lock() = Some(c);
647}
648
649fn summaries_to_json(summaries: &[SummaryAnnouncement]) -> String {
650 let wire: Vec<SummaryWire<'_>> = summaries.iter().map(SummaryWire::from).collect();
651 serde_json::to_string(&wire).unwrap_or_else(|_| "[]".to_string())
656}
657
658#[cfg(test)]
659fn summary_to_json(s: &SummaryAnnouncement) -> String {
660 serde_json::to_string(&SummaryWire::from(s)).unwrap_or_else(|_| "{}".to_string())
661}
662
663#[derive(serde::Serialize)]
664struct SummaryWire<'a> {
665 fold_kind: u16,
666 source_subnet: String,
667 generation: u64,
668 buckets: Vec<BucketWire<'a>>,
669}
670
671#[derive(serde::Serialize)]
672struct BucketWire<'a> {
673 name: &'a str,
674 count: u64,
675}
676
677impl<'a> From<&'a SummaryAnnouncement> for SummaryWire<'a> {
678 fn from(s: &'a SummaryAnnouncement) -> Self {
679 Self {
680 fold_kind: s.fold_kind,
681 source_subnet: format!("{}", s.source_subnet),
682 generation: s.generation,
683 buckets: s
684 .buckets
685 .iter()
686 .map(|(n, c)| BucketWire {
687 name: n.as_str(),
688 count: *c,
689 })
690 .collect(),
691 }
692 }
693}
694
695fn classify(err: &RegistryClientError) -> (i32, String) {
697 match err {
698 RegistryClientError::Transport(e) => (NET_REGISTRY_ERR_TRANSPORT, format!("{e}")),
699 RegistryClientError::Codec(c) => (NET_REGISTRY_ERR_CODEC, c.clone()),
700 RegistryClientError::Server(RegistryRpcError::DecodeFailed(s)) => {
701 (NET_REGISTRY_ERR_CODEC, format!("server decode: {s}"))
702 }
703 RegistryClientError::Server(RegistryRpcError::UnknownTemplate(t)) => (
704 NET_REGISTRY_ERR_UNKNOWN_TEMPLATE,
705 format!("unknown template: {t}"),
706 ),
707 RegistryClientError::Server(RegistryRpcError::DuplicateGroupName(n)) => (
708 NET_REGISTRY_ERR_DUPLICATE_GROUP_NAME,
709 format!("duplicate group name: {n}"),
710 ),
711 RegistryClientError::Server(RegistryRpcError::SpawnRejected(d)) => (
712 NET_REGISTRY_ERR_SPAWN_REJECTED,
713 format!("spawn rejected: {d}"),
714 ),
715 RegistryClientError::Server(RegistryRpcError::SpawnNotSupported) => (
716 NET_REGISTRY_ERR_SPAWN_NOT_SUPPORTED,
717 "daemon is read-only (no spawn handler installed)".to_string(),
718 ),
719 RegistryClientError::Server(RegistryRpcError::UnknownGroup(g)) => (
720 NET_REGISTRY_ERR_UNKNOWN_GROUP,
721 format!("unknown group: {g}"),
722 ),
723 RegistryClientError::Server(RegistryRpcError::ScaleRejected(d)) => (
724 NET_REGISTRY_ERR_SCALE_REJECTED,
725 format!("scale rejected: {d}"),
726 ),
727 RegistryClientError::Server(RegistryRpcError::ScaleNotSupported) => (
728 NET_REGISTRY_ERR_SCALE_NOT_SUPPORTED,
729 "daemon doesn't accept dynamic scale (no scaler installed)".to_string(),
730 ),
731 }
732}
733
734fn store_error_detail(h: &RegistryClientHandle, detail: String) {
735 let c = match CString::new(detail) {
736 Ok(c) => c,
737 Err(_) => CString::new("invalid utf-8 in error detail").unwrap_or_default(),
738 };
739 *h.last_error_detail.lock() = Some(c);
740}
741
742fn groups_to_json(groups: &[RegistryGroupSummary]) -> String {
750 let wire: Vec<GroupWire<'_>> = groups.iter().map(GroupWire::from).collect();
751 serde_json::to_string(&wire).unwrap_or_else(|_| "[]".to_string())
752}
753
754fn group_to_json(g: &RegistryGroupSummary) -> String {
755 serde_json::to_string(&GroupWire::from(g)).unwrap_or_else(|_| "{}".to_string())
756}
757
758#[derive(serde::Serialize)]
759struct GroupWire<'a> {
760 name: &'a str,
761 group_seed_hex: String,
762 replicas: Vec<ReplicaWire<'a>>,
763}
764
765#[derive(serde::Serialize)]
766struct ReplicaWire<'a> {
767 generation: u64,
768 healthy: bool,
769 diagnostic: Option<&'a str>,
770 placement_node_id: Option<u64>,
771}
772
773impl<'a> From<&'a RegistryGroupSummary> for GroupWire<'a> {
774 fn from(g: &'a RegistryGroupSummary) -> Self {
775 Self {
776 name: g.name.as_str(),
777 group_seed_hex: hex::encode(g.group_seed),
778 replicas: g
779 .replicas
780 .iter()
781 .map(|r| ReplicaWire {
782 generation: r.generation,
783 healthy: r.healthy,
784 diagnostic: r.diagnostic.as_deref(),
785 placement_node_id: r.placement_node_id,
786 })
787 .collect(),
788 }
789 }
790}
791
792#[cfg(test)]
793mod tests {
794 use super::*;
795
796 #[test]
797 fn visibility_round_trips_through_raw() {
798 for (raw, expected) in [
799 (0, Visibility::Global),
800 (1, Visibility::ParentVisible),
801 (2, Visibility::Exported),
802 (3, Visibility::SubnetLocal),
803 ] {
804 let back = NetVisibility::from_raw(raw).expect("known discriminant");
805 assert_eq!(format!("{back:?}"), format!("{expected:?}"));
806 }
807 assert!(NetVisibility::from_raw(99).is_none());
808 assert!(NetVisibility::from_raw(-1).is_none());
809 }
810
811 #[test]
812 fn group_to_json_includes_every_documented_field() {
813 let g = RegistryGroupSummary {
814 name: "alpha".into(),
815 group_seed: [0xABu8; 32],
816 source_subnet: crate::adapter::net::subnet::SubnetId::GLOBAL,
817 fold_kinds: vec![0x0001],
818 replicas: vec![
819 crate::adapter::net::behavior::aggregator::RegistryReplicaSummary {
820 generation: 42,
821 healthy: true,
822 diagnostic: None,
823 placement_node_id: Some(0xBEEF),
824 },
825 crate::adapter::net::behavior::aggregator::RegistryReplicaSummary {
826 generation: 0,
827 healthy: false,
828 diagnostic: Some("stuck".into()),
829 placement_node_id: None,
830 },
831 ],
832 };
833 let json = group_to_json(&g);
834 assert!(json.contains("\"name\":\"alpha\""));
835 assert!(json.contains("\"group_seed_hex\":\"abababababababababababababababababababababababababababababababab\""));
838 assert!(json.contains("\"generation\":42"));
839 assert!(json.contains("\"healthy\":true"));
840 assert!(json.contains("\"diagnostic\":null"));
841 assert!(json.contains("\"placement_node_id\":48879"));
842 assert!(json.contains("\"healthy\":false"));
843 assert!(json.contains("\"diagnostic\":\"stuck\""));
844 assert!(json.contains("\"placement_node_id\":null"));
845 }
846
847 #[test]
848 fn summary_to_json_includes_every_documented_field() {
849 let s = SummaryAnnouncement {
850 fold_kind: 0x42,
851 source_subnet: crate::adapter::net::subnet::SubnetId::GLOBAL,
852 generation: 7,
853 buckets: vec![("alpha".into(), 1), ("beta".into(), 2)],
854 };
855 let json = summary_to_json(&s);
856 assert!(json.contains("\"fold_kind\":66"));
857 assert!(json.contains("\"source_subnet\":\"global\""));
858 assert!(json.contains("\"generation\":7"));
859 assert!(json.contains("\"name\":\"alpha\""));
860 assert!(json.contains("\"count\":1"));
861 assert!(json.contains("\"name\":\"beta\""));
862 assert!(json.contains("\"count\":2"));
863 }
864
865 #[test]
866 fn classify_fold_query_maps_every_variant() {
867 use crate::adapter::net::mesh_rpc::RpcError;
868 let transport = FoldQueryClientError::Transport(RpcError::NoRoute {
871 target: 0,
872 reason: String::new(),
873 });
874 assert_eq!(
875 classify_fold_query(&transport).0,
876 NET_REGISTRY_ERR_TRANSPORT
877 );
878
879 let codec = FoldQueryClientError::Codec("bad".into());
880 assert_eq!(classify_fold_query(&codec).0, NET_REGISTRY_ERR_CODEC);
881
882 let unknown_kind = FoldQueryClientError::Server(FoldQueryError::UnknownKind { kind: 0x42 });
883 let (kind_code, detail) = classify_fold_query(&unknown_kind);
884 assert_eq!(kind_code, NET_REGISTRY_ERR_UNKNOWN_KIND);
885 assert!(detail.contains("0x0042"));
886
887 let decode_failed =
888 FoldQueryClientError::Server(FoldQueryError::DecodeFailed("boom".into()));
889 assert_eq!(
890 classify_fold_query(&decode_failed).0,
891 NET_REGISTRY_ERR_CODEC,
892 );
893 }
894}