use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use crate::assessment::{AssessmentDiff, AssessmentReport};
use crate::assessment::mesh::{AssessmentMessage, MeshCoordinator, PeerAssessmentState};
use crate::error::{KernelError, KernelResult};
use crate::mesh_framing::{FrameType, MeshFrame};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssessmentEnvelope {
pub source_node: String,
pub sequence: u64,
pub message: AssessmentMessage,
}
impl AssessmentEnvelope {
pub fn to_bytes(&self) -> KernelResult<Vec<u8>> {
serde_json::to_vec(self)
.map_err(|e| KernelError::Mesh(format!("assessment envelope serialize: {e}")))
}
pub fn from_bytes(data: &[u8]) -> KernelResult<Self> {
serde_json::from_slice(data)
.map_err(|e| KernelError::Mesh(format!("assessment envelope deserialize: {e}")))
}
pub fn to_frame(&self) -> KernelResult<MeshFrame> {
let payload = self.to_bytes()?;
Ok(MeshFrame {
frame_type: FrameType::AssessmentSync,
payload,
})
}
pub fn from_frame(frame: &MeshFrame) -> KernelResult<Self> {
if frame.frame_type != FrameType::AssessmentSync {
return Err(KernelError::Mesh(format!(
"expected AssessmentSync frame, got {:?}",
frame.frame_type
)));
}
Self::from_bytes(&frame.payload)
}
}
pub struct AssessmentTransport {
coordinator: Arc<MeshCoordinator>,
next_seq: std::sync::atomic::AtomicU64,
latest_report: Mutex<Option<AssessmentReport>>,
}
impl AssessmentTransport {
pub fn new(coordinator: Arc<MeshCoordinator>) -> Self {
Self {
coordinator,
next_seq: std::sync::atomic::AtomicU64::new(1),
latest_report: Mutex::new(None),
}
}
pub fn coordinator(&self) -> &MeshCoordinator {
&self.coordinator
}
pub fn coordinator_arc(&self) -> Arc<MeshCoordinator> {
Arc::clone(&self.coordinator)
}
pub fn publish_report(&self, report: AssessmentReport) {
*self.latest_report.lock().unwrap() = Some(report);
}
pub fn latest_report(&self) -> Option<AssessmentReport> {
self.latest_report.lock().unwrap().clone()
}
pub fn build_gossip_frame(&self, report: &AssessmentReport) -> KernelResult<Vec<u8>> {
let msg = self.coordinator.build_gossip(report);
self.wrap_and_encode(msg)
}
pub fn build_broadcast_frame(&self, report: &AssessmentReport) -> KernelResult<Vec<u8>> {
let msg = self.coordinator.build_broadcast(report);
self.wrap_and_encode(msg)
}
pub fn build_diff_frame(
&self,
report: &AssessmentReport,
diff: &AssessmentDiff,
) -> KernelResult<Vec<u8>> {
let msg = self.coordinator.build_finding_diff(report, diff);
self.wrap_and_encode(msg)
}
pub fn build_request_frame(&self) -> KernelResult<Vec<u8>> {
let msg = AssessmentMessage::RequestReport {
requesting_node: self.coordinator.node_id().to_string(),
};
self.wrap_and_encode(msg)
}
pub fn build_full_report_frame(&self, report: &AssessmentReport) -> KernelResult<Vec<u8>> {
let msg = AssessmentMessage::FullReport {
report: report.clone(),
};
self.wrap_and_encode(msg)
}
pub fn drain_pending(&self) -> Option<Vec<u8>> {
let msg = self.coordinator.take_pending_broadcast()?;
match self.wrap_and_encode(msg) {
Ok(bytes) => Some(bytes),
Err(e) => {
warn!(error = %e, "failed to encode pending assessment broadcast");
None
}
}
}
pub fn try_extract_payload(data: &[u8]) -> KernelResult<Option<Vec<u8>>> {
if data.is_empty() {
return Ok(None);
}
if data[0] == b'{' {
return Ok(None);
}
if data.len() >= 5 {
let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
if len + 4 == data.len() && data[4] == FrameType::AssessmentSync as u8 {
let frame = MeshFrame::decode(&data[4..])
.map_err(|e| KernelError::Mesh(format!("assessment frame decode: {e}")))?;
return Ok(Some(frame.payload));
}
}
if data[0] == FrameType::AssessmentSync as u8 {
let frame = MeshFrame::decode(data)
.map_err(|e| KernelError::Mesh(format!("assessment frame decode: {e}")))?;
return Ok(Some(frame.payload));
}
Ok(None)
}
pub fn try_handle_raw(&self, data: &[u8]) -> KernelResult<Option<Option<Vec<u8>>>> {
match Self::try_extract_payload(data)? {
Some(payload) => {
let reply = self.handle_incoming(&payload)?;
Ok(Some(reply))
}
None => Ok(None),
}
}
pub fn handle_incoming(&self, frame_payload: &[u8]) -> KernelResult<Option<Vec<u8>>> {
let envelope = AssessmentEnvelope::from_bytes(frame_payload)?;
debug!(
from = %envelope.source_node,
seq = envelope.sequence,
"received assessment sync message"
);
if matches!(
&envelope.message,
AssessmentMessage::RequestReport { .. }
) {
if let Some(report) = self.latest_report() {
return Ok(Some(self.build_full_report_frame(&report)?));
}
return Ok(None);
}
let response = self.coordinator.handle_message(envelope.message);
match response {
Some(resp_msg) => {
let bytes = self.wrap_and_encode(resp_msg)?;
Ok(Some(bytes))
}
None => Ok(None),
}
}
pub fn gossip_tick(&self, latest_report: Option<&AssessmentReport>) -> Option<Vec<u8>> {
let report = latest_report?;
match self.build_gossip_frame(report) {
Ok(bytes) => Some(bytes),
Err(e) => {
warn!(error = %e, "failed to build gossip frame");
None
}
}
}
pub fn peer_states(&self) -> Vec<PeerAssessmentState> {
self.coordinator.peer_states()
}
fn next_sequence(&self) -> u64 {
self.next_seq
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
fn wrap_and_encode(&self, message: AssessmentMessage) -> KernelResult<Vec<u8>> {
let envelope = AssessmentEnvelope {
source_node: self.coordinator.node_id().to_string(),
sequence: self.next_sequence(),
message,
};
let frame = envelope.to_frame()?;
frame
.encode()
.map_err(|e| KernelError::Mesh(format!("frame encode: {e}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assessment::{AssessmentReport, AssessmentSummary};
use chrono::Utc;
fn make_coordinator(node_id: &str, project: &str) -> Arc<MeshCoordinator> {
Arc::new(MeshCoordinator::new(node_id.into(), project.into()))
}
fn make_report() -> AssessmentReport {
AssessmentReport {
timestamp: Utc::now(),
scope: "full".into(),
project: "/tmp/test-project".into(),
files_scanned: 100,
summary: AssessmentSummary {
total_files: 100,
coherence_score: 0.92,
rust_files: 60,
lines_of_code: 15_000,
..Default::default()
},
findings: vec![],
analyzers_run: vec!["complexity".into(), "security".into(), "topology".into()],
}
}
#[test]
fn envelope_serde_roundtrip() {
let env = AssessmentEnvelope {
source_node: "node-a".into(),
sequence: 42,
message: AssessmentMessage::Gossip {
node_id: "node-a".into(),
project_name: "proj-a".into(),
last_assessment: Some("2026-04-04T00:00:00Z".into()),
finding_count: 5,
analyzer_count: 3,
},
};
let bytes = env.to_bytes().unwrap();
let restored = AssessmentEnvelope::from_bytes(&bytes).unwrap();
assert_eq!(restored.source_node, "node-a");
assert_eq!(restored.sequence, 42);
}
#[test]
fn envelope_to_frame_uses_correct_type() {
let env = AssessmentEnvelope {
source_node: "n".into(),
sequence: 1,
message: AssessmentMessage::RequestReport {
requesting_node: "n".into(),
},
};
let frame = env.to_frame().unwrap();
assert_eq!(frame.frame_type, FrameType::AssessmentSync);
}
#[test]
fn envelope_from_frame_rejects_wrong_type() {
let frame = MeshFrame {
frame_type: FrameType::Heartbeat,
payload: vec![],
};
let err = AssessmentEnvelope::from_frame(&frame).unwrap_err();
assert!(err.to_string().contains("expected AssessmentSync"));
}
#[test]
fn build_gossip_frame_roundtrip() {
let coord = make_coordinator("node-a", "proj-a");
let transport = AssessmentTransport::new(coord.clone());
let report = make_report();
let bytes = transport.build_gossip_frame(&report).unwrap();
let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
assert_eq!(frame.frame_type, FrameType::AssessmentSync);
let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
assert_eq!(envelope.source_node, "node-a");
match &envelope.message {
AssessmentMessage::Gossip {
node_id,
project_name,
analyzer_count,
..
} => {
assert_eq!(node_id, "node-a");
assert_eq!(project_name, "proj-a");
assert_eq!(*analyzer_count, 3);
}
other => panic!("expected Gossip, got: {other:?}"),
}
}
#[test]
fn build_broadcast_frame_roundtrip() {
let coord = make_coordinator("node-b", "proj-b");
let transport = AssessmentTransport::new(coord);
let report = make_report();
let bytes = transport.build_broadcast_frame(&report).unwrap();
let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
match &envelope.message {
AssessmentMessage::ReportAvailable {
node_id,
files_scanned,
coherence_score,
..
} => {
assert_eq!(node_id, "node-b");
assert_eq!(*files_scanned, 100);
assert!(*coherence_score > 0.9);
}
other => panic!("expected ReportAvailable, got: {other:?}"),
}
}
#[test]
fn build_request_frame_roundtrip() {
let coord = make_coordinator("requester", "proj");
let transport = AssessmentTransport::new(coord);
let bytes = transport.build_request_frame().unwrap();
let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
match &envelope.message {
AssessmentMessage::RequestReport { requesting_node } => {
assert_eq!(requesting_node, "requester");
}
other => panic!("expected RequestReport, got: {other:?}"),
}
}
#[test]
fn build_full_report_frame_roundtrip() {
let coord = make_coordinator("responder", "proj");
let transport = AssessmentTransport::new(coord);
let report = make_report();
let bytes = transport.build_full_report_frame(&report).unwrap();
let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
match &envelope.message {
AssessmentMessage::FullReport { report: r } => {
assert_eq!(r.files_scanned, 100);
assert_eq!(r.analyzers_run.len(), 3);
}
other => panic!("expected FullReport, got: {other:?}"),
}
}
#[test]
fn handle_incoming_gossip_updates_coordinator() {
let coord_a = make_coordinator("node-a", "proj-a");
let transport_a = AssessmentTransport::new(coord_a.clone());
let report = make_report();
let gossip_bytes = transport_a.build_gossip_frame(&report).unwrap();
let len = u32::from_be_bytes([
gossip_bytes[0],
gossip_bytes[1],
gossip_bytes[2],
gossip_bytes[3],
]) as usize;
let frame = MeshFrame::decode(&gossip_bytes[4..4 + len]).unwrap();
let coord_b = make_coordinator("node-b", "proj-b");
let transport_b = AssessmentTransport::new(coord_b.clone());
let response = transport_b.handle_incoming(&frame.payload).unwrap();
assert!(response.is_none(), "gossip should not produce a response");
let peers = coord_b.peer_states();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].node_id, "node-a");
assert_eq!(peers[0].project_name, "proj-a");
assert_eq!(peers[0].analyzer_count, 3);
}
#[test]
fn handle_incoming_broadcast_updates_coordinator() {
let coord_a = make_coordinator("node-a", "proj-a");
let transport_a = AssessmentTransport::new(coord_a);
let report = make_report();
let broadcast_bytes = transport_a.build_broadcast_frame(&report).unwrap();
let len = u32::from_be_bytes([
broadcast_bytes[0],
broadcast_bytes[1],
broadcast_bytes[2],
broadcast_bytes[3],
]) as usize;
let frame = MeshFrame::decode(&broadcast_bytes[4..4 + len]).unwrap();
let coord_b = make_coordinator("node-b", "proj-b");
let transport_b = AssessmentTransport::new(coord_b.clone());
transport_b.handle_incoming(&frame.payload).unwrap();
let peers = coord_b.peer_states();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].finding_count, 0); }
#[test]
fn sequence_numbers_increment() {
let coord = make_coordinator("n", "p");
let transport = AssessmentTransport::new(coord);
let s1 = transport.next_sequence();
let s2 = transport.next_sequence();
let s3 = transport.next_sequence();
assert_eq!(s1, 1);
assert_eq!(s2, 2);
assert_eq!(s3, 3);
}
#[test]
fn drain_pending_returns_none_when_empty() {
let coord = make_coordinator("n", "p");
let transport = AssessmentTransport::new(coord);
assert!(transport.drain_pending().is_none());
}
#[test]
fn drain_pending_returns_bytes_when_set() {
let coord = make_coordinator("n", "p");
let report = make_report();
let gossip = coord.build_gossip(&report);
coord.set_pending_broadcast(gossip);
let transport = AssessmentTransport::new(coord);
let bytes = transport.drain_pending();
assert!(bytes.is_some());
assert!(transport.drain_pending().is_none());
}
#[test]
fn gossip_tick_with_report_produces_frame() {
let coord = make_coordinator("n", "p");
let transport = AssessmentTransport::new(coord);
let report = make_report();
let bytes = transport.gossip_tick(Some(&report));
assert!(bytes.is_some());
}
#[test]
fn gossip_tick_without_report_returns_none() {
let coord = make_coordinator("n", "p");
let transport = AssessmentTransport::new(coord);
assert!(transport.gossip_tick(None).is_none());
}
#[test]
fn peer_states_empty_initially() {
let coord = make_coordinator("n", "p");
let transport = AssessmentTransport::new(coord);
assert!(transport.peer_states().is_empty());
}
#[test]
fn two_node_assessment_exchange() {
let coord_a = make_coordinator("node-a", "proj-a");
let coord_b = make_coordinator("node-b", "proj-b");
let transport_a = AssessmentTransport::new(coord_a.clone());
let transport_b = AssessmentTransport::new(coord_b.clone());
let report_a = make_report();
let broadcast = transport_a.build_broadcast_frame(&report_a).unwrap();
let len =
u32::from_be_bytes([broadcast[0], broadcast[1], broadcast[2], broadcast[3]]) as usize;
let frame = MeshFrame::decode(&broadcast[4..4 + len]).unwrap();
transport_b.handle_incoming(&frame.payload).unwrap();
let gossip = transport_a.build_gossip_frame(&report_a).unwrap();
let len = u32::from_be_bytes([gossip[0], gossip[1], gossip[2], gossip[3]]) as usize;
let frame = MeshFrame::decode(&gossip[4..4 + len]).unwrap();
transport_b.handle_incoming(&frame.payload).unwrap();
let peers = transport_b.peer_states();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].node_id, "node-a");
assert_eq!(peers[0].project_name, "proj-a");
assert_eq!(peers[0].analyzer_count, 3);
let req = transport_b.build_request_frame().unwrap();
let len = u32::from_be_bytes([req[0], req[1], req[2], req[3]]) as usize;
let frame = MeshFrame::decode(&req[4..4 + len]).unwrap();
let response = transport_a.handle_incoming(&frame.payload).unwrap();
assert!(response.is_none());
let full = transport_a.build_full_report_frame(&report_a).unwrap();
let len = u32::from_be_bytes([full[0], full[1], full[2], full[3]]) as usize;
let frame = MeshFrame::decode(&full[4..4 + len]).unwrap();
let resp = transport_b.handle_incoming(&frame.payload).unwrap();
assert!(resp.is_none()); }
fn make_finding(file: &str, msg: &str) -> crate::assessment::Finding {
crate::assessment::Finding {
severity: "warning".into(),
category: "size".into(),
file: file.into(),
line: Some(1),
message: msg.into(),
}
}
#[test]
fn build_diff_frame_only_changed_findings() {
use crate::assessment::analyzer::diff_reports;
let coord = make_coordinator("node-a", "proj-a");
let transport = AssessmentTransport::new(coord);
let mut prev = make_report();
prev.findings = vec![make_finding("a.rs", "old")];
let mut curr = make_report();
curr.findings = vec![
make_finding("a.rs", "old"),
make_finding("b.rs", "new issue"),
];
let diff = diff_reports(&curr, &prev);
assert_eq!(diff.findings_new.len(), 1);
assert!(diff.findings_resolved.is_empty());
let bytes = transport.build_diff_frame(&curr, &diff).unwrap();
let len = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
let frame = MeshFrame::decode(&bytes[4..4 + len]).unwrap();
let envelope = AssessmentEnvelope::from_bytes(&frame.payload).unwrap();
match envelope.message {
AssessmentMessage::FindingDiff {
findings_new,
findings_resolved,
finding_count,
..
} => {
assert_eq!(findings_new.len(), 1);
assert_eq!(findings_new[0].file, "b.rs");
assert!(findings_resolved.is_empty());
assert_eq!(finding_count, 2);
}
other => panic!("expected FindingDiff, got {other:?}"),
}
}
#[test]
fn try_extract_payload_from_full_encode_and_type_prefix() {
let coord = make_coordinator("n", "p");
let transport = AssessmentTransport::new(coord);
let report = make_report();
let full = transport.build_gossip_frame(&report).unwrap();
let payload = AssessmentTransport::try_extract_payload(&full)
.unwrap()
.expect("full encode should extract");
assert!(!payload.is_empty());
let len = u32::from_be_bytes([full[0], full[1], full[2], full[3]]) as usize;
let type_prefixed = &full[4..4 + len];
let payload2 = AssessmentTransport::try_extract_payload(type_prefixed)
.unwrap()
.expect("type-prefix should extract");
assert_eq!(payload, payload2);
assert!(
AssessmentTransport::try_extract_payload(br#"{"source_node":"x"}"#)
.unwrap()
.is_none()
);
}
#[test]
fn queue_mesh_propagation_prefers_finding_diff() {
use crate::assessment::AssessmentService;
use std::io::Write;
let coord = make_coordinator("node-a", "proj-a");
let svc = AssessmentService::with_mesh_coordinator(coord.clone());
let mut prev = make_report();
prev.findings = vec![make_finding("a.rs", "old")];
let dir = tempfile::tempdir().unwrap();
let prev_path = dir.path().join("prev.json");
let mut f = std::fs::File::create(&prev_path).unwrap();
f.write_all(serde_json::to_string(&prev).unwrap().as_bytes())
.unwrap();
svc.set_previous_report_path(prev_path);
let mut curr = make_report();
curr.findings = vec![
make_finding("a.rs", "old"),
make_finding("b.rs", "new"),
];
svc.queue_mesh_propagation(&curr);
let pending = coord.take_pending_broadcast().expect("pending set");
match pending {
AssessmentMessage::FindingDiff {
findings_new,
findings_resolved,
..
} => {
assert_eq!(findings_new.len(), 1);
assert!(findings_resolved.is_empty());
}
other => panic!("expected FindingDiff, got {other:?}"),
}
}
#[tokio::test]
async fn tcp_assessment_sync_between_two_nodes() {
use crate::mesh::MeshTransport;
use crate::mesh_tcp::TcpTransport;
let transport_tcp = TcpTransport;
let mut listener = transport_tcp.listen("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let coord_a = make_coordinator("node-a", "proj-a");
let assess_a = AssessmentTransport::new(coord_a);
let report_a = make_report();
let coord_b = make_coordinator("node-b", "proj-b");
let gossip_bytes = assess_a.build_gossip_frame(&report_a).unwrap();
let node_a_task = tokio::spawn(async move {
let mut stream = TcpTransport.connect(&addr.to_string()).await.unwrap();
stream.send(&gossip_bytes).await.unwrap();
stream.close().await.unwrap();
});
let (mut stream, _) = listener.accept().await.unwrap();
let data = stream.recv().await.unwrap();
let len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
let frame = MeshFrame::decode(&data[4..4 + len]).unwrap();
assert_eq!(frame.frame_type, FrameType::AssessmentSync);
let assess_b = AssessmentTransport::new(coord_b.clone());
let resp = assess_b.handle_incoming(&frame.payload).unwrap();
assert!(resp.is_none());
let peers = coord_b.peer_states();
assert_eq!(peers.len(), 1);
assert_eq!(peers[0].node_id, "node-a");
node_a_task.await.unwrap();
}
}