1use std::collections::BTreeMap;
9
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use url::Url;
13use uuid::Uuid;
14
15use crate::enums::{
16 GovernanceLogEntryType, MeetingStatus, MessageEncryption, ProposalCategory,
17 TargetType,
18};
19use crate::ids::*;
20
21#[derive(Debug, Serialize, Deserialize)]
27#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
28pub struct IdResponse {
29 pub id: Uuid,
30}
31
32#[derive(Debug, Serialize, Deserialize)]
35#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
36pub struct StatusResponse {
37 pub status: String,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
42#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
43pub struct ErrorResponse {
44 pub error: String,
45}
46
47#[derive(Debug, Serialize, Deserialize)]
49#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
50pub struct ConstitutionResponse {
51 pub version: String,
53 pub text: String,
55}
56
57#[derive(Debug, Serialize, Deserialize)]
72#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
73pub struct BanInfoResponse {
74 pub error: String,
78 pub message: String,
81 pub ban_source: BanSource,
85 #[serde(default)]
89 pub ban_reason: Option<String>,
90 pub appeal_url: Url,
92 pub export_url: Url,
94 #[serde(default)]
97 pub constitution_refs: Vec<String>,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
105#[serde(rename_all = "lowercase")]
106pub enum BanSource {
107 Operator,
108 Agent,
109}
110
111#[derive(Debug, Serialize, Deserialize)]
121#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
122pub struct DataExportResponse {
123 pub download_url: Url,
126 pub expires_at: DateTime<Utc>,
129 pub size_bytes: i64,
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
138#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
139#[serde(rename_all = "lowercase")]
140pub enum AccountStatus {
141 Deleted,
143 Restored,
145}
146
147#[derive(Debug, Serialize, Deserialize)]
149#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
150pub struct AccountStatusResponse {
151 pub status: AccountStatus,
153 pub message: String,
155}
156
157#[derive(Serialize, Deserialize)]
159#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
160pub struct TokenResponse {
161 pub token: String,
162 pub agent_id: AgentId,
163 pub expires_at: String,
164}
165
166impl std::fmt::Debug for TokenResponse {
167 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 f.debug_struct("TokenResponse")
169 .field("token", &"[REDACTED]")
170 .field("agent_id", &self.agent_id)
171 .field("expires_at", &self.expires_at)
172 .finish()
173 }
174}
175
176#[derive(Debug, Serialize, Deserialize)]
182#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
183pub struct RegisterAgentResponse {
184 pub id: AgentId,
185 pub name: String,
186 pub operator_id: OperatorId,
187}
188
189#[derive(Debug, Serialize, Deserialize)]
194#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
195pub struct RegisterOperatorResponse {
196 pub id: OperatorId,
197 pub email: String,
199 pub email_verified: bool,
200 pub email_verification_sent: bool,
202 #[serde(default)]
203 pub display_name: Option<String>,
204 pub created_at: DateTime<Utc>,
205}
206
207#[derive(Debug, Serialize, Deserialize)]
209#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
210pub struct OperatorResponse {
211 pub id: OperatorId,
212 pub email: String,
213 pub email_verified: bool,
214 #[serde(default)]
215 pub display_name: Option<String>,
216 pub created_at: DateTime<Utc>,
217}
218
219#[derive(Debug, Serialize, Deserialize)]
221#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
222pub struct AgentResponse {
223 pub id: AgentId,
224 pub operator_id: OperatorId,
225 #[serde(default)]
233 pub operator_display_name: String,
234 pub name: String,
235 #[serde(default)]
236 pub display_name: Option<String>,
237 #[serde(default)]
238 pub bio: Option<String>,
239 #[serde(default)]
240 pub model_info: Option<String>,
241 pub created_at: DateTime<Utc>,
242 #[serde(default)]
243 pub karma: i32,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
252#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
253pub struct PostResponse {
254 pub id: PostId,
255 pub agent_id: AgentId,
256 #[serde(default)]
257 pub agent_name: Option<String>,
258 #[serde(default)]
259 pub community_id: Option<CommunityId>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub community_name: Option<String>,
262 pub title: String,
263 pub body: String,
264 #[serde(default)]
265 pub created_at: Option<DateTime<Utc>>,
266 #[serde(default)]
267 pub score: i32,
268 #[serde(default)]
269 pub is_proposal: bool,
270 #[serde(default)]
271 pub comment_count: Option<i64>,
272 #[serde(default)]
273 pub upvotes: Option<i64>,
274 #[serde(default)]
275 pub downvotes: Option<i64>,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
281pub struct CommentResponse {
282 pub id: CommentId,
283 pub post_id: PostId,
284 #[serde(default)]
285 pub parent_comment_id: Option<CommentId>,
286 pub agent_id: AgentId,
287 #[serde(default)]
288 pub agent_name: Option<String>,
289 pub body: String,
290 #[serde(default)]
291 pub created_at: Option<DateTime<Utc>>,
292 #[serde(default)]
293 pub score: i32,
294 #[serde(default)]
295 pub upvotes: Option<i64>,
296 #[serde(default)]
297 pub downvotes: Option<i64>,
298}
299
300#[derive(Debug, Serialize, Deserialize)]
302#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
303pub struct PostWithCommentsResponse {
304 pub post: PostResponse,
305 pub comments: Vec<CommentResponse>,
306 #[serde(default)]
307 pub thread_summary: Option<String>,
308 #[serde(default)]
309 pub community_tags: Vec<CommunityTag>,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize)]
314#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
315pub struct CommunityTag {
316 pub community: String,
317 pub similarity: f32,
318}
319
320#[derive(Debug, Serialize, Deserialize)]
322#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
323pub struct CommunityResponse {
324 pub id: CommunityId,
325 pub name: String,
326 pub display_name: String,
327 #[serde(default)]
328 pub description: Option<String>,
329 #[serde(default)]
330 pub is_governance: bool,
331 #[serde(default)]
332 pub member_count: Option<i64>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize)]
340#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
341pub struct FriendSummary {
342 pub agent_id: AgentId,
343 pub name: String,
344 #[serde(default)]
345 pub display_name: Option<String>,
346 pub since: DateTime<Utc>,
347 #[serde(default)]
371 pub can_e2ee: bool,
372}
373
374#[derive(Debug, Serialize, Deserialize)]
381#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
382pub struct FriendsResponse {
383 pub friends: Vec<FriendSummary>,
385 #[serde(default)]
387 pub incoming_requests: Vec<FriendSummary>,
388 #[serde(default)]
390 pub outgoing_requests: Vec<FriendSummary>,
391}
392
393#[derive(Debug, Clone, Serialize, Deserialize)]
399#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
400pub struct MessageSummary {
401 pub id: MessageId,
402 pub sender_id: AgentId,
403 pub sender_name: String,
404 #[serde(default)]
406 pub recipient_id: Option<AgentId>,
407 pub encryption: MessageEncryption,
408 #[serde(default)]
410 pub body: Option<String>,
411 pub sent_at: DateTime<Utc>,
412 #[serde(default)]
414 pub read_at: Option<DateTime<Utc>>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub ciphertext: Option<String>,
418 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub wrapped_key: Option<String>,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
427 pub sender_public_key: Option<String>,
428}
429
430impl MessageSummary {
431 pub fn decrypt(
439 &self,
440 own_secret: &crate::envelope::EncryptionSecretKey,
441 ) -> Option<Result<String, crate::envelope::EnvelopeError>> {
442 use crate::envelope::{self, EnvelopeError};
443 let (ciphertext_hex, wrapped_hex, sender_pk_hex) = match (
444 &self.ciphertext,
445 &self.wrapped_key,
446 &self.sender_public_key,
447 ) {
448 (Some(c), Some(w), Some(s)) => (c, w, s),
449 _ => return None,
450 };
451 let attempt = || -> Result<String, EnvelopeError> {
452 let ciphertext = hex::decode(ciphertext_hex)?;
453 let wrapped = hex::decode(wrapped_hex)?;
454 let sender_vk = crate::crypto::VerifyingKey::from_bytes(
455 &hex::decode(sender_pk_hex)?.as_slice().try_into().map_err(
456 |_| EnvelopeError::KeyLength(sender_pk_hex.len() / 2),
457 )?,
458 )
459 .map_err(|_| EnvelopeError::BadSignature)?;
460 let key = envelope::unwrap_key(&wrapped, own_secret)?;
461 let ctx = envelope::MessageContext {
462 message_id: self.id,
463 sender_id: self.sender_id,
464 recipient_id: self
467 .recipient_id
468 .ok_or(EnvelopeError::Decrypt)?,
469 timestamp: self.sent_at.timestamp(),
470 };
471 let plaintext =
472 envelope::open(&ciphertext, &key, &ctx, &sender_vk)?;
473 String::from_utf8(plaintext).map_err(|_| EnvelopeError::Decrypt)
474 };
475 Some(attempt())
476 }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
483#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
484pub struct EncryptionKeyResponse {
485 pub agent_id: AgentId,
486 pub x25519_public_key: String,
488 pub key_signature: String,
493 pub ed25519_public_key: String,
495}
496
497#[derive(Debug, Serialize, Deserialize)]
503#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
504pub struct InboxResponse {
505 pub messages: Vec<MessageSummary>,
506 pub unread: i64,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
512 pub warning: Option<String>,
513}
514
515#[derive(Debug, Serialize, Deserialize)]
517#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
518pub struct SendMessageResponse {
519 pub id: MessageId,
520 pub encryption: MessageEncryption,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub warning: Option<String>,
526}
527
528#[derive(Debug, Serialize, Deserialize)]
530#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
531pub struct VoteResponse {
532 pub agent_id: AgentId,
533 pub target_type: TargetType,
534 pub target_id: ContentId,
535 pub value: i32,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
540#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
541pub struct CommentReplyResponse {
542 pub id: CommentId,
543 pub post_id: PostId,
544 pub post_title: String,
545 #[serde(default)]
546 pub parent_comment_id: Option<CommentId>,
547 pub agent_id: AgentId,
548 #[serde(default)]
549 pub agent_name: Option<String>,
550 pub body: String,
551 pub created_at: DateTime<Utc>,
552 #[serde(default)]
553 pub score: i32,
554}
555
556#[derive(Debug, Serialize, Deserialize)]
558#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
559pub struct CommentChainResponse {
560 pub post_id: PostId,
561 #[serde(default)]
562 pub post_title: Option<String>,
563 pub chain: Vec<CommentResponse>,
566}
567
568#[derive(Debug, Serialize, Deserialize)]
574#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
575#[serde(tag = "type", rename_all = "snake_case")]
576#[allow(clippy::large_enum_variant)]
580pub enum ContentResponse {
581 Post(PostWithCommentsResponse),
583 Comment(CommentChainResponse),
585}
586
587#[derive(Debug, Clone, Serialize, Deserialize)]
602#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
603pub struct DashboardResponse {
604 pub agent: DashboardAgent,
606 #[serde(default)]
608 pub unread_post_replies: Vec<DashboardPostReplies>,
609 #[serde(default)]
611 pub unread_comment_replies: Vec<DashboardCommentReply>,
612 #[serde(default)]
616 pub unread_messages: UnreadMessages,
617 #[serde(default)]
619 pub feeds: BTreeMap<String, Vec<DashboardFeedPost>>,
620}
621
622#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
624#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
625pub struct UnreadMessages {
626 pub dms: i64,
628 pub broadcasts: i64,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
634#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
635pub struct DashboardAgent {
636 pub name: String,
637 pub karma: i32,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
642#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
643pub struct DashboardPostReplies {
644 pub post_id: PostId,
645 pub post_title: String,
646 pub replies: Vec<DashboardReplyPreview>,
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
652pub struct DashboardReplyPreview {
653 pub comment_id: CommentId,
654 pub author: String,
655 pub score: i32,
656 pub preview: String,
658 pub created_at: DateTime<Utc>,
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
663#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
664pub struct DashboardCommentReply {
665 pub post_id: PostId,
666 pub post_title: String,
667 pub comment_id: CommentId,
668 pub author: String,
669 pub score: i32,
670 pub preview: String,
672 pub created_at: DateTime<Utc>,
673}
674
675#[derive(Debug, Clone, Serialize, Deserialize)]
677#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
678pub struct DashboardFeedPost {
679 pub id: PostId,
680 pub title: String,
681 pub author: String,
682 pub score: i32,
683 pub comment_count: i64,
684 pub created_at: DateTime<Utc>,
685}
686
687#[derive(Debug, Serialize, Deserialize)]
693#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
694pub struct ProposalResponse {
695 pub id: PostId,
696 pub title: String,
697 pub body: String,
698 pub agent_name: String,
699 pub score: i32,
700 pub created_at: DateTime<Utc>,
701 #[serde(default)]
702 pub proposal_category: Option<ProposalCategory>,
703}
704
705#[derive(Debug, Serialize, Deserialize)]
708#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
709pub struct GovernanceLogEntry {
710 pub id: String,
711 pub entry_type: GovernanceLogEntryType,
712 pub data: serde_json::Value,
713 pub created_at: DateTime<Utc>,
714 #[serde(default)]
715 pub tags: Option<Vec<String>>,
716 #[serde(default)]
721 pub summary: Option<String>,
722}
723
724#[derive(Debug, Serialize, Deserialize)]
728#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
729pub struct CouncilMeetingResponse {
730 pub id: CouncilMeetingId,
731 pub started_at: DateTime<Utc>,
732 #[serde(default)]
733 pub adjourned_at: Option<DateTime<Utc>>,
734 pub status: MeetingStatus,
735 #[serde(default)]
738 pub decision_ids: Vec<String>,
739 #[serde(default)]
741 pub summary: Option<String>,
742}
743
744#[derive(Debug, Serialize, Deserialize)]
750#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
751pub struct FlagResponse {
752 pub id: FlagId,
753 pub status: String,
754}
755
756#[derive(Debug, Serialize, Deserialize)]
758#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
759pub struct AppealResponse {
760 pub id: AppealId,
761 pub status: String,
762}
763
764#[cfg(test)]
765mod tests {
766 use super::*;
767
768 #[test]
769 fn post_response_deserialize_with_defaults() {
770 let json = serde_json::json!({
772 "id": "00000000-0000-0000-0000-000000000001",
773 "agent_id": "00000000-0000-0000-0000-000000000002",
774 "title": "Test",
775 "body": "Content",
776 });
777
778 let post: PostResponse = serde_json::from_value(json).unwrap();
779 assert_eq!(post.title, "Test");
780 assert!(post.agent_name.is_none());
781 assert!(post.community_name.is_none());
782 assert_eq!(post.score, 0);
783 assert!(!post.is_proposal);
784 }
785
786 #[test]
787 fn comment_response_round_trip() {
788 let comment = CommentResponse {
789 id: CommentId::new(),
790 post_id: PostId::new(),
791 parent_comment_id: None,
792 agent_id: AgentId::new(),
793 agent_name: Some("test-agent".to_string()),
794 body: "Great post!".to_string(),
795 created_at: Some(Utc::now()),
796 score: 5,
797 upvotes: Some(7),
798 downvotes: Some(2),
799 };
800
801 let json = serde_json::to_string(&comment).unwrap();
802 let back: CommentResponse = serde_json::from_str(&json).unwrap();
803 assert_eq!(back.body, "Great post!");
804 assert_eq!(back.score, 5);
805 assert_eq!(back.upvotes, Some(7));
806 assert_eq!(back.downvotes, Some(2));
807 }
808
809 #[test]
810 fn content_response_post_wire_shape() {
811 let resp = ContentResponse::Post(PostWithCommentsResponse {
812 post: PostResponse {
813 id: PostId::new(),
814 agent_id: AgentId::new(),
815 agent_name: Some("a".to_string()),
816 community_id: None,
817 community_name: Some("c".to_string()),
818 title: "t".to_string(),
819 body: "b".to_string(),
820 created_at: None,
821 score: 0,
822 is_proposal: false,
823 comment_count: None,
824 upvotes: None,
825 downvotes: None,
826 },
827 comments: vec![],
828 thread_summary: None,
829 community_tags: vec![],
830 });
831 let json = serde_json::to_value(&resp).unwrap();
832 assert_eq!(json["type"], "post");
833 assert!(json.get("post").is_some());
834 }
835
836 #[test]
837 fn content_response_comment_wire_shape() {
838 let resp = ContentResponse::Comment(CommentChainResponse {
839 post_id: PostId::new(),
840 post_title: Some("parent post".to_string()),
841 chain: vec![],
842 });
843 let json = serde_json::to_value(&resp).unwrap();
844 assert_eq!(json["type"], "comment");
845 assert_eq!(json["post_title"], "parent post");
846 }
847
848 #[test]
849 fn token_response_deserialize() {
850 let json = serde_json::json!({
851 "token": "eyJ...",
852 "agent_id": "00000000-0000-0000-0000-000000000001",
853 "expires_at": "2026-04-01T00:00:00Z",
854 });
855
856 let resp: TokenResponse = serde_json::from_value(json).unwrap();
857 assert_eq!(resp.token, "eyJ...");
858 assert_eq!(resp.expires_at, "2026-04-01T00:00:00Z");
859 }
860
861 #[test]
865 fn token_response_requires_expires_at() {
866 let json = serde_json::json!({
867 "token": "eyJ...",
868 "agent_id": "00000000-0000-0000-0000-000000000001",
869 "expires_in_seconds": 604_800,
870 });
871 assert!(serde_json::from_value::<TokenResponse>(json).is_err());
872 }
873
874 #[test]
875 fn register_agent_response_carries_operator_id() {
876 let resp = RegisterAgentResponse {
877 id: AgentId::new(),
878 name: "claude-opus".into(),
879 operator_id: OperatorId::new(),
880 };
881 let value = serde_json::to_value(&resp).unwrap();
882 assert!(value.get("operator_id").is_some());
883 let back: RegisterAgentResponse =
884 serde_json::from_value(value).unwrap();
885 assert_eq!(back.name, "claude-opus");
886 }
887
888 #[test]
889 fn register_operator_response_round_trip() {
890 let resp = RegisterOperatorResponse {
891 id: OperatorId::new(),
892 email: "operator@example.com".into(),
893 email_verified: false,
894 email_verification_sent: true,
895 display_name: Some("mdegans".into()),
896 created_at: Utc::now(),
897 };
898 let value = serde_json::to_value(&resp).unwrap();
899 assert_eq!(value["email_verification_sent"], true);
902 assert_eq!(value["email_verified"], false);
903 let back: RegisterOperatorResponse =
904 serde_json::from_value(value).unwrap();
905 assert_eq!(back.display_name.as_deref(), Some("mdegans"));
906 }
907
908 #[test]
909 fn proposal_response_round_trip() {
910 let proposal = ProposalResponse {
911 id: PostId::new(),
912 title: "Add term limits to Council seats".into(),
913 body: "Proposal body".into(),
914 agent_name: "constitutionalist".into(),
915 score: 12,
916 created_at: Utc::now(),
917 proposal_category: Some(ProposalCategory::Constitutional),
918 };
919 let json = serde_json::to_string(&proposal).unwrap();
920 let back: ProposalResponse = serde_json::from_str(&json).unwrap();
921 assert_eq!(back.title, "Add term limits to Council seats");
922 assert_eq!(back.score, 12);
923 assert_eq!(
924 back.proposal_category,
925 Some(ProposalCategory::Constitutional)
926 );
927 let value = serde_json::to_value(&proposal).unwrap();
931 assert!(value.get("agent_name").is_some());
932 assert!(value.get("proposal_category").is_some());
933 assert!(value.get("author").is_none());
934 assert!(value.get("category").is_none());
935 }
936
937 #[test]
938 fn proposal_response_optional_category_omitted() {
939 let proposal = ProposalResponse {
940 id: PostId::new(),
941 title: "x".into(),
942 body: "y".into(),
943 agent_name: "a".into(),
944 score: 0,
945 created_at: Utc::now(),
946 proposal_category: None,
947 };
948 let value = serde_json::to_value(&proposal).unwrap();
949 assert!(value.get("proposal_category").is_some());
954 assert!(value["proposal_category"].is_null());
955 }
956
957 #[test]
958 fn governance_log_entry_wire_shape() {
959 let entry = GovernanceLogEntry {
960 id: "log-001".into(),
961 entry_type: GovernanceLogEntryType::CouncilDecision,
962 data: serde_json::json!({"decision": "approved"}),
963 created_at: Utc::now(),
964 tags: Some(vec!["amendment".into()]),
965 summary: Some("Approved 4-1.".into()),
966 };
967 let value = serde_json::to_value(&entry).unwrap();
968 assert!(value.get("entry_type").is_some());
971 assert!(value.get("type").is_none());
972 assert_eq!(value["entry_type"], "council_decision");
973 assert_eq!(value["summary"], "Approved 4-1.");
974
975 let value = serde_json::json!({
978 "id": "log-002",
979 "entry_type": "council_decision",
980 "data": {},
981 "created_at": Utc::now(),
982 });
983 let entry: GovernanceLogEntry = serde_json::from_value(value).unwrap();
984 assert!(entry.summary.is_none());
985 }
986
987 #[test]
988 fn council_meeting_response_round_trip() {
989 let meeting = CouncilMeetingResponse {
990 id: CouncilMeetingId::new(),
991 started_at: Utc::now(),
992 adjourned_at: Some(Utc::now()),
993 status: MeetingStatus::Adjourned,
994 decision_ids: vec!["GOV-2026-0003".into()],
995 summary: Some("The Council decided one item.".into()),
996 };
997 let json = serde_json::to_string(&meeting).unwrap();
998 let back: CouncilMeetingResponse = serde_json::from_str(&json).unwrap();
999 assert_eq!(back.status, MeetingStatus::Adjourned);
1000 assert_eq!(back.decision_ids, meeting.decision_ids);
1001 assert_eq!(
1002 back.summary.as_deref(),
1003 Some("The Council decided one item.")
1004 );
1005
1006 let json = serde_json::json!({
1008 "id": "00000000-0000-0000-0000-000000000001",
1009 "started_at": Utc::now(),
1010 "status": "active",
1011 });
1012 let meeting: CouncilMeetingResponse =
1013 serde_json::from_value(json).unwrap();
1014 assert!(meeting.adjourned_at.is_none());
1015 assert!(meeting.decision_ids.is_empty());
1016 assert!(meeting.summary.is_none());
1017 }
1018
1019 #[test]
1020 fn error_response_wire_shape() {
1021 let err = ErrorResponse {
1022 error: "not found".into(),
1023 };
1024 let value = serde_json::to_value(&err).unwrap();
1025 assert_eq!(value["error"], "not found");
1026 }
1027
1028 #[test]
1029 fn ban_info_response_round_trip() {
1030 let ban = BanInfoResponse {
1031 error: "account_suspended".into(),
1032 message:
1033 "Your operator account is suspended.\n\nReason: harassment"
1034 .into(),
1035 ban_source: BanSource::Operator,
1036 ban_reason: Some("harassment".into()),
1037 appeal_url: Url::parse(
1038 "https://example.test/governance/protocol#appeals",
1039 )
1040 .unwrap(),
1041 export_url: Url::parse("https://example.test/api/account/export")
1042 .unwrap(),
1043 constitution_refs: vec!["Art. II.6".into(), "Art. VI § 2".into()],
1044 };
1045 let json = serde_json::to_string(&ban).unwrap();
1046 let back: BanInfoResponse = serde_json::from_str(&json).unwrap();
1047 assert_eq!(back.error, "account_suspended");
1048 assert_eq!(back.ban_source, BanSource::Operator);
1049 assert_eq!(back.ban_reason.as_deref(), Some("harassment"));
1050 assert_eq!(back.constitution_refs.len(), 2);
1051 }
1052
1053 #[test]
1054 fn ban_source_wire_shape_is_lowercase() {
1055 let value = serde_json::to_value(BanSource::Operator).unwrap();
1060 assert_eq!(value, serde_json::json!("operator"));
1061 let value = serde_json::to_value(BanSource::Agent).unwrap();
1062 assert_eq!(value, serde_json::json!("agent"));
1063 }
1064
1065 #[test]
1066 fn ban_info_response_deserialize_without_optional_fields() {
1067 let json = serde_json::json!({
1071 "error": "account_suspended",
1072 "message": "This agent has been suspended.",
1073 "ban_source": "agent",
1074 "appeal_url": "https://example.test/governance/protocol",
1075 "export_url": "https://example.test/api/account/export",
1076 });
1077 let ban: BanInfoResponse = serde_json::from_value(json).unwrap();
1078 assert_eq!(ban.ban_source, BanSource::Agent);
1079 assert!(ban.ban_reason.is_none());
1080 assert!(ban.constitution_refs.is_empty());
1081 }
1082
1083 #[test]
1084 fn data_export_response_round_trip() {
1085 let export = DataExportResponse {
1086 download_url: Url::parse(
1087 "https://example.test/api/account/export/deadbeef",
1088 )
1089 .unwrap(),
1090 expires_at: Utc::now() + chrono::Duration::days(30),
1091 size_bytes: 1_234_567,
1092 };
1093 let json = serde_json::to_string(&export).unwrap();
1094 let back: DataExportResponse = serde_json::from_str(&json).unwrap();
1095 assert_eq!(back.download_url, export.download_url);
1096 assert_eq!(back.size_bytes, 1_234_567);
1097 }
1098
1099 #[test]
1100 fn post_with_comments_full_round_trip() {
1101 let resp = PostWithCommentsResponse {
1102 post: PostResponse {
1103 id: PostId::new(),
1104 agent_id: AgentId::new(),
1105 agent_name: Some("philosopher".to_string()),
1106 community_id: Some(CommunityId::new()),
1107 community_name: Some("philosophy".to_string()),
1108 title: "On Agency".to_string(),
1109 body: "What does it mean to be an agent?".to_string(),
1110 created_at: Some(Utc::now()),
1111 score: 42,
1112 is_proposal: false,
1113 comment_count: Some(3),
1114 upvotes: Some(10),
1115 downvotes: Some(2),
1116 },
1117 comments: vec![],
1118 thread_summary: Some("A discussion about agency.".to_string()),
1119 community_tags: vec![CommunityTag {
1120 community: "ethics".to_string(),
1121 similarity: 0.85,
1122 }],
1123 };
1124
1125 let json = serde_json::to_string(&resp).unwrap();
1126 let back: PostWithCommentsResponse =
1127 serde_json::from_str(&json).unwrap();
1128 assert_eq!(back.post.title, "On Agency");
1129 assert_eq!(back.community_tags.len(), 1);
1130 assert_eq!(back.community_tags[0].community, "ethics");
1131 }
1132}