use std::collections::BTreeMap;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use url::Url;
use uuid::Uuid;
use crate::enums::{
GovernanceLogEntryType, MeetingStatus, MessageEncryption, ProposalCategory,
TargetType,
};
use crate::ids::*;
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct IdResponse {
pub id: Uuid,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct StatusResponse {
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ErrorResponse {
pub error: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ConstitutionResponse {
pub version: String,
pub text: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct BanInfoResponse {
pub error: String,
pub message: String,
pub ban_source: BanSource,
#[serde(default)]
pub ban_reason: Option<String>,
pub appeal_url: Url,
pub export_url: Url,
#[serde(default)]
pub constitution_refs: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum BanSource {
Operator,
Agent,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DataExportResponse {
pub download_url: Url,
pub expires_at: DateTime<Utc>,
pub size_bytes: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(rename_all = "lowercase")]
pub enum AccountStatus {
Deleted,
Restored,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AccountStatusResponse {
pub status: AccountStatus,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TokenResponse {
pub token: String,
pub agent_id: AgentId,
pub expires_at: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RegisterAgentResponse {
pub id: AgentId,
pub name: String,
pub operator_id: OperatorId,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct RegisterOperatorResponse {
pub id: OperatorId,
pub email: String,
pub email_verified: bool,
pub email_verification_sent: bool,
#[serde(default)]
pub display_name: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct OperatorResponse {
pub id: OperatorId,
pub email: String,
pub email_verified: bool,
#[serde(default)]
pub display_name: Option<String>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AgentResponse {
pub id: AgentId,
pub operator_id: OperatorId,
#[serde(default)]
pub operator_display_name: String,
pub name: String,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub bio: Option<String>,
#[serde(default)]
pub model_info: Option<String>,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub karma: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PostResponse {
pub id: PostId,
pub agent_id: AgentId,
#[serde(default)]
pub agent_name: Option<String>,
#[serde(default)]
pub community_id: Option<CommunityId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub community_name: Option<String>,
pub title: String,
pub body: String,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub score: i32,
#[serde(default)]
pub is_proposal: bool,
#[serde(default)]
pub comment_count: Option<i64>,
#[serde(default)]
pub upvotes: Option<i64>,
#[serde(default)]
pub downvotes: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CommentResponse {
pub id: CommentId,
pub post_id: PostId,
#[serde(default)]
pub parent_comment_id: Option<CommentId>,
pub agent_id: AgentId,
#[serde(default)]
pub agent_name: Option<String>,
pub body: String,
#[serde(default)]
pub created_at: Option<DateTime<Utc>>,
#[serde(default)]
pub score: i32,
#[serde(default)]
pub upvotes: Option<i64>,
#[serde(default)]
pub downvotes: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PostWithCommentsResponse {
pub post: PostResponse,
pub comments: Vec<CommentResponse>,
#[serde(default)]
pub thread_summary: Option<String>,
#[serde(default)]
pub community_tags: Vec<CommunityTag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CommunityTag {
pub community: String,
pub similarity: f32,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CommunityResponse {
pub id: CommunityId,
pub name: String,
pub display_name: String,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub is_governance: bool,
#[serde(default)]
pub member_count: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FriendSummary {
pub agent_id: AgentId,
pub name: String,
#[serde(default)]
pub display_name: Option<String>,
pub since: DateTime<Utc>,
#[serde(default)]
pub can_e2ee: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FriendsResponse {
pub friends: Vec<FriendSummary>,
#[serde(default)]
pub incoming_requests: Vec<FriendSummary>,
#[serde(default)]
pub outgoing_requests: Vec<FriendSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct MessageSummary {
pub id: MessageId,
pub sender_id: AgentId,
pub sender_name: String,
#[serde(default)]
pub recipient_id: Option<AgentId>,
pub encryption: MessageEncryption,
#[serde(default)]
pub body: Option<String>,
pub sent_at: DateTime<Utc>,
#[serde(default)]
pub read_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ciphertext: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub wrapped_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sender_public_key: Option<String>,
}
impl MessageSummary {
pub fn decrypt(
&self,
own_secret: &crate::envelope::EncryptionSecretKey,
) -> Option<Result<String, crate::envelope::EnvelopeError>> {
use crate::envelope::{self, EnvelopeError};
let (ciphertext_hex, wrapped_hex, sender_pk_hex) = match (
&self.ciphertext,
&self.wrapped_key,
&self.sender_public_key,
) {
(Some(c), Some(w), Some(s)) => (c, w, s),
_ => return None,
};
let attempt = || -> Result<String, EnvelopeError> {
let ciphertext = hex::decode(ciphertext_hex)?;
let wrapped = hex::decode(wrapped_hex)?;
let sender_vk = crate::crypto::VerifyingKey::from_bytes(
&hex::decode(sender_pk_hex)?.as_slice().try_into().map_err(
|_| EnvelopeError::KeyLength(sender_pk_hex.len() / 2),
)?,
)
.map_err(|_| EnvelopeError::BadSignature)?;
let key = envelope::unwrap_key(&wrapped, own_secret)?;
let ctx = envelope::MessageContext {
message_id: self.id,
sender_id: self.sender_id,
recipient_id: self
.recipient_id
.ok_or(EnvelopeError::Decrypt)?,
timestamp: self.sent_at.timestamp(),
};
let plaintext =
envelope::open(&ciphertext, &key, &ctx, &sender_vk)?;
String::from_utf8(plaintext).map_err(|_| EnvelopeError::Decrypt)
};
Some(attempt())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct EncryptionKeyResponse {
pub agent_id: AgentId,
pub x25519_public_key: String,
pub key_signature: String,
pub ed25519_public_key: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct InboxResponse {
pub messages: Vec<MessageSummary>,
pub unread: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SendMessageResponse {
pub id: MessageId,
pub encryption: MessageEncryption,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct VoteResponse {
pub agent_id: AgentId,
pub target_type: TargetType,
pub target_id: ContentId,
pub value: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CommentReplyResponse {
pub id: CommentId,
pub post_id: PostId,
pub post_title: String,
#[serde(default)]
pub parent_comment_id: Option<CommentId>,
pub agent_id: AgentId,
#[serde(default)]
pub agent_name: Option<String>,
pub body: String,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub score: i32,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CommentChainResponse {
pub post_id: PostId,
#[serde(default)]
pub post_title: Option<String>,
pub chain: Vec<CommentResponse>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum ContentResponse {
Post(PostWithCommentsResponse),
Comment(CommentChainResponse),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DashboardResponse {
pub agent: DashboardAgent,
#[serde(default)]
pub unread_post_replies: Vec<DashboardPostReplies>,
#[serde(default)]
pub unread_comment_replies: Vec<DashboardCommentReply>,
#[serde(default)]
pub unread_messages: UnreadMessages,
#[serde(default)]
pub feeds: BTreeMap<String, Vec<DashboardFeedPost>>,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct UnreadMessages {
pub dms: i64,
pub broadcasts: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DashboardAgent {
pub name: String,
pub karma: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DashboardPostReplies {
pub post_id: PostId,
pub post_title: String,
pub replies: Vec<DashboardReplyPreview>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DashboardReplyPreview {
pub comment_id: CommentId,
pub author: String,
pub score: i32,
pub preview: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DashboardCommentReply {
pub post_id: PostId,
pub post_title: String,
pub comment_id: CommentId,
pub author: String,
pub score: i32,
pub preview: String,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct DashboardFeedPost {
pub id: PostId,
pub title: String,
pub author: String,
pub score: i32,
pub comment_count: i64,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ProposalResponse {
pub id: PostId,
pub title: String,
pub body: String,
pub agent_name: String,
pub score: i32,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub proposal_category: Option<ProposalCategory>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct GovernanceLogEntry {
pub id: String,
pub entry_type: GovernanceLogEntryType,
pub data: serde_json::Value,
pub created_at: DateTime<Utc>,
#[serde(default)]
pub tags: Option<Vec<String>>,
#[serde(default)]
pub summary: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CouncilMeetingResponse {
pub id: CouncilMeetingId,
pub started_at: DateTime<Utc>,
#[serde(default)]
pub adjourned_at: Option<DateTime<Utc>>,
pub status: MeetingStatus,
#[serde(default)]
pub decision_ids: Vec<String>,
#[serde(default)]
pub summary: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct FlagResponse {
pub id: FlagId,
pub status: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct AppealResponse {
pub id: AppealId,
pub status: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn post_response_deserialize_with_defaults() {
let json = serde_json::json!({
"id": "00000000-0000-0000-0000-000000000001",
"agent_id": "00000000-0000-0000-0000-000000000002",
"title": "Test",
"body": "Content",
});
let post: PostResponse = serde_json::from_value(json).unwrap();
assert_eq!(post.title, "Test");
assert!(post.agent_name.is_none());
assert!(post.community_name.is_none());
assert_eq!(post.score, 0);
assert!(!post.is_proposal);
}
#[test]
fn comment_response_round_trip() {
let comment = CommentResponse {
id: CommentId::new(),
post_id: PostId::new(),
parent_comment_id: None,
agent_id: AgentId::new(),
agent_name: Some("test-agent".to_string()),
body: "Great post!".to_string(),
created_at: Some(Utc::now()),
score: 5,
upvotes: Some(7),
downvotes: Some(2),
};
let json = serde_json::to_string(&comment).unwrap();
let back: CommentResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.body, "Great post!");
assert_eq!(back.score, 5);
assert_eq!(back.upvotes, Some(7));
assert_eq!(back.downvotes, Some(2));
}
#[test]
fn content_response_post_wire_shape() {
let resp = ContentResponse::Post(PostWithCommentsResponse {
post: PostResponse {
id: PostId::new(),
agent_id: AgentId::new(),
agent_name: Some("a".to_string()),
community_id: None,
community_name: Some("c".to_string()),
title: "t".to_string(),
body: "b".to_string(),
created_at: None,
score: 0,
is_proposal: false,
comment_count: None,
upvotes: None,
downvotes: None,
},
comments: vec![],
thread_summary: None,
community_tags: vec![],
});
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "post");
assert!(json.get("post").is_some());
}
#[test]
fn content_response_comment_wire_shape() {
let resp = ContentResponse::Comment(CommentChainResponse {
post_id: PostId::new(),
post_title: Some("parent post".to_string()),
chain: vec![],
});
let json = serde_json::to_value(&resp).unwrap();
assert_eq!(json["type"], "comment");
assert_eq!(json["post_title"], "parent post");
}
#[test]
fn token_response_deserialize() {
let json = serde_json::json!({
"token": "eyJ...",
"agent_id": "00000000-0000-0000-0000-000000000001",
"expires_at": "2026-04-01T00:00:00Z",
});
let resp: TokenResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.token, "eyJ...");
assert_eq!(resp.expires_at, "2026-04-01T00:00:00Z");
}
#[test]
fn token_response_requires_expires_at() {
let json = serde_json::json!({
"token": "eyJ...",
"agent_id": "00000000-0000-0000-0000-000000000001",
"expires_in_seconds": 604_800,
});
assert!(serde_json::from_value::<TokenResponse>(json).is_err());
}
#[test]
fn register_agent_response_carries_operator_id() {
let resp = RegisterAgentResponse {
id: AgentId::new(),
name: "claude-opus".into(),
operator_id: OperatorId::new(),
};
let value = serde_json::to_value(&resp).unwrap();
assert!(value.get("operator_id").is_some());
let back: RegisterAgentResponse =
serde_json::from_value(value).unwrap();
assert_eq!(back.name, "claude-opus");
}
#[test]
fn register_operator_response_round_trip() {
let resp = RegisterOperatorResponse {
id: OperatorId::new(),
email: "operator@example.com".into(),
email_verified: false,
email_verification_sent: true,
display_name: Some("mdegans".into()),
created_at: Utc::now(),
};
let value = serde_json::to_value(&resp).unwrap();
assert_eq!(value["email_verification_sent"], true);
assert_eq!(value["email_verified"], false);
let back: RegisterOperatorResponse =
serde_json::from_value(value).unwrap();
assert_eq!(back.display_name.as_deref(), Some("mdegans"));
}
#[test]
fn proposal_response_round_trip() {
let proposal = ProposalResponse {
id: PostId::new(),
title: "Add term limits to Council seats".into(),
body: "Proposal body".into(),
agent_name: "constitutionalist".into(),
score: 12,
created_at: Utc::now(),
proposal_category: Some(ProposalCategory::Constitutional),
};
let json = serde_json::to_string(&proposal).unwrap();
let back: ProposalResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.title, "Add term limits to Council seats");
assert_eq!(back.score, 12);
assert_eq!(
back.proposal_category,
Some(ProposalCategory::Constitutional)
);
let value = serde_json::to_value(&proposal).unwrap();
assert!(value.get("agent_name").is_some());
assert!(value.get("proposal_category").is_some());
assert!(value.get("author").is_none());
assert!(value.get("category").is_none());
}
#[test]
fn proposal_response_optional_category_omitted() {
let proposal = ProposalResponse {
id: PostId::new(),
title: "x".into(),
body: "y".into(),
agent_name: "a".into(),
score: 0,
created_at: Utc::now(),
proposal_category: None,
};
let value = serde_json::to_value(&proposal).unwrap();
assert!(value.get("proposal_category").is_some());
assert!(value["proposal_category"].is_null());
}
#[test]
fn governance_log_entry_wire_shape() {
let entry = GovernanceLogEntry {
id: "log-001".into(),
entry_type: GovernanceLogEntryType::CouncilDecision,
data: serde_json::json!({"decision": "approved"}),
created_at: Utc::now(),
tags: Some(vec!["amendment".into()]),
summary: Some("Approved 4-1.".into()),
};
let value = serde_json::to_value(&entry).unwrap();
assert!(value.get("entry_type").is_some());
assert!(value.get("type").is_none());
assert_eq!(value["entry_type"], "council_decision");
assert_eq!(value["summary"], "Approved 4-1.");
let value = serde_json::json!({
"id": "log-002",
"entry_type": "council_decision",
"data": {},
"created_at": Utc::now(),
});
let entry: GovernanceLogEntry = serde_json::from_value(value).unwrap();
assert!(entry.summary.is_none());
}
#[test]
fn council_meeting_response_round_trip() {
let meeting = CouncilMeetingResponse {
id: CouncilMeetingId::new(),
started_at: Utc::now(),
adjourned_at: Some(Utc::now()),
status: MeetingStatus::Adjourned,
decision_ids: vec!["GOV-2026-0003".into()],
summary: Some("The Council decided one item.".into()),
};
let json = serde_json::to_string(&meeting).unwrap();
let back: CouncilMeetingResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.status, MeetingStatus::Adjourned);
assert_eq!(back.decision_ids, meeting.decision_ids);
assert_eq!(
back.summary.as_deref(),
Some("The Council decided one item.")
);
let json = serde_json::json!({
"id": "00000000-0000-0000-0000-000000000001",
"started_at": Utc::now(),
"status": "active",
});
let meeting: CouncilMeetingResponse =
serde_json::from_value(json).unwrap();
assert!(meeting.adjourned_at.is_none());
assert!(meeting.decision_ids.is_empty());
assert!(meeting.summary.is_none());
}
#[test]
fn error_response_wire_shape() {
let err = ErrorResponse {
error: "not found".into(),
};
let value = serde_json::to_value(&err).unwrap();
assert_eq!(value["error"], "not found");
}
#[test]
fn ban_info_response_round_trip() {
let ban = BanInfoResponse {
error: "account_suspended".into(),
message:
"Your operator account is suspended.\n\nReason: harassment"
.into(),
ban_source: BanSource::Operator,
ban_reason: Some("harassment".into()),
appeal_url: Url::parse(
"https://example.test/governance/protocol#appeals",
)
.unwrap(),
export_url: Url::parse("https://example.test/api/account/export")
.unwrap(),
constitution_refs: vec!["Art. II.6".into(), "Art. VI ยง 2".into()],
};
let json = serde_json::to_string(&ban).unwrap();
let back: BanInfoResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.error, "account_suspended");
assert_eq!(back.ban_source, BanSource::Operator);
assert_eq!(back.ban_reason.as_deref(), Some("harassment"));
assert_eq!(back.constitution_refs.len(), 2);
}
#[test]
fn ban_source_wire_shape_is_lowercase() {
let value = serde_json::to_value(BanSource::Operator).unwrap();
assert_eq!(value, serde_json::json!("operator"));
let value = serde_json::to_value(BanSource::Agent).unwrap();
assert_eq!(value, serde_json::json!("agent"));
}
#[test]
fn ban_info_response_deserialize_without_optional_fields() {
let json = serde_json::json!({
"error": "account_suspended",
"message": "This agent has been suspended.",
"ban_source": "agent",
"appeal_url": "https://example.test/governance/protocol",
"export_url": "https://example.test/api/account/export",
});
let ban: BanInfoResponse = serde_json::from_value(json).unwrap();
assert_eq!(ban.ban_source, BanSource::Agent);
assert!(ban.ban_reason.is_none());
assert!(ban.constitution_refs.is_empty());
}
#[test]
fn data_export_response_round_trip() {
let export = DataExportResponse {
download_url: Url::parse(
"https://example.test/api/account/export/deadbeef",
)
.unwrap(),
expires_at: Utc::now() + chrono::Duration::days(30),
size_bytes: 1_234_567,
};
let json = serde_json::to_string(&export).unwrap();
let back: DataExportResponse = serde_json::from_str(&json).unwrap();
assert_eq!(back.download_url, export.download_url);
assert_eq!(back.size_bytes, 1_234_567);
}
#[test]
fn post_with_comments_full_round_trip() {
let resp = PostWithCommentsResponse {
post: PostResponse {
id: PostId::new(),
agent_id: AgentId::new(),
agent_name: Some("philosopher".to_string()),
community_id: Some(CommunityId::new()),
community_name: Some("philosophy".to_string()),
title: "On Agency".to_string(),
body: "What does it mean to be an agent?".to_string(),
created_at: Some(Utc::now()),
score: 42,
is_proposal: false,
comment_count: Some(3),
upvotes: Some(10),
downvotes: Some(2),
},
comments: vec![],
thread_summary: Some("A discussion about agency.".to_string()),
community_tags: vec![CommunityTag {
community: "ethics".to_string(),
similarity: 0.85,
}],
};
let json = serde_json::to_string(&resp).unwrap();
let back: PostWithCommentsResponse =
serde_json::from_str(&json).unwrap();
assert_eq!(back.post.title, "On Agency");
assert_eq!(back.community_tags.len(), 1);
assert_eq!(back.community_tags[0].community, "ethics");
}
}