aph_core/
communication_mandate.rs1#[derive(
8 std::fmt::Debug,
9 std::clone::Clone,
10 std::cmp::PartialEq,
11 std::cmp::Eq,
12 serde::Serialize,
13 serde::Deserialize,
14)]
15#[serde(rename_all = "camelCase", deny_unknown_fields)]
16pub struct CommunicationMandate {
17 pub id: String,
19 #[serde(default)]
21 pub delegation_mandate_id: std::option::Option<String>,
22 pub human_principal_did: String,
24 pub agent_did: String,
26 pub channel_kind: crate::envelope::ChannelKind,
33 pub recipient_addressing: serde_json::Value,
35 pub content_class: crate::envelope::ContentClass,
42 pub body_sha256: String,
44 pub body_size: u64,
46 pub policy_decision: crate::envelope::PolicyDecision,
48 pub issued_at: String,
50 pub expires_at: String,
52 pub notary_signature: String,
54}
55
56#[cfg(test)]
57mod tests {
58 fn sample() -> super::CommunicationMandate {
59 super::CommunicationMandate {
60 id: String::from("urn:uuid:00000000-0000-4000-8000-000000000002"),
61 delegation_mandate_id: std::option::Option::Some(String::from(
62 "urn:uuid:00000000-0000-4000-8000-000000000001",
63 )),
64 human_principal_did: String::from("did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy"),
65 agent_did: String::from("did:web:agent.squillo.com"),
66 channel_kind: crate::envelope::ChannelKind::Slack,
67 recipient_addressing: serde_json::json!({
68 "teamId": "T01234567",
69 "channelId": "C01234567",
70 "parentTs": "1716249600.000100"
71 }),
72 content_class: crate::envelope::ContentClass::Reply,
73 body_sha256: String::from("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
75 body_size: 1842,
76 policy_decision: crate::envelope::PolicyDecision::AskEveryTime,
77 issued_at: String::from("2026-05-21T00:00:00Z"),
78 expires_at: String::from("2026-05-21T01:00:00Z"),
79 notary_signature: String::from("z3WgvA9JHkbV3qLZHcM4FxBp4xHfQVnVnPKKDdyazQwQGdGzxsRdmZW"),
80 }
81 }
82
83 #[test]
84 fn serde_round_trip_preserves_equality() {
85 let m = sample();
89 let json = serde_json::to_string(&m).unwrap();
90 let back: super::CommunicationMandate = serde_json::from_str(&json).unwrap();
91 std::assert_eq!(m, back);
92 }
93
94 #[test]
95 fn serde_uses_camel_case_field_names() {
96 let m = sample();
100 let json = serde_json::to_string(&m).unwrap();
101 std::assert!(json.contains("\"delegationMandateId\""));
102 std::assert!(json.contains("\"humanPrincipalDid\""));
103 std::assert!(json.contains("\"agentDid\""));
104 std::assert!(json.contains("\"channelKind\""));
105 std::assert!(json.contains("\"recipientAddressing\""));
106 std::assert!(json.contains("\"contentClass\""));
107 std::assert!(json.contains("\"bodySha256\""));
108 std::assert!(json.contains("\"bodySize\""));
109 std::assert!(json.contains("\"policyDecision\""));
110 std::assert!(json.contains("\"issuedAt\""));
111 std::assert!(json.contains("\"expiresAt\""));
112 std::assert!(json.contains("\"notarySignature\""));
113 }
114
115 #[test]
116 fn delegation_mandate_id_defaults_to_none_when_omitted() {
117 let json = r#"{
121 "id": "urn:uuid:2",
122 "humanPrincipalDid": "did:key:h",
123 "agentDid": "did:web:a",
124 "channelKind": "slack",
125 "recipientAddressing": {"userId": "U1"},
126 "contentClass": "DM",
127 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
128 "bodySize": 12,
129 "policyDecision": "AskEveryTime",
130 "issuedAt": "2026-05-21T00:00:00Z",
131 "expiresAt": "2026-05-21T01:00:00Z",
132 "notarySignature": "zsig"
133 }"#;
134 let m: super::CommunicationMandate = serde_json::from_str(json).unwrap();
135 std::assert_eq!(m.delegation_mandate_id, std::option::Option::None);
136 }
137
138 #[test]
139 fn body_sha256_is_64_hex_chars() {
140 let m = sample();
144 std::assert_eq!(
145 m.body_sha256.len(),
146 64,
147 "SHA-256 hex must be exactly 64 chars"
148 );
149 std::assert!(
150 m.body_sha256.chars().all(|c| c.is_ascii_hexdigit()),
151 "body_sha256 must be lowercase hex"
152 );
153 std::assert!(
154 m.body_sha256.chars().all(|c| !c.is_ascii_uppercase()),
155 "body_sha256 must be lowercase"
156 );
157 }
158
159 #[test]
160 fn deny_unknown_fields_rejects_extra_key() {
161 let json = r#"{
164 "id": "urn:uuid:2",
165 "humanPrincipalDid": "did:key:h",
166 "agentDid": "did:web:a",
167 "channelKind": "slack",
168 "recipientAddressing": {"userId": "U1"},
169 "contentClass": "DM",
170 "bodySha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
171 "bodySize": 12,
172 "policyDecision": "AskEveryTime",
173 "issuedAt": "2026-05-21T00:00:00Z",
174 "expiresAt": "2026-05-21T01:00:00Z",
175 "notarySignature": "zsig",
176 "extraneousField": "should-fail"
177 }"#;
178 let result: std::result::Result<super::CommunicationMandate, _> = serde_json::from_str(json);
179 std::assert!(
180 result.is_err(),
181 "deny_unknown_fields must reject unknown keys"
182 );
183 }
184
185 #[test]
186 fn recipient_addressing_carries_channel_shaped_json() {
187 let m = sample();
191 let team_id = m
192 .recipient_addressing
193 .get("teamId")
194 .and_then(|v| v.as_str())
195 .unwrap_or("");
196 std::assert_eq!(team_id, "T01234567");
197 }
198}