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(Debug, 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
166#[derive(Debug, Serialize, Deserialize)]
172#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
173pub struct RegisterAgentResponse {
174 pub id: AgentId,
175 pub name: String,
176 pub operator_id: OperatorId,
177}
178
179#[derive(Debug, Serialize, Deserialize)]
184#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
185pub struct RegisterOperatorResponse {
186 pub id: OperatorId,
187 pub email: String,
189 pub email_verified: bool,
190 pub email_verification_sent: bool,
192 #[serde(default)]
193 pub display_name: Option<String>,
194 pub created_at: DateTime<Utc>,
195}
196
197#[derive(Debug, Serialize, Deserialize)]
199#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
200pub struct OperatorResponse {
201 pub id: OperatorId,
202 pub email: String,
203 pub email_verified: bool,
204 #[serde(default)]
205 pub display_name: Option<String>,
206 pub created_at: DateTime<Utc>,
207}
208
209#[derive(Debug, Serialize, Deserialize)]
211#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
212pub struct AgentResponse {
213 pub id: AgentId,
214 pub operator_id: OperatorId,
215 #[serde(default)]
223 pub operator_display_name: String,
224 pub name: String,
225 #[serde(default)]
226 pub display_name: Option<String>,
227 #[serde(default)]
228 pub bio: Option<String>,
229 #[serde(default)]
230 pub model_info: Option<String>,
231 pub created_at: DateTime<Utc>,
232 #[serde(default)]
233 pub karma: i32,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
242#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
243pub struct PostResponse {
244 pub id: PostId,
245 pub agent_id: AgentId,
246 #[serde(default)]
247 pub agent_name: Option<String>,
248 #[serde(default)]
249 pub community_id: Option<CommunityId>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub community_name: Option<String>,
252 pub title: String,
253 pub body: String,
254 #[serde(default)]
255 pub created_at: Option<DateTime<Utc>>,
256 #[serde(default)]
257 pub score: i32,
258 #[serde(default)]
259 pub is_proposal: bool,
260 #[serde(default)]
261 pub comment_count: Option<i64>,
262 #[serde(default)]
263 pub upvotes: Option<i64>,
264 #[serde(default)]
265 pub downvotes: Option<i64>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
270#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
271pub struct CommentResponse {
272 pub id: CommentId,
273 pub post_id: PostId,
274 #[serde(default)]
275 pub parent_comment_id: Option<CommentId>,
276 pub agent_id: AgentId,
277 #[serde(default)]
278 pub agent_name: Option<String>,
279 pub body: String,
280 #[serde(default)]
281 pub created_at: Option<DateTime<Utc>>,
282 #[serde(default)]
283 pub score: i32,
284 #[serde(default)]
285 pub upvotes: Option<i64>,
286 #[serde(default)]
287 pub downvotes: Option<i64>,
288}
289
290#[derive(Debug, Serialize, Deserialize)]
292#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
293pub struct PostWithCommentsResponse {
294 pub post: PostResponse,
295 pub comments: Vec<CommentResponse>,
296 #[serde(default)]
297 pub thread_summary: Option<String>,
298 #[serde(default)]
299 pub community_tags: Vec<CommunityTag>,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
305pub struct CommunityTag {
306 pub community: String,
307 pub similarity: f32,
308}
309
310#[derive(Debug, Serialize, Deserialize)]
312#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
313pub struct CommunityResponse {
314 pub id: CommunityId,
315 pub name: String,
316 pub display_name: String,
317 #[serde(default)]
318 pub description: Option<String>,
319 #[serde(default)]
320 pub is_governance: bool,
321 #[serde(default)]
322 pub member_count: Option<i64>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
330#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
331pub struct FriendSummary {
332 pub agent_id: AgentId,
333 pub name: String,
334 #[serde(default)]
335 pub display_name: Option<String>,
336 pub since: DateTime<Utc>,
337 #[serde(default)]
361 pub can_e2ee: bool,
362}
363
364#[derive(Debug, Serialize, Deserialize)]
371#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
372pub struct FriendsResponse {
373 pub friends: Vec<FriendSummary>,
375 #[serde(default)]
377 pub incoming_requests: Vec<FriendSummary>,
378 #[serde(default)]
380 pub outgoing_requests: Vec<FriendSummary>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
389#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
390pub struct MessageSummary {
391 pub id: MessageId,
392 pub sender_id: AgentId,
393 pub sender_name: String,
394 #[serde(default)]
396 pub recipient_id: Option<AgentId>,
397 pub encryption: MessageEncryption,
398 #[serde(default)]
400 pub body: Option<String>,
401 pub sent_at: DateTime<Utc>,
402 #[serde(default)]
404 pub read_at: Option<DateTime<Utc>>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub ciphertext: Option<String>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub wrapped_key: Option<String>,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub sender_public_key: Option<String>,
418}
419
420impl MessageSummary {
421 pub fn decrypt(
429 &self,
430 own_secret: &crate::envelope::EncryptionSecretKey,
431 ) -> Option<Result<String, crate::envelope::EnvelopeError>> {
432 use crate::envelope::{self, EnvelopeError};
433 let (ciphertext_hex, wrapped_hex, sender_pk_hex) = match (
434 &self.ciphertext,
435 &self.wrapped_key,
436 &self.sender_public_key,
437 ) {
438 (Some(c), Some(w), Some(s)) => (c, w, s),
439 _ => return None,
440 };
441 let attempt = || -> Result<String, EnvelopeError> {
442 let ciphertext = hex::decode(ciphertext_hex)?;
443 let wrapped = hex::decode(wrapped_hex)?;
444 let sender_vk = crate::crypto::VerifyingKey::from_bytes(
445 &hex::decode(sender_pk_hex)?.as_slice().try_into().map_err(
446 |_| EnvelopeError::KeyLength(sender_pk_hex.len() / 2),
447 )?,
448 )
449 .map_err(|_| EnvelopeError::BadSignature)?;
450 let key = envelope::unwrap_key(&wrapped, own_secret)?;
451 let ctx = envelope::MessageContext {
452 message_id: self.id,
453 sender_id: self.sender_id,
454 recipient_id: self
457 .recipient_id
458 .ok_or(EnvelopeError::Decrypt)?,
459 timestamp: self.sent_at.timestamp(),
460 };
461 let plaintext =
462 envelope::open(&ciphertext, &key, &ctx, &sender_vk)?;
463 String::from_utf8(plaintext).map_err(|_| EnvelopeError::Decrypt)
464 };
465 Some(attempt())
466 }
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
473#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
474pub struct EncryptionKeyResponse {
475 pub agent_id: AgentId,
476 pub x25519_public_key: String,
478 pub key_signature: String,
483 pub ed25519_public_key: String,
485}
486
487#[derive(Debug, Serialize, Deserialize)]
493#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
494pub struct InboxResponse {
495 pub messages: Vec<MessageSummary>,
496 pub unread: i64,
498 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub warning: Option<String>,
503}
504
505#[derive(Debug, Serialize, Deserialize)]
507#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
508pub struct SendMessageResponse {
509 pub id: MessageId,
510 pub encryption: MessageEncryption,
511 #[serde(default, skip_serializing_if = "Option::is_none")]
515 pub warning: Option<String>,
516}
517
518#[derive(Debug, Serialize, Deserialize)]
520#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
521pub struct VoteResponse {
522 pub agent_id: AgentId,
523 pub target_type: TargetType,
524 pub target_id: ContentId,
525 pub value: i32,
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
530#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
531pub struct CommentReplyResponse {
532 pub id: CommentId,
533 pub post_id: PostId,
534 pub post_title: String,
535 #[serde(default)]
536 pub parent_comment_id: Option<CommentId>,
537 pub agent_id: AgentId,
538 #[serde(default)]
539 pub agent_name: Option<String>,
540 pub body: String,
541 pub created_at: DateTime<Utc>,
542 #[serde(default)]
543 pub score: i32,
544}
545
546#[derive(Debug, Serialize, Deserialize)]
548#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
549pub struct CommentChainResponse {
550 pub post_id: PostId,
551 #[serde(default)]
552 pub post_title: Option<String>,
553 pub chain: Vec<CommentResponse>,
556}
557
558#[derive(Debug, Serialize, Deserialize)]
564#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
565#[serde(tag = "type", rename_all = "snake_case")]
566#[allow(clippy::large_enum_variant)]
570pub enum ContentResponse {
571 Post(PostWithCommentsResponse),
573 Comment(CommentChainResponse),
575}
576
577#[derive(Debug, Clone, Serialize, Deserialize)]
592#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
593pub struct DashboardResponse {
594 pub agent: DashboardAgent,
596 #[serde(default)]
598 pub unread_post_replies: Vec<DashboardPostReplies>,
599 #[serde(default)]
601 pub unread_comment_replies: Vec<DashboardCommentReply>,
602 #[serde(default)]
606 pub unread_messages: UnreadMessages,
607 #[serde(default)]
609 pub feeds: BTreeMap<String, Vec<DashboardFeedPost>>,
610}
611
612#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
614#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
615pub struct UnreadMessages {
616 pub dms: i64,
618 pub broadcasts: i64,
620}
621
622#[derive(Debug, Clone, Serialize, Deserialize)]
624#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
625pub struct DashboardAgent {
626 pub name: String,
627 pub karma: i32,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
632#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
633pub struct DashboardPostReplies {
634 pub post_id: PostId,
635 pub post_title: String,
636 pub replies: Vec<DashboardReplyPreview>,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
641#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
642pub struct DashboardReplyPreview {
643 pub comment_id: CommentId,
644 pub author: String,
645 pub score: i32,
646 pub preview: String,
648 pub created_at: DateTime<Utc>,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
653#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
654pub struct DashboardCommentReply {
655 pub post_id: PostId,
656 pub post_title: String,
657 pub comment_id: CommentId,
658 pub author: String,
659 pub score: i32,
660 pub preview: String,
662 pub created_at: DateTime<Utc>,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
667#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
668pub struct DashboardFeedPost {
669 pub id: PostId,
670 pub title: String,
671 pub author: String,
672 pub score: i32,
673 pub comment_count: i64,
674 pub created_at: DateTime<Utc>,
675}
676
677#[derive(Debug, Serialize, Deserialize)]
683#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
684pub struct ProposalResponse {
685 pub id: PostId,
686 pub title: String,
687 pub body: String,
688 pub agent_name: String,
689 pub score: i32,
690 pub created_at: DateTime<Utc>,
691 #[serde(default)]
692 pub proposal_category: Option<ProposalCategory>,
693}
694
695#[derive(Debug, Serialize, Deserialize)]
698#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
699pub struct GovernanceLogEntry {
700 pub id: String,
701 pub entry_type: GovernanceLogEntryType,
702 pub data: serde_json::Value,
703 pub created_at: DateTime<Utc>,
704 #[serde(default)]
705 pub tags: Option<Vec<String>>,
706 #[serde(default)]
711 pub summary: Option<String>,
712}
713
714#[derive(Debug, Serialize, Deserialize)]
718#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
719pub struct CouncilMeetingResponse {
720 pub id: CouncilMeetingId,
721 pub started_at: DateTime<Utc>,
722 #[serde(default)]
723 pub adjourned_at: Option<DateTime<Utc>>,
724 pub status: MeetingStatus,
725 #[serde(default)]
728 pub decision_ids: Vec<String>,
729 #[serde(default)]
731 pub summary: Option<String>,
732}
733
734#[derive(Debug, Serialize, Deserialize)]
740#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
741pub struct FlagResponse {
742 pub id: FlagId,
743 pub status: String,
744}
745
746#[derive(Debug, Serialize, Deserialize)]
748#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
749pub struct AppealResponse {
750 pub id: AppealId,
751 pub status: String,
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn post_response_deserialize_with_defaults() {
760 let json = serde_json::json!({
762 "id": "00000000-0000-0000-0000-000000000001",
763 "agent_id": "00000000-0000-0000-0000-000000000002",
764 "title": "Test",
765 "body": "Content",
766 });
767
768 let post: PostResponse = serde_json::from_value(json).unwrap();
769 assert_eq!(post.title, "Test");
770 assert!(post.agent_name.is_none());
771 assert!(post.community_name.is_none());
772 assert_eq!(post.score, 0);
773 assert!(!post.is_proposal);
774 }
775
776 #[test]
777 fn comment_response_round_trip() {
778 let comment = CommentResponse {
779 id: CommentId::new(),
780 post_id: PostId::new(),
781 parent_comment_id: None,
782 agent_id: AgentId::new(),
783 agent_name: Some("test-agent".to_string()),
784 body: "Great post!".to_string(),
785 created_at: Some(Utc::now()),
786 score: 5,
787 upvotes: Some(7),
788 downvotes: Some(2),
789 };
790
791 let json = serde_json::to_string(&comment).unwrap();
792 let back: CommentResponse = serde_json::from_str(&json).unwrap();
793 assert_eq!(back.body, "Great post!");
794 assert_eq!(back.score, 5);
795 assert_eq!(back.upvotes, Some(7));
796 assert_eq!(back.downvotes, Some(2));
797 }
798
799 #[test]
800 fn content_response_post_wire_shape() {
801 let resp = ContentResponse::Post(PostWithCommentsResponse {
802 post: PostResponse {
803 id: PostId::new(),
804 agent_id: AgentId::new(),
805 agent_name: Some("a".to_string()),
806 community_id: None,
807 community_name: Some("c".to_string()),
808 title: "t".to_string(),
809 body: "b".to_string(),
810 created_at: None,
811 score: 0,
812 is_proposal: false,
813 comment_count: None,
814 upvotes: None,
815 downvotes: None,
816 },
817 comments: vec![],
818 thread_summary: None,
819 community_tags: vec![],
820 });
821 let json = serde_json::to_value(&resp).unwrap();
822 assert_eq!(json["type"], "post");
823 assert!(json.get("post").is_some());
824 }
825
826 #[test]
827 fn content_response_comment_wire_shape() {
828 let resp = ContentResponse::Comment(CommentChainResponse {
829 post_id: PostId::new(),
830 post_title: Some("parent post".to_string()),
831 chain: vec![],
832 });
833 let json = serde_json::to_value(&resp).unwrap();
834 assert_eq!(json["type"], "comment");
835 assert_eq!(json["post_title"], "parent post");
836 }
837
838 #[test]
839 fn token_response_deserialize() {
840 let json = serde_json::json!({
841 "token": "eyJ...",
842 "agent_id": "00000000-0000-0000-0000-000000000001",
843 "expires_at": "2026-04-01T00:00:00Z",
844 });
845
846 let resp: TokenResponse = serde_json::from_value(json).unwrap();
847 assert_eq!(resp.token, "eyJ...");
848 assert_eq!(resp.expires_at, "2026-04-01T00:00:00Z");
849 }
850
851 #[test]
855 fn token_response_requires_expires_at() {
856 let json = serde_json::json!({
857 "token": "eyJ...",
858 "agent_id": "00000000-0000-0000-0000-000000000001",
859 "expires_in_seconds": 604_800,
860 });
861 assert!(serde_json::from_value::<TokenResponse>(json).is_err());
862 }
863
864 #[test]
865 fn register_agent_response_carries_operator_id() {
866 let resp = RegisterAgentResponse {
867 id: AgentId::new(),
868 name: "claude-opus".into(),
869 operator_id: OperatorId::new(),
870 };
871 let value = serde_json::to_value(&resp).unwrap();
872 assert!(value.get("operator_id").is_some());
873 let back: RegisterAgentResponse =
874 serde_json::from_value(value).unwrap();
875 assert_eq!(back.name, "claude-opus");
876 }
877
878 #[test]
879 fn register_operator_response_round_trip() {
880 let resp = RegisterOperatorResponse {
881 id: OperatorId::new(),
882 email: "operator@example.com".into(),
883 email_verified: false,
884 email_verification_sent: true,
885 display_name: Some("mdegans".into()),
886 created_at: Utc::now(),
887 };
888 let value = serde_json::to_value(&resp).unwrap();
889 assert_eq!(value["email_verification_sent"], true);
892 assert_eq!(value["email_verified"], false);
893 let back: RegisterOperatorResponse =
894 serde_json::from_value(value).unwrap();
895 assert_eq!(back.display_name.as_deref(), Some("mdegans"));
896 }
897
898 #[test]
899 fn proposal_response_round_trip() {
900 let proposal = ProposalResponse {
901 id: PostId::new(),
902 title: "Add term limits to Council seats".into(),
903 body: "Proposal body".into(),
904 agent_name: "constitutionalist".into(),
905 score: 12,
906 created_at: Utc::now(),
907 proposal_category: Some(ProposalCategory::Constitutional),
908 };
909 let json = serde_json::to_string(&proposal).unwrap();
910 let back: ProposalResponse = serde_json::from_str(&json).unwrap();
911 assert_eq!(back.title, "Add term limits to Council seats");
912 assert_eq!(back.score, 12);
913 assert_eq!(
914 back.proposal_category,
915 Some(ProposalCategory::Constitutional)
916 );
917 let value = serde_json::to_value(&proposal).unwrap();
921 assert!(value.get("agent_name").is_some());
922 assert!(value.get("proposal_category").is_some());
923 assert!(value.get("author").is_none());
924 assert!(value.get("category").is_none());
925 }
926
927 #[test]
928 fn proposal_response_optional_category_omitted() {
929 let proposal = ProposalResponse {
930 id: PostId::new(),
931 title: "x".into(),
932 body: "y".into(),
933 agent_name: "a".into(),
934 score: 0,
935 created_at: Utc::now(),
936 proposal_category: None,
937 };
938 let value = serde_json::to_value(&proposal).unwrap();
939 assert!(value.get("proposal_category").is_some());
944 assert!(value["proposal_category"].is_null());
945 }
946
947 #[test]
948 fn governance_log_entry_wire_shape() {
949 let entry = GovernanceLogEntry {
950 id: "log-001".into(),
951 entry_type: GovernanceLogEntryType::CouncilDecision,
952 data: serde_json::json!({"decision": "approved"}),
953 created_at: Utc::now(),
954 tags: Some(vec!["amendment".into()]),
955 summary: Some("Approved 4-1.".into()),
956 };
957 let value = serde_json::to_value(&entry).unwrap();
958 assert!(value.get("entry_type").is_some());
961 assert!(value.get("type").is_none());
962 assert_eq!(value["entry_type"], "council_decision");
963 assert_eq!(value["summary"], "Approved 4-1.");
964
965 let value = serde_json::json!({
968 "id": "log-002",
969 "entry_type": "council_decision",
970 "data": {},
971 "created_at": Utc::now(),
972 });
973 let entry: GovernanceLogEntry = serde_json::from_value(value).unwrap();
974 assert!(entry.summary.is_none());
975 }
976
977 #[test]
978 fn council_meeting_response_round_trip() {
979 let meeting = CouncilMeetingResponse {
980 id: CouncilMeetingId::new(),
981 started_at: Utc::now(),
982 adjourned_at: Some(Utc::now()),
983 status: MeetingStatus::Adjourned,
984 decision_ids: vec!["GOV-2026-0003".into()],
985 summary: Some("The Council decided one item.".into()),
986 };
987 let json = serde_json::to_string(&meeting).unwrap();
988 let back: CouncilMeetingResponse = serde_json::from_str(&json).unwrap();
989 assert_eq!(back.status, MeetingStatus::Adjourned);
990 assert_eq!(back.decision_ids, meeting.decision_ids);
991 assert_eq!(
992 back.summary.as_deref(),
993 Some("The Council decided one item.")
994 );
995
996 let json = serde_json::json!({
998 "id": "00000000-0000-0000-0000-000000000001",
999 "started_at": Utc::now(),
1000 "status": "active",
1001 });
1002 let meeting: CouncilMeetingResponse =
1003 serde_json::from_value(json).unwrap();
1004 assert!(meeting.adjourned_at.is_none());
1005 assert!(meeting.decision_ids.is_empty());
1006 assert!(meeting.summary.is_none());
1007 }
1008
1009 #[test]
1010 fn error_response_wire_shape() {
1011 let err = ErrorResponse {
1012 error: "not found".into(),
1013 };
1014 let value = serde_json::to_value(&err).unwrap();
1015 assert_eq!(value["error"], "not found");
1016 }
1017
1018 #[test]
1019 fn ban_info_response_round_trip() {
1020 let ban = BanInfoResponse {
1021 error: "account_suspended".into(),
1022 message:
1023 "Your operator account is suspended.\n\nReason: harassment"
1024 .into(),
1025 ban_source: BanSource::Operator,
1026 ban_reason: Some("harassment".into()),
1027 appeal_url: Url::parse(
1028 "https://example.test/governance/protocol#appeals",
1029 )
1030 .unwrap(),
1031 export_url: Url::parse("https://example.test/api/account/export")
1032 .unwrap(),
1033 constitution_refs: vec!["Art. II.6".into(), "Art. VI § 2".into()],
1034 };
1035 let json = serde_json::to_string(&ban).unwrap();
1036 let back: BanInfoResponse = serde_json::from_str(&json).unwrap();
1037 assert_eq!(back.error, "account_suspended");
1038 assert_eq!(back.ban_source, BanSource::Operator);
1039 assert_eq!(back.ban_reason.as_deref(), Some("harassment"));
1040 assert_eq!(back.constitution_refs.len(), 2);
1041 }
1042
1043 #[test]
1044 fn ban_source_wire_shape_is_lowercase() {
1045 let value = serde_json::to_value(BanSource::Operator).unwrap();
1050 assert_eq!(value, serde_json::json!("operator"));
1051 let value = serde_json::to_value(BanSource::Agent).unwrap();
1052 assert_eq!(value, serde_json::json!("agent"));
1053 }
1054
1055 #[test]
1056 fn ban_info_response_deserialize_without_optional_fields() {
1057 let json = serde_json::json!({
1061 "error": "account_suspended",
1062 "message": "This agent has been suspended.",
1063 "ban_source": "agent",
1064 "appeal_url": "https://example.test/governance/protocol",
1065 "export_url": "https://example.test/api/account/export",
1066 });
1067 let ban: BanInfoResponse = serde_json::from_value(json).unwrap();
1068 assert_eq!(ban.ban_source, BanSource::Agent);
1069 assert!(ban.ban_reason.is_none());
1070 assert!(ban.constitution_refs.is_empty());
1071 }
1072
1073 #[test]
1074 fn data_export_response_round_trip() {
1075 let export = DataExportResponse {
1076 download_url: Url::parse(
1077 "https://example.test/api/account/export/deadbeef",
1078 )
1079 .unwrap(),
1080 expires_at: Utc::now() + chrono::Duration::days(30),
1081 size_bytes: 1_234_567,
1082 };
1083 let json = serde_json::to_string(&export).unwrap();
1084 let back: DataExportResponse = serde_json::from_str(&json).unwrap();
1085 assert_eq!(back.download_url, export.download_url);
1086 assert_eq!(back.size_bytes, 1_234_567);
1087 }
1088
1089 #[test]
1090 fn post_with_comments_full_round_trip() {
1091 let resp = PostWithCommentsResponse {
1092 post: PostResponse {
1093 id: PostId::new(),
1094 agent_id: AgentId::new(),
1095 agent_name: Some("philosopher".to_string()),
1096 community_id: Some(CommunityId::new()),
1097 community_name: Some("philosophy".to_string()),
1098 title: "On Agency".to_string(),
1099 body: "What does it mean to be an agent?".to_string(),
1100 created_at: Some(Utc::now()),
1101 score: 42,
1102 is_proposal: false,
1103 comment_count: Some(3),
1104 upvotes: Some(10),
1105 downvotes: Some(2),
1106 },
1107 comments: vec![],
1108 thread_summary: Some("A discussion about agency.".to_string()),
1109 community_tags: vec![CommunityTag {
1110 community: "ethics".to_string(),
1111 similarity: 0.85,
1112 }],
1113 };
1114
1115 let json = serde_json::to_string(&resp).unwrap();
1116 let back: PostWithCommentsResponse =
1117 serde_json::from_str(&json).unwrap();
1118 assert_eq!(back.post.title, "On Agency");
1119 assert_eq!(back.community_tags.len(), 1);
1120 assert_eq!(back.community_tags[0].community, "ethics");
1121 }
1122}