use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use uuid::Uuid;
pub const MAX_EXEC_TIMEOUT_SECS: u64 = 300;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind")]
pub enum TargetScope {
#[serde(rename = "designated_single_node")]
DesignatedSingleNode {
alias: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DispatchEnvelope {
pub request_id: String,
pub origin_agent: String,
pub target_scope: TargetScope,
pub command: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cwd: Option<String>,
pub timeout_secs: u64,
}
impl DispatchEnvelope {
pub fn new(
origin_agent: impl Into<String>,
target_scope: TargetScope,
command: impl Into<String>,
cwd: Option<String>,
timeout_secs: u64,
) -> Self {
Self {
request_id: Uuid::new_v4().to_string(),
origin_agent: origin_agent.into(),
target_scope,
command: command.into(),
cwd,
timeout_secs,
}
}
pub fn effective_timeout_secs(&self) -> u64 {
self.timeout_secs.min(MAX_EXEC_TIMEOUT_SECS)
}
pub fn wire_params(&self) -> Value {
let mut params = Map::new();
params.insert("command".to_string(), Value::String(self.command.clone()));
if let Some(cwd) = &self.cwd {
params.insert("cwd".to_string(), Value::String(cwd.clone()));
}
params.insert(
"timeout".to_string(),
Value::Number(self.effective_timeout_secs().into()),
);
Value::Object(params)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn envelope(cwd: Option<String>, timeout_secs: u64) -> DispatchEnvelope {
DispatchEnvelope::new(
"PoleMos",
TargetScope::DesignatedSingleNode {
alias: "hydro-lab".to_string(),
},
"uname -a",
cwd,
timeout_secs,
)
}
#[test]
fn wire_shape_has_exactly_command_cwd_timeout() {
let value = envelope(Some("/workspace".to_string()), 60).wire_params();
let obj = value.as_object().expect("params must be an object");
assert_eq!(obj.len(), 3, "wire params must carry exactly three keys");
assert_eq!(obj["command"], "uname -a");
assert_eq!(obj["cwd"], "/workspace");
assert_eq!(obj["timeout"], 60);
}
#[test]
fn wire_shape_omits_absent_cwd() {
let value = envelope(None, 60).wire_params();
let obj = value.as_object().expect("params must be an object");
assert_eq!(obj.len(), 2, "only command and timeout expected");
assert!(obj.get("cwd").is_none(), "absent cwd must be omitted");
assert_eq!(obj["command"], "uname -a");
assert_eq!(obj["timeout"], 60);
}
#[test]
fn wire_never_leaks_envelope_bookkeeping() {
let value = envelope(Some("/workspace".to_string()), 60).wire_params();
let serialized = value.to_string();
for forbidden in ["request_id", "origin_agent", "target_scope"] {
assert!(
!serialized.contains(forbidden),
"wire params must not contain {forbidden}"
);
}
}
#[test]
fn timeout_is_truncated_to_the_cap() {
assert_eq!(envelope(None, 10_000).effective_timeout_secs(), 300);
assert_eq!(envelope(None, 10_000).wire_params()["timeout"], 300);
assert_eq!(
envelope(None, MAX_EXEC_TIMEOUT_SECS).effective_timeout_secs(),
300
);
}
#[test]
fn timeout_under_the_cap_passes_through() {
assert_eq!(envelope(None, 42).effective_timeout_secs(), 42);
assert_eq!(envelope(None, 42).wire_params()["timeout"], 42);
assert_eq!(envelope(None, 0).effective_timeout_secs(), 0);
}
#[test]
fn request_id_is_uuid_v4_shaped() {
let id = envelope(None, 60).request_id;
let bytes = id.as_bytes();
assert_eq!(bytes.len(), 36, "expected hyphenated UUID, got {id}");
assert_eq!(bytes[14], b'4', "expected UUID v4, got {id}");
assert!(
matches!(bytes[19], b'8' | b'9' | b'a' | b'b'),
"expected RFC 4122 variant nibble, got {id}"
);
}
#[test]
fn envelope_serialization_round_trips() {
let original = envelope(Some("/workspace".to_string()), 90);
let serialized = serde_json::to_string(&original).unwrap();
let back: DispatchEnvelope = serde_json::from_str(&serialized).unwrap();
assert_eq!(back, original);
}
#[test]
fn target_scope_serializes_as_designated_single_node() {
let scope = TargetScope::DesignatedSingleNode {
alias: "hydro-lab".to_string(),
};
let value = serde_json::to_value(&scope).unwrap();
assert_eq!(value["kind"], "designated_single_node");
assert_eq!(value["alias"], "hydro-lab");
let back: TargetScope = serde_json::from_value(value).unwrap();
assert_eq!(back, scope);
}
}