use khive_types::Timestamp;
use serde::{Deserialize, Serialize};
pub const CURRENT_SCHEMA_VERSION: u32 = 1;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Envelope {
pub schema_version: u32,
pub producer: String,
pub producer_model_family: String,
pub staged_at: Timestamp,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub batch_id: Option<String>,
}
impl Envelope {
pub fn new(
producer: impl Into<String>,
producer_model_family: impl Into<String>,
staged_at: Timestamp,
) -> Self {
Self {
schema_version: CURRENT_SCHEMA_VERSION,
producer: producer.into(),
producer_model_family: producer_model_family.into(),
staged_at,
batch_id: None,
}
}
pub fn with_batch_id(mut self, batch_id: impl Into<String>) -> Self {
self.batch_id = Some(batch_id.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_stamps_current_schema_version() {
let env = Envelope::new("agent:test", "family:sonnet", Timestamp::from_secs(1));
assert_eq!(env.schema_version, CURRENT_SCHEMA_VERSION);
assert_eq!(env.producer, "agent:test");
assert_eq!(env.producer_model_family, "family:sonnet");
}
#[test]
fn rejects_unknown_field() {
let json = serde_json::json!({
"schema_version": 1,
"producer": "agent:test",
"producer_model_family": "family:sonnet",
"staged_at": 1_000_000_u64,
"unexpected": "surprise"
});
let result: Result<Envelope, _> = serde_json::from_value(json);
assert!(result.is_err());
}
#[test]
fn batch_id_absent_by_default_and_not_serialized() {
let env = Envelope::new("agent:test", "family:sonnet", Timestamp::from_secs(1));
assert_eq!(env.batch_id, None);
let json = serde_json::to_string(&env).unwrap();
assert!(
!json.contains("batch_id"),
"absent batch_id must not appear on the wire at all (not even as null): {json}"
);
}
#[test]
fn batch_id_round_trips_when_present() {
let env = Envelope::new("agent:test", "family:sonnet", Timestamp::from_secs(1))
.with_batch_id("batch-123");
let json = serde_json::to_string(&env).unwrap();
let decoded: Envelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, env);
assert_eq!(decoded.batch_id.as_deref(), Some("batch-123"));
}
#[test]
fn batch_id_round_trips_when_absent() {
let env = Envelope::new("agent:test", "family:sonnet", Timestamp::from_secs(1));
let json = serde_json::to_string(&env).unwrap();
let decoded: Envelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, env);
assert_eq!(decoded.batch_id, None);
}
}