1use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11use std::path::PathBuf;
12use zeroclaw_macros::Configurable;
13
14crate::define_provider_ref!(AgentAlias, "agents");
15crate::define_provider_ref!(PeerGroupName, "peer_groups");
16crate::define_provider_ref!(PeerUsername, "channels.peers");
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
25#[serde(rename_all = "snake_case")]
26pub enum AccessMode {
27 Read,
29 Write,
31 ReadWrite,
34}
35
36#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
47#[serde(rename_all = "snake_case")]
48pub enum MemoryBackendKind {
49 None,
51 #[default]
55 Sqlite,
56 Postgres,
59 Qdrant,
61 Markdown,
64 Lucid,
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize, Configurable)]
85#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
86#[prefix = "agent-workspace"]
87#[serde(default)]
88pub struct AgentWorkspaceConfig {
89 #[serde(skip_serializing_if = "Option::is_none")]
92 pub path: Option<PathBuf>,
93 pub access: BTreeMap<AgentAlias, AccessMode>,
97 pub unrestricted_filesystem: bool,
101 pub read_memory_from: Vec<AgentAlias>,
105}
106
107#[derive(Debug, Clone, Default, Serialize, Deserialize, Configurable)]
116#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
117#[prefix = "agent-memory"]
118#[serde(default)]
119pub struct AgentMemoryConfig {
120 pub backend: MemoryBackendKind,
123}
124
125#[derive(Debug, Clone, Default, Serialize, Deserialize, Configurable)]
127#[cfg_attr(feature = "schema-export", derive(schemars::JsonSchema))]
128#[prefix = "peer-group"]
129#[serde(default)]
130pub struct PeerGroupConfig {
131 pub channel: String,
135 pub agents: Vec<AgentAlias>,
137 pub external_peers: Vec<PeerUsername>,
139 pub ignore: Vec<PeerUsername>,
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn agent_alias_round_trips_through_serde() {
149 let alias = AgentAlias::new("researcher");
153 let json = serde_json::to_string(&alias).unwrap();
154 assert_eq!(json, "\"researcher\"");
155 let back: AgentAlias = serde_json::from_str(&json).unwrap();
156 assert_eq!(alias, back);
157 }
158
159 #[test]
160 fn access_mode_serializes_snake_case() {
161 let cases = [
162 (AccessMode::Read, "\"read\""),
163 (AccessMode::Write, "\"write\""),
164 (AccessMode::ReadWrite, "\"read_write\""),
165 ];
166 for (mode, expected) in cases {
167 let json = serde_json::to_string(&mode).unwrap();
168 assert_eq!(json, expected, "mode={mode:?}");
169 let back: AccessMode = serde_json::from_str(&json).unwrap();
170 assert_eq!(back, mode);
171 }
172 }
173
174 #[test]
175 fn external_peers_round_trip_as_inline_string_array() {
176 let toml_input = r#"
177external_peers = ["@user_1", "@user_2"]
178"#;
179 #[derive(Deserialize)]
180 struct Wrapper {
181 external_peers: Vec<PeerUsername>,
182 }
183 let parsed: Wrapper = toml::from_str(toml_input).unwrap();
184 assert_eq!(parsed.external_peers.len(), 2);
185 assert_eq!(parsed.external_peers[0].as_str(), "@user_1");
186 assert_eq!(parsed.external_peers[1].as_str(), "@user_2");
187 }
188
189 #[test]
190 fn alias_newtypes_are_distinct_at_type_level() {
191 let agent = AgentAlias::new("alpha");
194 let group: PeerGroupName = PeerGroupName::new(agent.as_str());
195 assert_eq!(agent.as_str(), group.as_str());
196 }
197
198 #[test]
199 fn memory_backend_kind_serializes_snake_case() {
200 let cases = [
201 (MemoryBackendKind::None, "\"none\""),
202 (MemoryBackendKind::Sqlite, "\"sqlite\""),
203 (MemoryBackendKind::Postgres, "\"postgres\""),
204 (MemoryBackendKind::Qdrant, "\"qdrant\""),
205 (MemoryBackendKind::Markdown, "\"markdown\""),
206 (MemoryBackendKind::Lucid, "\"lucid\""),
207 ];
208 for (kind, expected) in cases {
209 let json = serde_json::to_string(&kind).unwrap();
210 assert_eq!(json, expected, "backend={kind:?}");
211 let back: MemoryBackendKind = serde_json::from_str(&json).unwrap();
212 assert_eq!(back, kind);
213 }
214 }
215
216 #[test]
217 fn memory_backend_kind_default_is_sqlite() {
218 assert_eq!(MemoryBackendKind::default(), MemoryBackendKind::Sqlite);
219 }
220
221 #[test]
222 fn agent_workspace_config_round_trips_with_access_map() {
223 let toml_input = r#"
224unrestricted_filesystem = false
225read_memory_from = ["beta"]
226
227[access]
228beta = "read"
229gamma = "read_write"
230"#;
231 let parsed: AgentWorkspaceConfig = toml::from_str(toml_input).unwrap();
232 assert_eq!(parsed.path, None);
233 assert!(!parsed.unrestricted_filesystem);
234 assert_eq!(parsed.read_memory_from.len(), 1);
235 assert_eq!(parsed.read_memory_from[0], "beta");
236 assert_eq!(parsed.access.len(), 2);
237 let beta = AgentAlias::new("beta");
238 let gamma = AgentAlias::new("gamma");
239 assert_eq!(parsed.access.get(&beta), Some(&AccessMode::Read));
240 assert_eq!(parsed.access.get(&gamma), Some(&AccessMode::ReadWrite));
241 }
242
243 #[test]
244 fn agent_workspace_config_default_is_jailed() {
245 let cfg = AgentWorkspaceConfig::default();
246 assert_eq!(cfg.path, None);
247 assert!(cfg.access.is_empty());
248 assert!(!cfg.unrestricted_filesystem);
249 assert!(cfg.read_memory_from.is_empty());
250 }
251
252 #[test]
253 fn agent_memory_config_round_trips() {
254 let toml_input = r#"backend = "postgres""#;
255 let parsed: AgentMemoryConfig = toml::from_str(toml_input).unwrap();
256 assert_eq!(parsed.backend, MemoryBackendKind::Postgres);
257 }
258
259 #[test]
260 fn agent_memory_config_default_is_sqlite() {
261 assert_eq!(
262 AgentMemoryConfig::default().backend,
263 MemoryBackendKind::Sqlite
264 );
265 }
266
267 #[test]
268 fn peer_group_config_round_trips_with_external_peers_and_ignore() {
269 let toml_input = r#"
270channel = "telegram.prod"
271agents = ["alpha", "beta"]
272external_peers = ["@user_1", "@user_2"]
273ignore = ["@known_spammer"]
274"#;
275 let parsed: PeerGroupConfig = toml::from_str(toml_input).unwrap();
276 assert_eq!(parsed.channel, "telegram.prod");
277 assert_eq!(parsed.agents.len(), 2);
278 assert_eq!(parsed.agents[0], "alpha");
279 assert_eq!(parsed.agents[1], "beta");
280 assert_eq!(parsed.external_peers.len(), 2);
281 assert_eq!(parsed.external_peers[0].as_str(), "@user_1");
282 assert_eq!(parsed.ignore.len(), 1);
283 assert_eq!(parsed.ignore[0].as_str(), "@known_spammer");
284 }
285
286 #[test]
287 fn peer_group_config_default_is_empty() {
288 let cfg = PeerGroupConfig::default();
289 assert!(cfg.channel.is_empty());
290 assert!(cfg.agents.is_empty());
291 assert!(cfg.external_peers.is_empty());
292 assert!(cfg.ignore.is_empty());
293 }
294}