use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Heartbeat {
pub pc_id: String,
pub at: chrono::DateTime<chrono::Utc>,
pub agent_version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub os_family: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
fn heartbeat_round_trips_through_json() {
let hb = Heartbeat {
pc_id: "minipc".into(),
at: chrono::Utc.with_ymd_and_hms(2026, 5, 16, 0, 0, 0).unwrap(),
agent_version: "0.12.0".into(),
hostname: Some("MINIPC".into()),
os_family: Some("windows".into()),
};
let json = serde_json::to_string(&hb).unwrap();
let back: Heartbeat = serde_json::from_str(&json).unwrap();
assert_eq!(back.pc_id, hb.pc_id);
assert_eq!(back.at, hb.at);
assert_eq!(back.agent_version, hb.agent_version);
assert_eq!(back.hostname, hb.hostname);
assert_eq!(back.os_family, hb.os_family);
}
#[test]
fn heartbeat_without_enrichment_still_decodes() {
let json = r#"{"pc_id":"x","at":"2026-05-16T00:00:00Z","agent_version":"0.11.5"}"#;
let hb: Heartbeat = serde_json::from_str(json).unwrap();
assert_eq!(hb.pc_id, "x");
assert_eq!(hb.hostname, None);
assert_eq!(hb.os_family, None);
}
}