//! `recall`: quoted terms only, ranked hits with source tags and labels, the result's label, and //! the recall log (M5a spec, section 5). Do not edit. #[path = "support/logs.rs"] mod logs; #[path = "support/tmp.rs"] mod tmp; use logs::{assistant, result, user, write_log}; use memoryd::files::Store; use memoryd::index::Index; use memoryd::recall::{recall, terms}; use proto::{DataClass, RecallArgs, Timestamp}; use tmp::TempDir; fn now() -> Timestamp { Timestamp::parse("2026-09-24T21:00:00.000Z").unwrap() } struct Setup { dir: TempDir, store: Store, index: Index, } fn setup(tag: &str) -> Setup { let dir = TempDir::new(tag); let starter = dir.write("etc/soul.md", "Soul.\n"); let store = Store::new(&dir.path().join("memory")); store.init(&starter).unwrap(); let index = Index::open(&dir.path().join("index/memory.sqlite3"), DataClass::Private).unwrap(); Setup { dir, store, index } } fn ask(s: &Setup, query: &str, k: u64) -> memoryd::recall::Recalled { let args = RecallArgs { query: query.to_string(), k, }; recall(&s.index, &s.store, &args, "mm-now", now()).unwrap() } #[test] fn terms_are_quoted_and_operators_never_pass() { assert_eq!( terms("launch window"), Some("\"launch\" OR \"window\"".to_string()) ); assert_eq!( terms(r#"a NEAR(b c) OR "x" title:y z* ^w -v"#), Some( r#""a" OR "NEAR" OR "b" OR "c" OR "OR" OR "x" OR "title" OR "y" OR "z" OR "w" OR "v""# .to_string() ) ); assert_eq!(terms("Київ 東京"), Some("\"Київ\" OR \"東京\"".to_string())); assert_eq!(terms("*** ::: \"\""), None); } #[test] fn hits_are_tagged_and_labelled() { let s = setup("recall-tags"); write_log( s.dir.path(), "mm-a", &[ user(2, "The launch window moved to Thursday."), assistant( 3, None, &[("http_fetch", r#"{"url":"https://example.org/launch"}"#)], ), result(4, "page", DataClass::Private, true), assistant(5, Some("The page says the launch is in March."), &[]), ], ); s.index .catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md")) .unwrap(); std::fs::write( s.store.path("notes.md"), "§ moved 2026-09-24T20:14Z from memory.md in session mm-b\nOwner cares about the launch.\n§ dropped 2026-09-24T20:15Z from user.md in session mm-b\nOld launch plan.\n§ noted 2026-09-22T18:20Z in session mm-c · private · untrusted\nexample.org says the launch moved.\n", ) .unwrap(); s.index .catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md")) .unwrap(); let got = ask(&s, "launch", 10); let headers: Vec<&str> = got.text.lines().filter(|l| l.starts_with('[')).collect(); let mut sorted: Vec = headers .iter() .map(|h| h.split_once("] ").unwrap().1.to_string()) .collect(); sorted.sort(); assert_eq!( sorted, [ "identity · moved 2026-09-24 from memory.md · private", "memory · dropped 2026-09-24 from user.md · private", "memory · session mm-a, 2026-09-20 14:02 UTC, the owner said · private", "memory · session mm-a, 2026-09-20 14:03 UTC, the assistant called · private", "memory-tainted · note, 2026-09-22 18:20 UTC, session mm-c · private", "memory-tainted · session mm-a, 2026-09-20 14:05 UTC, the assistant said · private", ] ); assert!(got.text.starts_with("[1] ")); assert!(got.text.contains("\nThe launch window moved to Thursday.")); assert!(!got.text.contains("page\n"), "tool results are not indexed"); assert_eq!((got.class, got.untrusted), (DataClass::Private, true)); } #[test] fn a_clean_result_is_trusted_and_k_limits_the_hits() { let s = setup("recall-clean"); write_log( s.dir.path(), "mm-a", &[ user(1, "alpha one"), user(2, "alpha two"), user(3, "alpha three"), ], ); s.index .catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md")) .unwrap(); let got = ask(&s, "alpha", 2); assert_eq!(got.text.lines().filter(|l| l.starts_with('[')).count(), 2); assert_eq!((got.class, got.untrusted), (DataClass::Private, false)); let none = ask(&s, "zebra", 5); assert_eq!(none.text, "(no matches for \"zebra\")"); assert_eq!(ask(&s, "NEAR(", 5).text, "(no matches for \"NEAR(\")"); } #[test] fn long_hits_are_cut() { let s = setup("recall-long"); // 64 words of 40 letters: longer than 600 characters even after `snippet` keeps 64 words. let long = format!("needle {}", format!("{} ", "w".repeat(40)).repeat(100)); write_log(s.dir.path(), "mm-a", &[user(1, &long)]); s.index .catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md")) .unwrap(); let got = ask(&s, "needle", 1); let body = got.text.split_once('\n').unwrap().1; assert!(body.chars().count() <= 600, "{}", body.chars().count()); } #[test] fn every_recall_is_logged_with_its_hits() { let s = setup("recall-log"); write_log(s.dir.path(), "mm-a", &[user(1, "found me")]); s.index .catch_up(&s.dir.path().join("sessions"), &s.store.path("notes.md")) .unwrap(); ask(&s, "found", 5); ask(&s, "missing", 5); let log = std::fs::read_to_string(s.store.path("recalls/2026-09.jsonl")).unwrap(); let lines: Vec = log .lines() .map(|l| serde_json::from_str(l).unwrap()) .collect(); assert_eq!(lines.len(), 2); assert_eq!(lines[0]["query"], "found"); assert_eq!(lines[0]["session"], "mm-now"); assert_eq!(lines[0]["hits"][0]["session"], "mm-a"); assert_eq!(lines[0]["hits"][0]["record"], 0); assert_eq!(lines[1]["hits"], serde_json::json!([])); } #[test] fn bad_arguments_are_refused() { let s = setup("recall-bad"); let args = RecallArgs { query: String::new(), k: 5, }; assert!(recall(&s.index, &s.store, &args, "mm-now", now()).is_err()); }