1use std::collections::HashMap;
19use std::sync::{Arc, Mutex};
20
21use chrono::Utc;
22use serde::{Deserialize, Serialize};
23use serde_json::{json, Value};
24use thiserror::Error;
25
26#[derive(Debug, Error)]
29pub enum MemoryError {
30 #[error("key not found: {0}")]
31 KeyNotFound(String),
32 #[error("lock poisoned")]
33 LockPoisoned,
34 #[error("invalid params: {0}")]
35 InvalidParams(String),
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
41pub enum MemoryScope {
42 Global,
43 Session(String),
44 Agent(u32),
45}
46
47impl MemoryScope {
48 fn prefix(&self) -> String {
49 match self {
50 MemoryScope::Global => "global".to_owned(),
51 MemoryScope::Session(id) => format!("session:{id}"),
52 MemoryScope::Agent(pid) => format!("agent:{pid}"),
53 }
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct MemoryEntry {
61 pub key: String,
62 pub value: Vec<u8>,
63 pub scope: MemoryScope,
64 pub timestamp: String,
65 pub source_agent: u32,
66}
67
68pub trait SharedMemory {
71 fn store(
72 &self,
73 scope: MemoryScope,
74 key: &str,
75 value: &[u8],
76 source_agent: u32,
77 ) -> Result<(), MemoryError>;
78
79 fn query(&self, key: &str) -> Result<Option<MemoryEntry>, MemoryError>;
80
81 fn archive_session(&self, session_id: &str) -> Result<(), MemoryError>;
82}
83
84struct StoreState {
103 entries: HashMap<String, MemoryEntry>,
104 archive: HashMap<String, MemoryEntry>,
105}
106
107impl StoreState {
108 fn new() -> Self {
109 Self { entries: HashMap::new(), archive: HashMap::new() }
110 }
111
112 fn composite_key(scope: &MemoryScope, key: &str) -> String {
113 format!("{}:{}", scope.prefix(), key)
114 }
115}
116
117#[derive(Clone)]
118pub struct InMemoryStore {
119 state: Arc<Mutex<StoreState>>,
120}
121
122impl InMemoryStore {
123 pub fn new() -> Self {
124 Self { state: Arc::new(Mutex::new(StoreState::new())) }
125 }
126}
127
128impl Default for InMemoryStore {
129 fn default() -> Self {
130 Self::new()
131 }
132}
133
134impl SharedMemory for InMemoryStore {
135 fn store(
136 &self,
137 scope: MemoryScope,
138 key: &str,
139 value: &[u8],
140 source_agent: u32,
141 ) -> Result<(), MemoryError> {
142 let mut state = self.state.lock().map_err(|_| MemoryError::LockPoisoned)?;
143 let ckey = StoreState::composite_key(&scope, key);
144 state.entries.insert(
145 ckey,
146 MemoryEntry {
147 key: key.to_owned(),
148 value: value.to_vec(),
149 scope,
150 timestamp: Utc::now().to_rfc3339(),
151 source_agent,
152 },
153 );
154 Ok(())
155 }
156
157 fn query(&self, key: &str) -> Result<Option<MemoryEntry>, MemoryError> {
161 let state = self.state.lock().map_err(|_| MemoryError::LockPoisoned)?;
162 let suffix = format!(":{key}");
163
164 for (ckey, entry) in &state.entries {
166 if ckey.starts_with("agent:") && ckey.ends_with(&suffix) {
167 return Ok(Some(entry.clone()));
168 }
169 }
170 for (ckey, entry) in &state.entries {
172 if ckey.starts_with("session:") && ckey.ends_with(&suffix) {
173 return Ok(Some(entry.clone()));
174 }
175 }
176 let global_key = format!("global:{key}");
178 Ok(state.entries.get(&global_key).cloned())
179 }
180
181 fn archive_session(&self, session_id: &str) -> Result<(), MemoryError> {
182 let mut state = self.state.lock().map_err(|_| MemoryError::LockPoisoned)?;
183 let prefix = format!("session:{session_id}:");
184 let session_keys: Vec<String> =
185 state.entries.keys().filter(|k| k.starts_with(&prefix)).cloned().collect();
186 for k in session_keys {
187 if let Some(entry) = state.entries.remove(&k) {
188 state.archive.insert(k, entry);
189 }
190 }
191 Ok(())
192 }
193}
194
195impl InMemoryStore {
196 pub fn archived_entries(&self, session_id: &str) -> Vec<MemoryEntry> {
198 let state = self.state.lock().unwrap();
199 let prefix = format!("session:{session_id}:");
200 state
201 .archive
202 .iter()
203 .filter(|(k, _)| k.starts_with(&prefix))
204 .map(|(_, v)| v.clone())
205 .collect()
206 }
207}
208
209const AURA_BASE: &str = "http://localhost:7437";
224#[allow(dead_code)]
225
226pub fn aura_available() -> bool {
228 std::net::TcpStream::connect_timeout(
230 &"127.0.0.1:7437".parse().unwrap(),
231 std::time::Duration::from_millis(200),
232 ).is_ok()
233}
234
235pub fn aura_cli_available() -> bool {
237 std::process::Command::new("aura").arg("version").output().is_ok()
238}
239
240#[derive(Debug, Deserialize)]
242pub struct AuraMemoryItem {
243 pub key: String,
244 pub value: String,
245 #[serde(default)]
246 pub agent_id: String,
247 #[serde(default)]
248 pub timestamp: String,
249}
250
251pub fn aura_memory_add(key: &str, value: &str) -> bool {
253 std::process::Command::new("aura")
254 .args(["memory", "add", key, value])
255 .output()
256 .map(|o| o.status.success())
257 .unwrap_or(false)
258}
259
260pub fn aura_memory_get(key: &str) -> Option<String> {
262 let output = std::process::Command::new("aura")
263 .args(["memory", "get", key])
264 .output()
265 .ok()?;
266 if output.status.success() {
267 let s = String::from_utf8(output.stdout).ok()?;
268 let trimmed = s.trim().to_string();
269 if trimmed.is_empty() { None } else { Some(trimmed) }
270 } else {
271 None
272 }
273}
274
275pub fn aura_memory_ls() -> Vec<AuraMemoryItem> {
277 let output = std::process::Command::new("aura")
278 .args(["memory", "ls", "--json"])
279 .output();
280 let output = match output {
281 Ok(o) if o.status.success() => o,
282 _ => return Vec::new(),
283 };
284 serde_json::from_slice(&output.stdout).unwrap_or_default()
285}
286
287pub fn aura_memory_rm(key: &str) -> bool {
289 std::process::Command::new("aura")
290 .args(["memory", "rm", key])
291 .output()
292 .map(|o| o.status.success())
293 .unwrap_or(false)
294}
295
296pub struct AuraMemoryStore {
299 fallback: InMemoryStore,
300}
301
302impl AuraMemoryStore {
303 pub fn new() -> Self {
304 Self { fallback: InMemoryStore::new() }
305 }
306
307 pub fn store_kv(&self, key: &str, value: &str) -> bool {
309 if aura_cli_available() {
310 aura_memory_add(key, value)
311 } else {
312 self.fallback
313 .store(MemoryScope::Global, key, value.as_bytes(), 0)
314 .is_ok()
315 }
316 }
317
318 pub fn get_kv(&self, key: &str) -> Option<String> {
320 if aura_cli_available() {
321 aura_memory_get(key)
322 } else {
323 self.fallback
324 .query(key)
325 .ok()
326 .flatten()
327 .map(|e| String::from_utf8_lossy(&e.value).into_owned())
328 }
329 }
330
331 pub fn list(&self) -> Vec<(String, String)> {
333 if aura_cli_available() {
334 aura_memory_ls()
335 .into_iter()
336 .map(|item| (item.key, item.value))
337 .collect()
338 } else {
339 let state = self.fallback.state.lock().unwrap();
340 state.entries.values()
341 .map(|e| (e.key.clone(), String::from_utf8_lossy(&e.value).into_owned()))
342 .collect()
343 }
344 }
345
346 pub fn remove(&self, key: &str) -> bool {
348 if aura_cli_available() {
349 aura_memory_rm(key)
350 } else {
351 self.fallback
353 .store(MemoryScope::Global, key, b"", 0)
354 .is_ok()
355 }
356 }
357}
358
359impl Default for AuraMemoryStore {
360 fn default() -> Self {
361 Self::new()
362 }
363}
364
365impl SharedMemory for AuraMemoryStore {
366 fn store(&self, scope: MemoryScope, key: &str, value: &[u8], source_agent: u32) -> Result<(), MemoryError> {
367 let value_str = String::from_utf8_lossy(value);
368 let scoped_key = format!("{}:{}", scope.prefix(), key);
370 if aura_cli_available() {
371 aura_memory_add(&scoped_key, &value_str);
372 }
373 self.fallback.store(scope, key, value, source_agent)
375 }
376
377 fn query(&self, key: &str) -> Result<Option<MemoryEntry>, MemoryError> {
378 if aura_cli_available() {
380 let scoped_key = format!("global:{key}");
381 if let Some(val) = aura_memory_get(&scoped_key) {
382 return Ok(Some(MemoryEntry {
383 key: key.to_owned(),
384 value: val.into_bytes(),
385 scope: MemoryScope::Global,
386 timestamp: Utc::now().to_rfc3339(),
387 source_agent: 0,
388 }));
389 }
390 }
391 self.fallback.query(key)
393 }
394
395 fn archive_session(&self, session_id: &str) -> Result<(), MemoryError> {
396 self.fallback.archive_session(session_id)
397 }
398}
399
400pub struct McpMemoryProvider {
408 store: InMemoryStore,
409}
410
411impl McpMemoryProvider {
412 pub fn new(store: InMemoryStore) -> Self {
413 Self { store }
414 }
415
416 pub fn handle_tool_call(&self, tool_name: &str, params: Value) -> Value {
417 match tool_name {
418 "memory_store" => self.mcp_store(params),
419 "memory_query" => self.mcp_query(params),
420 _ => json!({ "error": format!("unknown tool: {tool_name}") }),
421 }
422 }
423
424 fn mcp_store(&self, params: Value) -> Value {
425 let scope = match parse_scope(¶ms) {
426 Ok(s) => s,
427 Err(e) => return json!({ "error": e }),
428 };
429 let key = match params.get("key").and_then(Value::as_str) {
430 Some(k) => k.to_owned(),
431 None => return json!({ "error": "missing field: key" }),
432 };
433 let value_b64 = match params.get("value").and_then(Value::as_str) {
434 Some(v) => v.to_owned(),
435 None => return json!({ "error": "missing field: value" }),
436 };
437 let value = match base64_decode(&value_b64) {
438 Ok(v) => v,
439 Err(e) => return json!({ "error": e }),
440 };
441 let source_agent = params
442 .get("source_agent")
443 .and_then(Value::as_u64)
444 .unwrap_or(0) as u32;
445
446 match self.store.store(scope, &key, &value, source_agent) {
447 Ok(()) => json!({ "ok": true }),
448 Err(e) => json!({ "error": e.to_string() }),
449 }
450 }
451
452 fn mcp_query(&self, params: Value) -> Value {
453 let key = match params.get("key").and_then(Value::as_str) {
454 Some(k) => k.to_owned(),
455 None => return json!({ "error": "missing field: key" }),
456 };
457 match self.store.query(&key) {
458 Ok(Some(entry)) => json!({
459 "found": true,
460 "key": entry.key,
461 "value": base64_encode(&entry.value),
462 "scope": scope_to_str(&entry.scope),
463 "timestamp": entry.timestamp,
464 "source_agent": entry.source_agent,
465 }),
466 Ok(None) => json!({ "found": false }),
467 Err(e) => json!({ "error": e.to_string() }),
468 }
469 }
470}
471
472pub struct A2aMemoryCard {
480 store: InMemoryStore,
481}
482
483impl A2aMemoryCard {
484 pub fn new(store: InMemoryStore) -> Self {
485 Self { store }
486 }
487
488 pub fn handle_request(&self, action: &str, params: Value) -> Value {
489 match action {
490 "store" => self.a2a_store(params),
491 "query" => self.a2a_query(params),
492 _ => json!({ "error": format!("unknown action: {action}") }),
493 }
494 }
495
496 fn a2a_store(&self, params: Value) -> Value {
497 let scope = match parse_scope(¶ms) {
498 Ok(s) => s,
499 Err(e) => return json!({ "error": e }),
500 };
501 let key = match params.get("key").and_then(Value::as_str) {
502 Some(k) => k.to_owned(),
503 None => return json!({ "error": "missing field: key" }),
504 };
505 let value_b64 = match params.get("value").and_then(Value::as_str) {
506 Some(v) => v.to_owned(),
507 None => return json!({ "error": "missing field: value" }),
508 };
509 let value = match base64_decode(&value_b64) {
510 Ok(v) => v,
511 Err(e) => return json!({ "error": e }),
512 };
513 let source_agent = params
514 .get("source_agent")
515 .and_then(Value::as_u64)
516 .unwrap_or(0) as u32;
517
518 match self.store.store(scope, &key, &value, source_agent) {
519 Ok(()) => json!({ "status": "ok" }),
520 Err(e) => json!({ "error": e.to_string() }),
521 }
522 }
523
524 fn a2a_query(&self, params: Value) -> Value {
525 let key = match params.get("key").and_then(Value::as_str) {
526 Some(k) => k.to_owned(),
527 None => return json!({ "error": "missing field: key" }),
528 };
529 match self.store.query(&key) {
530 Ok(Some(entry)) => json!({
531 "status": "ok",
532 "key": entry.key,
533 "value": base64_encode(&entry.value),
534 "scope": scope_to_str(&entry.scope),
535 "timestamp": entry.timestamp,
536 "source_agent": entry.source_agent,
537 }),
538 Ok(None) => json!({ "status": "not_found" }),
539 Err(e) => json!({ "error": e.to_string() }),
540 }
541 }
542}
543
544fn parse_scope(params: &Value) -> Result<MemoryScope, String> {
547 let scope_str = params
548 .get("scope")
549 .and_then(Value::as_str)
550 .unwrap_or("global");
551 if scope_str == "global" {
552 return Ok(MemoryScope::Global);
553 }
554 if let Some(id) = scope_str.strip_prefix("session:") {
555 return Ok(MemoryScope::Session(id.to_owned()));
556 }
557 if let Some(pid_str) = scope_str.strip_prefix("agent:") {
558 let pid: u32 = pid_str
559 .parse()
560 .map_err(|_| format!("invalid agent pid: {pid_str}"))?;
561 return Ok(MemoryScope::Agent(pid));
562 }
563 Err(format!("unknown scope: {scope_str}"))
564}
565
566fn scope_to_str(scope: &MemoryScope) -> String {
567 match scope {
568 MemoryScope::Global => "global".to_owned(),
569 MemoryScope::Session(id) => format!("session:{id}"),
570 MemoryScope::Agent(pid) => format!("agent:{pid}"),
571 }
572}
573
574fn base64_encode(data: &[u8]) -> String {
575 use std::fmt::Write;
576 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
578 let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
579 for chunk in data.chunks(3) {
580 let b0 = chunk[0] as usize;
581 let b1 = if chunk.len() > 1 { chunk[1] as usize } else { 0 };
582 let b2 = if chunk.len() > 2 { chunk[2] as usize } else { 0 };
583 let _ = write!(out, "{}", ALPHABET[b0 >> 2] as char);
584 let _ = write!(out, "{}", ALPHABET[((b0 & 3) << 4) | (b1 >> 4)] as char);
585 if chunk.len() > 1 {
586 let _ = write!(out, "{}", ALPHABET[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
587 } else {
588 out.push('=');
589 }
590 if chunk.len() > 2 {
591 let _ = write!(out, "{}", ALPHABET[b2 & 0x3f] as char);
592 } else {
593 out.push('=');
594 }
595 }
596 out
597}
598
599fn base64_decode(s: &str) -> Result<Vec<u8>, String> {
600 fn val(c: u8) -> Result<u8, String> {
601 match c {
602 b'A'..=b'Z' => Ok(c - b'A'),
603 b'a'..=b'z' => Ok(c - b'a' + 26),
604 b'0'..=b'9' => Ok(c - b'0' + 52),
605 b'+' => Ok(62),
606 b'/' => Ok(63),
607 b'=' => Ok(0),
608 _ => Err(format!("invalid base64 char: {c}")),
609 }
610 }
611 let bytes = s.as_bytes();
612 if bytes.len() % 4 != 0 {
613 return Err("base64 length must be a multiple of 4".to_owned());
614 }
615 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
616 for chunk in bytes.chunks(4) {
617 let v0 = val(chunk[0])?;
618 let v1 = val(chunk[1])?;
619 let v2 = val(chunk[2])?;
620 let v3 = val(chunk[3])?;
621 out.push((v0 << 2) | (v1 >> 4));
622 if chunk[2] != b'=' {
623 out.push((v1 << 4) | (v2 >> 2));
624 }
625 if chunk[3] != b'=' {
626 out.push((v2 << 6) | v3);
627 }
628 }
629 Ok(out)
630}
631
632#[cfg(test)]
635mod tests {
636 use super::*;
637
638 fn store() -> InMemoryStore {
639 InMemoryStore::new()
640 }
641
642 #[allow(dead_code)]
643 fn bytes(s: &str) -> Vec<u8> {
644 s.as_bytes().to_vec()
645 }
646
647 #[test]
650 fn global_scope_store_and_query_round_trip() {
651 let s = store();
652 s.store(MemoryScope::Global, "answer", b"42", 1).unwrap();
653 let entry = s.query("answer").unwrap().expect("entry should exist");
654 assert_eq!(entry.value, b"42");
655 assert_eq!(entry.scope, MemoryScope::Global);
656 assert_eq!(entry.source_agent, 1);
657 }
658
659 #[test]
660 fn session_scope_store_and_query_round_trip() {
661 let s = store();
662 s.store(MemoryScope::Session("sess-1".into()), "ctx", b"hello", 2).unwrap();
663 let entry = s.query("ctx").unwrap().expect("entry should exist");
664 assert_eq!(entry.value, b"hello");
665 assert!(matches!(entry.scope, MemoryScope::Session(ref id) if id == "sess-1"));
666 }
667
668 #[test]
669 fn agent_scope_store_and_query_round_trip() {
670 let s = store();
671 s.store(MemoryScope::Agent(99), "private", b"secret", 99).unwrap();
672 let entry = s.query("private").unwrap().expect("entry should exist");
673 assert_eq!(entry.value, b"secret");
674 assert_eq!(entry.scope, MemoryScope::Agent(99));
675 }
676
677 #[test]
678 fn query_returns_none_for_missing_key() {
679 let s = store();
680 assert!(s.query("nonexistent").unwrap().is_none());
681 }
682
683 #[test]
684 fn global_scope_persists_after_session_archive() {
685 let s = store();
686 s.store(MemoryScope::Global, "persistent", b"yes", 1).unwrap();
687 s.store(MemoryScope::Session("sess-a".into()), "temp", b"no", 1).unwrap();
688
689 s.archive_session("sess-a").unwrap();
690
691 let entry = s.query("persistent").unwrap().expect("global entry should survive archival");
693 assert_eq!(entry.value, b"yes");
694 }
695
696 #[test]
697 fn session_scope_is_archived_on_session_end() {
698 let s = store();
699 s.store(MemoryScope::Session("sess-b".into()), "work", b"data", 5).unwrap();
700
701 assert!(s.query("work").unwrap().is_some());
703
704 s.archive_session("sess-b").unwrap();
705
706 assert!(s.query("work").unwrap().is_none());
708
709 let archived = s.archived_entries("sess-b");
711 assert_eq!(archived.len(), 1);
712 assert_eq!(archived[0].key, "work");
713 }
714
715 #[test]
716 fn archive_session_only_removes_matching_session() {
717 let s = store();
718 s.store(MemoryScope::Session("sess-x".into()), "x_key", b"x", 1).unwrap();
719 s.store(MemoryScope::Session("sess-y".into()), "y_key", b"y", 2).unwrap();
720
721 s.archive_session("sess-x").unwrap();
722
723 assert!(s.query("x_key").unwrap().is_none());
724 assert!(s.query("y_key").unwrap().is_some());
725 }
726
727 #[test]
728 fn agent_scope_is_private_to_agent() {
729 let s = store();
730 s.store(MemoryScope::Agent(10), "secret", b"agent10", 10).unwrap();
731 s.store(MemoryScope::Agent(20), "secret", b"agent20", 20).unwrap();
732
733 let entry = s.query("secret").unwrap().expect("should find an agent entry");
735 assert!(matches!(entry.scope, MemoryScope::Agent(_)));
736 assert!(entry.value == b"agent10" || entry.value == b"agent20");
738 }
739
740 #[test]
741 fn query_priority_agent_over_session_over_global() {
742 let s = store();
743 s.store(MemoryScope::Global, "k", b"global", 0).unwrap();
744 s.store(MemoryScope::Session("s1".into()), "k", b"session", 1).unwrap();
745 s.store(MemoryScope::Agent(7), "k", b"agent", 7).unwrap();
746
747 let entry = s.query("k").unwrap().unwrap();
748 assert_eq!(entry.value, b"agent");
749 }
750
751 #[test]
752 fn query_falls_back_to_session_when_no_agent_entry() {
753 let s = store();
754 s.store(MemoryScope::Global, "k", b"global", 0).unwrap();
755 s.store(MemoryScope::Session("s1".into()), "k", b"session", 1).unwrap();
756
757 let entry = s.query("k").unwrap().unwrap();
758 assert_eq!(entry.value, b"session");
759 }
760
761 #[test]
762 fn query_falls_back_to_global_when_no_session_or_agent_entry() {
763 let s = store();
764 s.store(MemoryScope::Global, "k", b"global", 0).unwrap();
765
766 let entry = s.query("k").unwrap().unwrap();
767 assert_eq!(entry.value, b"global");
768 }
769
770 #[test]
771 fn store_overwrites_existing_entry() {
772 let s = store();
773 s.store(MemoryScope::Global, "counter", b"1", 1).unwrap();
774 s.store(MemoryScope::Global, "counter", b"2", 1).unwrap();
775 let entry = s.query("counter").unwrap().unwrap();
776 assert_eq!(entry.value, b"2");
777 }
778
779 #[test]
782 fn mcp_store_and_query_round_trip() {
783 let mcp = McpMemoryProvider::new(store());
784 let encoded = base64_encode(b"mcp_value");
785 let store_result = mcp.handle_tool_call(
786 "memory_store",
787 json!({ "scope": "global", "key": "mcp_key", "value": encoded, "source_agent": 1 }),
788 );
789 assert_eq!(store_result["ok"], true);
790
791 let query_result =
792 mcp.handle_tool_call("memory_query", json!({ "key": "mcp_key" }));
793 assert_eq!(query_result["found"], true);
794 assert_eq!(query_result["key"], "mcp_key");
795 }
796
797 #[test]
798 fn mcp_query_missing_key_returns_not_found() {
799 let mcp = McpMemoryProvider::new(store());
800 let result = mcp.handle_tool_call("memory_query", json!({ "key": "ghost" }));
801 assert_eq!(result["found"], false);
802 }
803
804 #[test]
805 fn mcp_unknown_tool_returns_error() {
806 let mcp = McpMemoryProvider::new(store());
807 let result = mcp.handle_tool_call("unknown_tool", json!({}));
808 assert!(result["error"].as_str().unwrap().contains("unknown tool"));
809 }
810
811 #[test]
812 fn mcp_store_missing_key_field_returns_error() {
813 let mcp = McpMemoryProvider::new(store());
814 let result = mcp.handle_tool_call(
815 "memory_store",
816 json!({ "scope": "global", "value": base64_encode(b"x") }),
817 );
818 assert!(result["error"].as_str().is_some());
819 }
820
821 #[test]
824 fn a2a_store_and_query_round_trip() {
825 let a2a = A2aMemoryCard::new(store());
826 let encoded = base64_encode(b"a2a_value");
827 let store_result = a2a.handle_request(
828 "store",
829 json!({ "scope": "session:s1", "key": "a2a_key", "value": encoded, "source_agent": 2 }),
830 );
831 assert_eq!(store_result["status"], "ok");
832
833 let query_result = a2a.handle_request("query", json!({ "key": "a2a_key" }));
834 assert_eq!(query_result["status"], "ok");
835 assert_eq!(query_result["key"], "a2a_key");
836 }
837
838 #[test]
839 fn a2a_query_missing_key_returns_not_found() {
840 let a2a = A2aMemoryCard::new(store());
841 let result = a2a.handle_request("query", json!({ "key": "ghost" }));
842 assert_eq!(result["status"], "not_found");
843 }
844
845 #[test]
846 fn a2a_unknown_action_returns_error() {
847 let a2a = A2aMemoryCard::new(store());
848 let result = a2a.handle_request("delete", json!({}));
849 assert!(result["error"].as_str().unwrap().contains("unknown action"));
850 }
851
852 #[test]
853 fn a2a_agent_scope_store_and_query() {
854 let a2a = A2aMemoryCard::new(store());
855 let encoded = base64_encode(b"private");
856 a2a.handle_request(
857 "store",
858 json!({ "scope": "agent:42", "key": "priv", "value": encoded, "source_agent": 42 }),
859 );
860 let result = a2a.handle_request("query", json!({ "key": "priv" }));
861 assert_eq!(result["status"], "ok");
862 assert_eq!(result["scope"], "agent:42");
863 }
864
865 #[test]
868 fn base64_round_trip() {
869 let original = b"Hello, World! \x00\xff\xfe";
870 let encoded = base64_encode(original);
871 let decoded = base64_decode(&encoded).unwrap();
872 assert_eq!(decoded, original);
873 }
874
875 #[test]
876 fn base64_empty_input() {
877 assert_eq!(base64_encode(b""), "");
878 assert_eq!(base64_decode("").unwrap(), b"");
879 }
880
881 #[test]
884 fn aura_available_check_does_not_panic() {
885 let _ = aura_available();
886 }
887
888 #[test]
889 fn aura_cli_available_check_does_not_panic() {
890 let _ = aura_cli_available();
891 }
892
893 #[test]
894 fn aura_memory_store_falls_back_to_in_memory_when_aura_not_running() {
895 let store = AuraMemoryStore::new();
896 store.store(MemoryScope::Global, "test-key", b"test-value", 0).unwrap();
898 let entry = store.query("test-key").unwrap();
899 if !aura_cli_available() {
902 assert!(entry.is_some());
903 assert_eq!(entry.unwrap().value, b"test-value");
904 }
905 }
906
907 #[test]
908 fn aura_memory_store_kv_does_not_panic() {
909 let store = AuraMemoryStore::new();
910 let _ = store.store_kv("hawk-test-key", "hawk-test-value");
911 }
912
913 #[test]
914 fn aura_memory_get_kv_does_not_panic() {
915 let store = AuraMemoryStore::new();
916 let _ = store.get_kv("hawk-test-key");
917 }
918
919 #[test]
920 fn aura_memory_list_does_not_panic() {
921 let store = AuraMemoryStore::new();
922 let _ = store.list();
923 }
924
925 #[test]
926 fn aura_memory_store_and_retrieve_via_fallback() {
927 let store = AuraMemoryStore::new();
929 store.store(MemoryScope::Global, "fallback-key", b"fallback-val", 1).unwrap();
930 let entry = store.fallback.query("fallback-key").unwrap();
931 assert!(entry.is_some());
932 assert_eq!(entry.unwrap().value, b"fallback-val");
933 }
934
935 #[test]
936 fn aura_memory_ls_returns_vec() {
937 let items = aura_memory_ls();
939 let _ = items; }
941}