wist_contracts/
local_work.rs1use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
15#[jumo(kind = "struct", domain = "Reporting", module = "Reporting.Protocol")]
16#[serde(deny_unknown_fields)]
17pub struct AgentLocalTask {
18 pub input_id: String,
20 pub path: String,
22 pub startup_position: String,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
28#[jumo(kind = "struct", domain = "Reporting", module = "Reporting.Protocol")]
29#[serde(deny_unknown_fields)]
30pub struct AgentLocalStandingWork {
31 pub work_id: String,
32 pub family: String,
34 pub status: String,
36 pub plan_version: i64,
38 #[serde(default)]
40 pub acknowledged_version: Option<i64>,
41 pub effective_from: String,
42 #[serde(default)]
44 pub tasks: Vec<AgentLocalTask>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
52#[jumo(kind = "struct", domain = "Reporting", module = "Reporting.Protocol")]
53#[serde(deny_unknown_fields)]
54pub struct AgentLocalOneShotWork {
55 pub work_id: String,
56 pub action: String,
58 pub status: String,
59 pub execution: String,
61 pub scheduled_at: String,
62 pub deadline_at: String,
63 pub timeout_seconds: i64,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ::jumo_derive::Jumo)]
68#[jumo(kind = "struct", domain = "Reporting", module = "Reporting.Protocol")]
69#[serde(deny_unknown_fields)]
70pub struct AgentLocalWork {
71 pub recorded_at: String,
73 pub gateway_sequence: i64,
75 #[serde(default)]
77 pub standing: Vec<AgentLocalStandingWork>,
78 #[serde(default)]
80 pub one_shot: Vec<AgentLocalOneShotWork>,
81 #[serde(default)]
83 pub local_inputs: Vec<AgentLocalTask>,
84 #[serde(default)]
86 pub metrics_interval_seconds: Option<i64>,
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn a_minimal_report_decodes_with_empty_collections() {
95 let json = r#"{"recorded_at":"2026-09-27T00:00:00Z","gateway_sequence":4}"#;
97 let work: AgentLocalWork = serde_json::from_str(json).expect("decode");
98 assert!(work.standing.is_empty());
99 assert!(work.one_shot.is_empty());
100 assert!(work.local_inputs.is_empty());
101 assert_eq!(work.metrics_interval_seconds, None);
102 }
103
104 #[test]
105 fn a_full_report_round_trips_and_rejects_unknown_fields() {
106 let work = AgentLocalWork {
107 recorded_at: "2026-09-27T00:00:00Z".to_string(),
108 gateway_sequence: 9,
109 standing: vec![AgentLocalStandingWork {
110 work_id: "work-1".to_string(),
111 family: "ServiceLifecycle".to_string(),
112 status: "active".to_string(),
113 plan_version: 2,
114 acknowledged_version: Some(2),
115 effective_from: "2026-09-27T00:00:00Z".to_string(),
116 tasks: vec![AgentLocalTask {
117 input_id: "app".to_string(),
118 path: "/var/log/app.log".to_string(),
119 startup_position: "tail".to_string(),
120 }],
121 }],
122 one_shot: vec![AgentLocalOneShotWork {
123 work_id: "work-2".to_string(),
124 action: "upgrade".to_string(),
125 status: "dispatched".to_string(),
126 execution: "unexecuted".to_string(),
127 scheduled_at: "2026-09-27T00:00:00Z".to_string(),
128 deadline_at: "2026-09-28T00:00:00Z".to_string(),
129 timeout_seconds: 600,
130 }],
131 local_inputs: Vec::new(),
132 metrics_interval_seconds: Some(15),
133 };
134 let json = serde_json::to_string(&work).expect("encode");
135 let back: AgentLocalWork = serde_json::from_str(&json).expect("decode");
136 assert_eq!(back, work);
137
138 let mutated = json.replacen('{', "{\"extra\":1,", 1);
139 assert!(serde_json::from_str::<AgentLocalWork>(&mutated).is_err());
140 }
141}