1use serde::{Deserialize, Serialize};
6
7use super::{OneShotWork, StandingWork};
8
9pub const API_VERSION: &str = wist_contracts::API_VERSION_V1;
11
12pub const POLL_WORK_KIND: &str = "poll_work";
14pub const ACK_WORK_KIND: &str = "ack_work";
16pub const REPORT_WORK_RESULT_KIND: &str = "report_work_result";
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct WorkGrant {
26 pub agent_id: String,
27 #[serde(default)]
29 pub standing: Vec<StandingWork>,
30 #[serde(default)]
31 pub one_shot: Vec<OneShotWork>,
32 pub sequence: i64,
34 pub granted_at: String,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(deny_unknown_fields)]
43pub struct PollWork {
44 pub api_version: String,
45 pub kind: String,
46 pub agent_id: String,
47 pub instance_id: String,
48 pub last_seen_sequence: i64,
49 pub wait_ms: i64,
50 pub requested_at: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct AckWork {
60 pub api_version: String,
61 pub kind: String,
62 pub agent_id: String,
63 pub instance_id: String,
64 pub work_id: String,
65 pub plan_version: i64,
66 pub acknowledged_at: String,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(deny_unknown_fields)]
75pub struct WorkAccepted {
76 pub work_id: String,
77 pub status: String,
79 pub accepted_at: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(deny_unknown_fields)]
91pub struct ReportWorkResult {
92 pub api_version: String,
93 pub kind: String,
94 pub agent_id: String,
95 pub instance_id: String,
96 pub work_id: String,
97 pub status: String,
98 #[serde(default)]
100 pub detail: String,
101 pub reported_at: String,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106#[serde(deny_unknown_fields)]
107pub struct WorkResultAccepted {
108 pub work_id: String,
109 pub status: String,
113 pub accepted_at: String,
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 fn standing() -> StandingWork {
121 StandingWork {
122 work_id: "work-a".to_string(),
123 agent_id: "agent-1".to_string(),
124 family: "LoginSession".to_string(),
125 spec: "unit-a,unit-b".to_string(),
126 catalog_version: 1,
127 proposal_id: None,
128 plan_version: 2,
129 effective_from: "2026-09-23T00:00:00Z".to_string(),
130 status: "active".to_string(),
131 updated_by: "admin".to_string(),
132 updated_at: "2026-09-23T00:00:00Z".to_string(),
133 }
134 }
135
136 fn one_shot() -> OneShotWork {
137 OneShotWork {
138 work_id: "work-1".to_string(),
139 agent_id: "agent-1".to_string(),
140 action: "upgrade".to_string(),
141 spec: "0.1.4".to_string(),
142 scheduled_at: "2026-09-23T00:00:00Z".to_string(),
143 deadline_at: "2026-09-24T00:00:00Z".to_string(),
144 timeout_seconds: 600,
145 interruptible: true,
146 status: "running".to_string(),
147 paused_at: None,
148 paused_total_seconds: 0,
149 current_step: None,
150 completed_steps: vec![],
151 attempt: 0,
152 issued_by: "admin".to_string(),
153 issued_at: "2026-09-23T00:00:00Z".to_string(),
154 }
155 }
156
157 #[test]
158 fn grant_round_trips_with_serde() {
159 let grant = WorkGrant {
160 agent_id: "agent-1".to_string(),
161 standing: vec![standing()],
162 one_shot: vec![one_shot()],
163 sequence: 7,
164 granted_at: "2026-09-23T00:00:00Z".to_string(),
165 };
166 let json = serde_json::to_string(&grant).expect("serialize");
167 let decoded: WorkGrant = serde_json::from_str(&json).expect("deserialize");
168 assert_eq!(decoded, grant);
169 }
170
171 #[test]
172 fn grant_omits_absent_optional_fields_and_still_decodes() {
173 let json = r#"{"agent_id":"a","standing":[],"one_shot":[],"sequence":0,
174 "granted_at":"t"}"#;
175 let grant: WorkGrant = serde_json::from_str(json).expect("deserialize");
176 assert!(grant.standing.is_empty());
177 assert!(grant.one_shot.is_empty());
178 }
179
180 #[test]
181 fn grant_rejects_unknown_fields() {
182 let json = r#"{"agent_id":"a","standing":[],"one_shot":[],"sequence":0,
184 "granted_at":"t","extra":1}"#;
185 assert!(serde_json::from_str::<WorkGrant>(json).is_err());
186 }
187}