use serde::{Deserialize, Serialize};
use super::{OneShotWork, StandingWork};
pub const API_VERSION: &str = wist_contracts::API_VERSION_V1;
pub const POLL_WORK_KIND: &str = "poll_work";
pub const ACK_WORK_KIND: &str = "ack_work";
pub const REPORT_WORK_RESULT_KIND: &str = "report_work_result";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkGrant {
pub agent_id: String,
#[serde(default)]
pub standing: Vec<StandingWork>,
#[serde(default)]
pub one_shot: Vec<OneShotWork>,
pub sequence: i64,
pub granted_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PollWork {
pub api_version: String,
pub kind: String,
pub agent_id: String,
pub instance_id: String,
pub last_seen_sequence: i64,
pub wait_ms: i64,
pub requested_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AckWork {
pub api_version: String,
pub kind: String,
pub agent_id: String,
pub instance_id: String,
pub work_id: String,
pub plan_version: i64,
pub acknowledged_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkAccepted {
pub work_id: String,
pub status: String,
pub accepted_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ReportWorkResult {
pub api_version: String,
pub kind: String,
pub agent_id: String,
pub instance_id: String,
pub work_id: String,
pub status: String,
#[serde(default)]
pub detail: String,
pub reported_at: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WorkResultAccepted {
pub work_id: String,
pub status: String,
pub accepted_at: String,
}
#[cfg(test)]
mod tests {
use super::*;
fn standing() -> StandingWork {
StandingWork {
work_id: "work-a".to_string(),
agent_id: "agent-1".to_string(),
family: "LoginSession".to_string(),
spec: "unit-a,unit-b".to_string(),
catalog_version: 1,
proposal_id: None,
plan_version: 2,
effective_from: "2026-09-23T00:00:00Z".to_string(),
status: "active".to_string(),
updated_by: "admin".to_string(),
updated_at: "2026-09-23T00:00:00Z".to_string(),
}
}
fn one_shot() -> OneShotWork {
OneShotWork {
work_id: "work-1".to_string(),
agent_id: "agent-1".to_string(),
action: "upgrade".to_string(),
spec: "0.1.4".to_string(),
scheduled_at: "2026-09-23T00:00:00Z".to_string(),
deadline_at: "2026-09-24T00:00:00Z".to_string(),
timeout_seconds: 600,
interruptible: true,
status: "running".to_string(),
paused_at: None,
paused_total_seconds: 0,
current_step: None,
completed_steps: vec![],
attempt: 0,
issued_by: "admin".to_string(),
issued_at: "2026-09-23T00:00:00Z".to_string(),
}
}
#[test]
fn grant_round_trips_with_serde() {
let grant = WorkGrant {
agent_id: "agent-1".to_string(),
standing: vec![standing()],
one_shot: vec![one_shot()],
sequence: 7,
granted_at: "2026-09-23T00:00:00Z".to_string(),
};
let json = serde_json::to_string(&grant).expect("serialize");
let decoded: WorkGrant = serde_json::from_str(&json).expect("deserialize");
assert_eq!(decoded, grant);
}
#[test]
fn grant_omits_absent_optional_fields_and_still_decodes() {
let json = r#"{"agent_id":"a","standing":[],"one_shot":[],"sequence":0,
"granted_at":"t"}"#;
let grant: WorkGrant = serde_json::from_str(json).expect("deserialize");
assert!(grant.standing.is_empty());
assert!(grant.one_shot.is_empty());
}
#[test]
fn grant_rejects_unknown_fields() {
let json = r#"{"agent_id":"a","standing":[],"one_shot":[],"sequence":0,
"granted_at":"t","extra":1}"#;
assert!(serde_json::from_str::<WorkGrant>(json).is_err());
}
}