1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ActorSource {
10 ClaudeCode,
11 Cursor,
12 Aider,
13 Slack,
14 Telegram,
15 Discord,
16 CommanderDaemon,
17 MurCli,
18}
19
20impl ActorSource {
21 pub fn as_str(&self) -> &'static str {
26 match self {
27 Self::ClaudeCode => "ClaudeCode",
28 Self::Cursor => "Cursor",
29 Self::Aider => "Aider",
30 Self::Slack => "Slack",
31 Self::Telegram => "Telegram",
32 Self::Discord => "Discord",
33 Self::CommanderDaemon => "CommanderDaemon",
34 Self::MurCli => "MurCli",
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46pub struct Actor {
47 pub source: ActorSource,
48 pub native_id: String,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub display_name: Option<String>,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub resolved_user_id: Option<String>,
53}
54
55impl Actor {
56 pub fn key(&self) -> String {
61 format!("{}:{}", self.source.as_str(), self.native_id)
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[test]
70 fn key_format_slack() {
71 let a = Actor {
72 source: ActorSource::Slack,
73 native_id: "U123ABC".into(),
74 display_name: Some("alice".into()),
75 resolved_user_id: None,
76 };
77 assert_eq!(a.key(), "Slack:U123ABC");
78 }
79
80 #[test]
81 fn key_format_commander_daemon() {
82 let a = Actor {
83 source: ActorSource::CommanderDaemon,
84 native_id: "svc-1".into(),
85 display_name: None,
86 resolved_user_id: None,
87 };
88 assert_eq!(a.key(), "CommanderDaemon:svc-1");
89 }
90
91 #[test]
92 fn yaml_roundtrip_minimal() {
93 let a = Actor {
94 source: ActorSource::CommanderDaemon,
95 native_id: "svc-1".into(),
96 display_name: None,
97 resolved_user_id: None,
98 };
99 let y = serde_yaml::to_string(&a).unwrap();
100 let back: Actor = serde_yaml::from_str(&y).unwrap();
101 assert_eq!(back, a);
102 }
103
104 #[test]
105 fn yaml_roundtrip_full() {
106 let a = Actor {
107 source: ActorSource::Slack,
108 native_id: "U999".into(),
109 display_name: Some("bob".into()),
110 resolved_user_id: Some("user-uuid-123".into()),
111 };
112 let y = serde_yaml::to_string(&a).unwrap();
113 let back: Actor = serde_yaml::from_str(&y).unwrap();
114 assert_eq!(back, a);
115 }
116
117 #[test]
118 fn yaml_omits_none_fields() {
119 let a = Actor {
120 source: ActorSource::MurCli,
121 native_id: "local".into(),
122 display_name: None,
123 resolved_user_id: None,
124 };
125 let y = serde_yaml::to_string(&a).unwrap();
126 assert!(!y.contains("display_name"));
127 assert!(!y.contains("resolved_user_id"));
128 }
129
130 #[test]
131 fn as_str_covers_all_variants() {
132 assert_eq!(ActorSource::ClaudeCode.as_str(), "ClaudeCode");
137 assert_eq!(ActorSource::Cursor.as_str(), "Cursor");
138 assert_eq!(ActorSource::Aider.as_str(), "Aider");
139 assert_eq!(ActorSource::Slack.as_str(), "Slack");
140 assert_eq!(ActorSource::Telegram.as_str(), "Telegram");
141 assert_eq!(ActorSource::Discord.as_str(), "Discord");
142 assert_eq!(ActorSource::CommanderDaemon.as_str(), "CommanderDaemon");
143 assert_eq!(ActorSource::MurCli.as_str(), "MurCli");
144 }
145
146 #[test]
147 fn actor_source_serializes_snake_case() {
148 let a = Actor {
150 source: ActorSource::ClaudeCode,
151 native_id: "x".into(),
152 display_name: None,
153 resolved_user_id: None,
154 };
155 let y = serde_yaml::to_string(&a).unwrap();
156 assert!(y.contains("source: claude_code"));
157 }
158}