1use std::sync::{Arc, Mutex};
33
34use serde::{Deserialize, Serialize};
35use tracing::{debug, warn};
36
37use crate::assessment::{AssessmentDiff, AssessmentReport};
38use crate::assessment::mesh::{AssessmentMessage, MeshCoordinator, PeerAssessmentState};
39use crate::error::{KernelError, KernelResult};
40use crate::mesh_framing::{FrameType, MeshFrame};
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct AssessmentEnvelope {
49 pub source_node: String,
51 pub sequence: u64,
53 pub message: AssessmentMessage,
55}
56
57impl AssessmentEnvelope {
58 pub fn to_bytes(&self) -> KernelResult<Vec<u8>> {
60 serde_json::to_vec(self)
61 .map_err(|e| KernelError::Mesh(format!("assessment envelope serialize: {e}")))
62 }
63
64 pub fn from_bytes(data: &[u8]) -> KernelResult<Self> {
66 serde_json::from_slice(data)
67 .map_err(|e| KernelError::Mesh(format!("assessment envelope deserialize: {e}")))
68 }
69
70 pub fn to_frame(&self) -> KernelResult<MeshFrame> {
72 let payload = self.to_bytes()?;
73 Ok(MeshFrame {
74 frame_type: FrameType::AssessmentSync,
75 payload,
76 })
77 }
78
79 pub fn from_frame(frame: &MeshFrame) -> KernelResult<Self> {
84 if frame.frame_type != FrameType::AssessmentSync {
85 return Err(KernelError::Mesh(format!(
86 "expected AssessmentSync frame, got {:?}",
87 frame.frame_type
88 )));
89 }
90 Self::from_bytes(&frame.payload)
91 }
92}
93
94pub struct AssessmentTransport {
107 coordinator: Arc<MeshCoordinator>,
109 next_seq: std::sync::atomic::AtomicU64,
111 latest_report: Mutex<Option<AssessmentReport>>,
113}
114
115impl AssessmentTransport {
116 pub fn new(coordinator: Arc<MeshCoordinator>) -> Self {
118 Self {
119 coordinator,
120 next_seq: std::sync::atomic::AtomicU64::new(1),
121 latest_report: Mutex::new(None),
122 }
123 }
124
125 pub fn coordinator(&self) -> &MeshCoordinator {
127 &self.coordinator
128 }
129
130 pub fn coordinator_arc(&self) -> Arc<MeshCoordinator> {
132 Arc::clone(&self.coordinator)
133 }
134
135 pub fn publish_report(&self, report: AssessmentReport) {
137 *self.latest_report.lock().unwrap() = Some(report);
138 }
139
140 pub fn latest_report(&self) -> Option<AssessmentReport> {
142 self.latest_report.lock().unwrap().clone()
143 }
144
145 pub fn build_gossip_frame(&self, report: &AssessmentReport) -> KernelResult<Vec<u8>> {
151 let msg = self.coordinator.build_gossip(report);
152 self.wrap_and_encode(msg)
153 }
154
155 pub fn build_broadcast_frame(&self, report: &AssessmentReport) -> KernelResult<Vec<u8>> {
157 let msg = self.coordinator.build_broadcast(report);
158 self.wrap_and_encode(msg)
159 }
160
161 pub fn build_diff_frame(
163 &self,
164 report: &AssessmentReport,
165 diff: &AssessmentDiff,
166 ) -> KernelResult<Vec<u8>> {
167 let msg = self.coordinator.build_finding_diff(report, diff);
168 self.wrap_and_encode(msg)
169 }
170
171 pub fn build_request_frame(&self) -> KernelResult<Vec<u8>> {
173 let msg = AssessmentMessage::RequestReport {
174 requesting_node: self.coordinator.node_id().to_string(),
175 };
176 self.wrap_and_encode(msg)
177 }
178
179 pub fn build_full_report_frame(&self, report: &AssessmentReport) -> KernelResult<Vec<u8>> {
181 let msg = AssessmentMessage::FullReport {
182 report: report.clone(),
183 };
184 self.wrap_and_encode(msg)
185 }
186
187 pub fn drain_pending(&self) -> Option<Vec<u8>> {
190 let msg = self.coordinator.take_pending_broadcast()?;
191 match self.wrap_and_encode(msg) {
192 Ok(bytes) => Some(bytes),
193 Err(e) => {
194 warn!(error = %e, "failed to encode pending assessment broadcast");
195 None
196 }
197 }
198 }
199
200 pub fn try_extract_payload(data: &[u8]) -> KernelResult<Option<Vec<u8>>> {
210 if data.is_empty() {
211 return Ok(None);
212 }
213 if data[0] == b'{' {
215 return Ok(None);
216 }
217 if data.len() >= 5 {
219 let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
220 if len + 4 == data.len() && data[4] == FrameType::AssessmentSync as u8 {
221 let frame = MeshFrame::decode(&data[4..])
222 .map_err(|e| KernelError::Mesh(format!("assessment frame decode: {e}")))?;
223 return Ok(Some(frame.payload));
224 }
225 }
226 if data[0] == FrameType::AssessmentSync as u8 {
228 let frame = MeshFrame::decode(data)
229 .map_err(|e| KernelError::Mesh(format!("assessment frame decode: {e}")))?;
230 return Ok(Some(frame.payload));
231 }
232 Ok(None)
233 }
234
235 pub fn try_handle_raw(&self, data: &[u8]) -> KernelResult<Option<Option<Vec<u8>>>> {
243 match Self::try_extract_payload(data)? {
244 Some(payload) => {
245 let reply = self.handle_incoming(&payload)?;
246 Ok(Some(reply))
247 }
248 None => Ok(None),
249 }
250 }
251
252 pub fn handle_incoming(&self, frame_payload: &[u8]) -> KernelResult<Option<Vec<u8>>> {
259 let envelope = AssessmentEnvelope::from_bytes(frame_payload)?;
260 debug!(
261 from = %envelope.source_node,
262 seq = envelope.sequence,
263 "received assessment sync message"
264 );
265
266 if matches!(
269 &envelope.message,
270 AssessmentMessage::RequestReport { .. }
271 ) {
272 if let Some(report) = self.latest_report() {
273 return Ok(Some(self.build_full_report_frame(&report)?));
274 }
275 return Ok(None);
276 }
277
278 let response = self.coordinator.handle_message(envelope.message);
279 match response {
280 Some(resp_msg) => {
281 let bytes = self.wrap_and_encode(resp_msg)?;
282 Ok(Some(bytes))
283 }
284 None => Ok(None),
285 }
286 }
287
288 pub fn gossip_tick(&self, latest_report: Option<&AssessmentReport>) -> Option<Vec<u8>> {
299 let report = latest_report?;
300 match self.build_gossip_frame(report) {
301 Ok(bytes) => Some(bytes),
302 Err(e) => {
303 warn!(error = %e, "failed to build gossip frame");
304 None
305 }
306 }
307 }
308
309 pub fn peer_states(&self) -> Vec<PeerAssessmentState> {
313 self.coordinator.peer_states()
314 }
315
316 fn next_sequence(&self) -> u64 {
319 self.next_seq
320 .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
321 }
322
323 fn wrap_and_encode(&self, message: AssessmentMessage) -> KernelResult<Vec<u8>> {
324 let envelope = AssessmentEnvelope {
325 source_node: self.coordinator.node_id().to_string(),
326 sequence: self.next_sequence(),
327 message,
328 };
329 let frame = envelope.to_frame()?;
330 frame
331 .encode()
332 .map_err(|e| KernelError::Mesh(format!("frame encode: {e}")))
333 }
334}
335
336#[cfg(test)]
339mod tests {
340 use super::*;
341 use crate::assessment::{AssessmentReport, AssessmentSummary};
342 use chrono::Utc;
343
344 fn make_coordinator(node_id: &str, project: &str) -> Arc<MeshCoordinator> {
345 Arc::new(MeshCoordinator::new(node_id.into(), project.into()))
346 }
347
348 fn make_report() -> AssessmentReport {
349 AssessmentReport {
350 timestamp: Utc::now(),
351 scope: "full".into(),
352 project: "/tmp/test-project".into(),
353 files_scanned: 100,
354 summary: AssessmentSummary {
355 total_files: 100,
356 coherence_score: 0.92,
357 rust_files: 60,
358 lines_of_code: 15_000,
359 ..Default::default()
360 },
361 findings: vec![],
362 analyzers_run: vec!["complexity".into(), "security".into(), "topology".into()],
363 }
364 }
365
366 #[test]
367 fn envelope_serde_roundtrip() {
368 let env = AssessmentEnvelope {
369 source_node: "node-a".into(),
370 sequence: 42,
371 message: AssessmentMessage::Gossip {
372 node_id: "node-a".into(),
373 project_name: "proj-a".into(),
374 last_assessment: Some("2026-04-04T00:00:00Z".into()),
375 finding_count: 5,
376 analyzer_count: 3,
377 },
378 };
379 let bytes = env.to_bytes().unwrap();
380 let restored = AssessmentEnvelope::from_bytes(&bytes).unwrap();
381 assert_eq!(restored.source_node, "node-a");
382 assert_eq!(restored.sequence, 42);
383 }
384
385 #[test]
386 fn envelope_to_frame_uses_correct_type() {
387 let env = AssessmentEnvelope {
388 source_node: "n".into(),
389 sequence: 1,
390 message: AssessmentMessage::RequestReport {
391 requesting_node: "n".into(),
392 },
393 };
394 let frame = env.to_frame().unwrap();
395 assert_eq!(frame.frame_type, FrameType::AssessmentSync);
396 }
397
398 #[test]
399 fn envelope_from_frame_rejects_wrong_type() {
400 let frame = MeshFrame {
401 frame_type: FrameType::Heartbeat,
402 payload: vec![],
403 };
404 let err = AssessmentEnvelope::from_frame(&frame).unwrap_err();
405 assert!(err.to_string().contains("expected AssessmentSync"));
406 }
407
408 #[test]
409 fn build_gossip_frame_roundtrip() {
410 let coord = make_coordinator("node-a", "proj-a");
411 let transport = AssessmentTransport::new(coord.clone());
412 let report = make_report();
413
414 let bytes = transport.build_gossip_frame(&report).unwrap();
415 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
417 let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
418 assert_eq!(frame.frame_type, FrameType::AssessmentSync);
419
420 let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
421 assert_eq!(envelope.source_node, "node-a");
422 match &envelope.message {
423 AssessmentMessage::Gossip {
424 node_id,
425 project_name,
426 analyzer_count,
427 ..
428 } => {
429 assert_eq!(node_id, "node-a");
430 assert_eq!(project_name, "proj-a");
431 assert_eq!(*analyzer_count, 3);
432 }
433 other => panic!("expected Gossip, got: {other:?}"),
434 }
435 }
436
437 #[test]
438 fn build_broadcast_frame_roundtrip() {
439 let coord = make_coordinator("node-b", "proj-b");
440 let transport = AssessmentTransport::new(coord);
441 let report = make_report();
442
443 let bytes = transport.build_broadcast_frame(&report).unwrap();
444 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
445 let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
446 let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
447
448 match &envelope.message {
449 AssessmentMessage::ReportAvailable {
450 node_id,
451 files_scanned,
452 coherence_score,
453 ..
454 } => {
455 assert_eq!(node_id, "node-b");
456 assert_eq!(*files_scanned, 100);
457 assert!(*coherence_score > 0.9);
458 }
459 other => panic!("expected ReportAvailable, got: {other:?}"),
460 }
461 }
462
463 #[test]
464 fn build_request_frame_roundtrip() {
465 let coord = make_coordinator("requester", "proj");
466 let transport = AssessmentTransport::new(coord);
467
468 let bytes = transport.build_request_frame().unwrap();
469 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
470 let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
471 let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
472
473 match &envelope.message {
474 AssessmentMessage::RequestReport { requesting_node } => {
475 assert_eq!(requesting_node, "requester");
476 }
477 other => panic!("expected RequestReport, got: {other:?}"),
478 }
479 }
480
481 #[test]
482 fn build_full_report_frame_roundtrip() {
483 let coord = make_coordinator("responder", "proj");
484 let transport = AssessmentTransport::new(coord);
485 let report = make_report();
486
487 let bytes = transport.build_full_report_frame(&report).unwrap();
488 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
489 let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
490 let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
491
492 match &envelope.message {
493 AssessmentMessage::FullReport { report: r } => {
494 assert_eq!(r.files_scanned, 100);
495 assert_eq!(r.analyzers_run.len(), 3);
496 }
497 other => panic!("expected FullReport, got: {other:?}"),
498 }
499 }
500
501 #[test]
502 fn handle_incoming_gossip_updates_coordinator() {
503 let coord_a = make_coordinator("node-a", "proj-a");
504 let transport_a = AssessmentTransport::new(coord_a.clone());
505 let report = make_report();
506
507 let gossip_bytes = transport_a.build_gossip_frame(&report).unwrap();
509
510 let len = u32::from_be_bytes([
512 gossip_bytes[0],
513 gossip_bytes[1],
514 gossip_bytes[2],
515 gossip_bytes[3],
516 ]) as usize;
517 let frame = MeshFrame::decode(&gossip_bytes[4..4 + len]).unwrap();
518
519 let coord_b = make_coordinator("node-b", "proj-b");
521 let transport_b = AssessmentTransport::new(coord_b.clone());
522 let response = transport_b.handle_incoming(&frame.payload).unwrap();
523 assert!(response.is_none(), "gossip should not produce a response");
524
525 let peers = coord_b.peer_states();
527 assert_eq!(peers.len(), 1);
528 assert_eq!(peers[0].node_id, "node-a");
529 assert_eq!(peers[0].project_name, "proj-a");
530 assert_eq!(peers[0].analyzer_count, 3);
531 }
532
533 #[test]
534 fn handle_incoming_broadcast_updates_coordinator() {
535 let coord_a = make_coordinator("node-a", "proj-a");
536 let transport_a = AssessmentTransport::new(coord_a);
537 let report = make_report();
538
539 let broadcast_bytes = transport_a.build_broadcast_frame(&report).unwrap();
540 let len = u32::from_be_bytes([
541 broadcast_bytes[0],
542 broadcast_bytes[1],
543 broadcast_bytes[2],
544 broadcast_bytes[3],
545 ]) as usize;
546 let frame = MeshFrame::decode(&broadcast_bytes[4..4 + len]).unwrap();
547
548 let coord_b = make_coordinator("node-b", "proj-b");
549 let transport_b = AssessmentTransport::new(coord_b.clone());
550 transport_b.handle_incoming(&frame.payload).unwrap();
551
552 let peers = coord_b.peer_states();
553 assert_eq!(peers.len(), 1);
554 assert_eq!(peers[0].finding_count, 0); }
556
557 #[test]
558 fn sequence_numbers_increment() {
559 let coord = make_coordinator("n", "p");
560 let transport = AssessmentTransport::new(coord);
561
562 let s1 = transport.next_sequence();
563 let s2 = transport.next_sequence();
564 let s3 = transport.next_sequence();
565
566 assert_eq!(s1, 1);
567 assert_eq!(s2, 2);
568 assert_eq!(s3, 3);
569 }
570
571 #[test]
572 fn drain_pending_returns_none_when_empty() {
573 let coord = make_coordinator("n", "p");
574 let transport = AssessmentTransport::new(coord);
575 assert!(transport.drain_pending().is_none());
576 }
577
578 #[test]
579 fn drain_pending_returns_bytes_when_set() {
580 let coord = make_coordinator("n", "p");
581 let report = make_report();
582 let gossip = coord.build_gossip(&report);
583 coord.set_pending_broadcast(gossip);
584
585 let transport = AssessmentTransport::new(coord);
586 let bytes = transport.drain_pending();
587 assert!(bytes.is_some());
588 assert!(transport.drain_pending().is_none());
590 }
591
592 #[test]
593 fn gossip_tick_with_report_produces_frame() {
594 let coord = make_coordinator("n", "p");
595 let transport = AssessmentTransport::new(coord);
596 let report = make_report();
597
598 let bytes = transport.gossip_tick(Some(&report));
599 assert!(bytes.is_some());
600 }
601
602 #[test]
603 fn gossip_tick_without_report_returns_none() {
604 let coord = make_coordinator("n", "p");
605 let transport = AssessmentTransport::new(coord);
606 assert!(transport.gossip_tick(None).is_none());
607 }
608
609 #[test]
610 fn peer_states_empty_initially() {
611 let coord = make_coordinator("n", "p");
612 let transport = AssessmentTransport::new(coord);
613 assert!(transport.peer_states().is_empty());
614 }
615
616 #[test]
617 fn two_node_assessment_exchange() {
618 let coord_a = make_coordinator("node-a", "proj-a");
620 let coord_b = make_coordinator("node-b", "proj-b");
621 let transport_a = AssessmentTransport::new(coord_a.clone());
622 let transport_b = AssessmentTransport::new(coord_b.clone());
623
624 let report_a = make_report();
625
626 let broadcast = transport_a.build_broadcast_frame(&report_a).unwrap();
628 let len =
629 u32::from_be_bytes([broadcast[0], broadcast[1], broadcast[2], broadcast[3]]) as usize;
630 let frame = MeshFrame::decode(&broadcast[4..4 + len]).unwrap();
631 transport_b.handle_incoming(&frame.payload).unwrap();
632
633 let gossip = transport_a.build_gossip_frame(&report_a).unwrap();
635 let len = u32::from_be_bytes([gossip[0], gossip[1], gossip[2], gossip[3]]) as usize;
636 let frame = MeshFrame::decode(&gossip[4..4 + len]).unwrap();
637 transport_b.handle_incoming(&frame.payload).unwrap();
638
639 let peers = transport_b.peer_states();
641 assert_eq!(peers.len(), 1);
642 assert_eq!(peers[0].node_id, "node-a");
643 assert_eq!(peers[0].project_name, "proj-a");
644 assert_eq!(peers[0].analyzer_count, 3);
645
646 let req = transport_b.build_request_frame().unwrap();
648 let len = u32::from_be_bytes([req[0], req[1], req[2], req[3]]) as usize;
649 let frame = MeshFrame::decode(&req[4..4 + len]).unwrap();
650 let response = transport_a.handle_incoming(&frame.payload).unwrap();
653 assert!(response.is_none());
654
655 let full = transport_a.build_full_report_frame(&report_a).unwrap();
657 let len = u32::from_be_bytes([full[0], full[1], full[2], full[3]]) as usize;
658 let frame = MeshFrame::decode(&full[4..4 + len]).unwrap();
659 let resp = transport_b.handle_incoming(&frame.payload).unwrap();
660 assert!(resp.is_none()); }
662
663 fn make_finding(file: &str, msg: &str) -> crate::assessment::Finding {
664 crate::assessment::Finding {
665 severity: "warning".into(),
666 category: "size".into(),
667 file: file.into(),
668 line: Some(1),
669 message: msg.into(),
670 }
671 }
672
673 #[test]
674 fn build_diff_frame_only_changed_findings() {
675 use crate::assessment::analyzer::diff_reports;
676
677 let coord = make_coordinator("node-a", "proj-a");
678 let transport = AssessmentTransport::new(coord);
679 let mut prev = make_report();
680 prev.findings = vec![make_finding("a.rs", "old")];
681 let mut curr = make_report();
682 curr.findings = vec![
683 make_finding("a.rs", "old"),
684 make_finding("b.rs", "new issue"),
685 ];
686 let diff = diff_reports(&curr, &prev);
687 assert_eq!(diff.findings_new.len(), 1);
688 assert!(diff.findings_resolved.is_empty());
689
690 let bytes = transport.build_diff_frame(&curr, &diff).unwrap();
691 let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
692 let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
693 let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
694 match envelope.message {
695 AssessmentMessage::FindingDiff {
696 findings_new,
697 findings_resolved,
698 finding_count,
699 ..
700 } => {
701 assert_eq!(findings_new.len(), 1);
702 assert_eq!(findings_new[0].file, "b.rs");
703 assert!(findings_resolved.is_empty());
704 assert_eq!(finding_count, 2);
705 }
706 other => panic!("expected FindingDiff, got {other:?}"),
707 }
708 }
709
710 #[test]
711 fn try_extract_payload_from_full_encode_and_type_prefix() {
712 let coord = make_coordinator("n", "p");
713 let transport = AssessmentTransport::new(coord);
714 let report = make_report();
715 let full = transport.build_gossip_frame(&report).unwrap();
716 let payload = AssessmentTransport::try_extract_payload(&full)
717 .unwrap()
718 .expect("full encode should extract");
719 assert!(!payload.is_empty());
720
721 let len = u32::from_be_bytes([full[0], full[1], full[2], full[3]]) as usize;
723 let type_prefixed = &full[4..4 + len];
724 let payload2 = AssessmentTransport::try_extract_payload(type_prefixed)
725 .unwrap()
726 .expect("type-prefix should extract");
727 assert_eq!(payload, payload2);
728
729 assert!(
731 AssessmentTransport::try_extract_payload(br#"{"source_node":"x"}"#)
732 .unwrap()
733 .is_none()
734 );
735 }
736
737 #[test]
738 fn queue_mesh_propagation_prefers_finding_diff() {
739 use crate::assessment::AssessmentService;
740 use std::io::Write;
741
742 let coord = make_coordinator("node-a", "proj-a");
743 let svc = AssessmentService::with_mesh_coordinator(coord.clone());
744
745 let mut prev = make_report();
746 prev.findings = vec![make_finding("a.rs", "old")];
747 let dir = tempfile::tempdir().unwrap();
748 let prev_path = dir.path().join("prev.json");
749 let mut f = std::fs::File::create(&prev_path).unwrap();
750 f.write_all(serde_json::to_string(&prev).unwrap().as_bytes())
751 .unwrap();
752 svc.set_previous_report_path(prev_path);
753
754 let mut curr = make_report();
755 curr.findings = vec![
756 make_finding("a.rs", "old"),
757 make_finding("b.rs", "new"),
758 ];
759 svc.queue_mesh_propagation(&curr);
760
761 let pending = coord.take_pending_broadcast().expect("pending set");
762 match pending {
763 AssessmentMessage::FindingDiff {
764 findings_new,
765 findings_resolved,
766 ..
767 } => {
768 assert_eq!(findings_new.len(), 1);
769 assert!(findings_resolved.is_empty());
770 }
771 other => panic!("expected FindingDiff, got {other:?}"),
772 }
773 }
774
775 #[tokio::test]
778 async fn tcp_assessment_sync_between_two_nodes() {
779 use crate::mesh::MeshTransport;
780 use crate::mesh_tcp::TcpTransport;
781
782 let transport_tcp = TcpTransport;
783 let mut listener = transport_tcp.listen("127.0.0.1:0").await.unwrap();
784 let addr = listener.local_addr().unwrap();
785
786 let coord_a = make_coordinator("node-a", "proj-a");
787 let assess_a = AssessmentTransport::new(coord_a);
788 let report_a = make_report();
789
790 let coord_b = make_coordinator("node-b", "proj-b");
791
792 let gossip_bytes = assess_a.build_gossip_frame(&report_a).unwrap();
794 let node_a_task = tokio::spawn(async move {
795 let mut stream = TcpTransport.connect(&addr.to_string()).await.unwrap();
796 stream.send(&gossip_bytes).await.unwrap();
800 stream.close().await.unwrap();
801 });
802
803 let (mut stream, _) = listener.accept().await.unwrap();
805 let data = stream.recv().await.unwrap();
806
807 let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
809 let frame = MeshFrame::decode(&data[4..4 + len]).unwrap();
810 assert_eq!(frame.frame_type, FrameType::AssessmentSync);
811
812 let assess_b = AssessmentTransport::new(coord_b.clone());
814 let resp = assess_b.handle_incoming(&frame.payload).unwrap();
815 assert!(resp.is_none());
816
817 let peers = coord_b.peer_states();
819 assert_eq!(peers.len(), 1);
820 assert_eq!(peers[0].node_id, "node-a");
821
822 node_a_task.await.unwrap();
823 }
824}