use super::*;
use crate::tools::Tool;
use agent_core::memory::store::{MemoryProvenance, MemoryRetention};
use std::process::Command;
fn git(root: &Path, args: &[&str]) {
let result = Command::new("git")
.arg("-C")
.arg(root)
.args(args)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.output()
.unwrap();
assert!(
result.status.success(),
"git failed: {}",
String::from_utf8_lossy(&result.stderr)
);
}
fn note(body: &str) -> NewMemoryRecord {
NewMemoryRecord {
content: body.into(),
tags: vec![],
provenance: MemoryProvenance {
source: "synthetic-test".into(),
session: None,
},
sensitivity: MemorySensitivity::Normal,
retention: MemoryRetention::Standard,
}
}
fn binding(base: &Path, root: &Path, exe: &Path) -> MemoryBinding {
let identity = ProjectScope::discover_repository_with_override(root, None).unwrap();
let mut binding = MemoryBinding::new(
base.to_path_buf(),
Ok(identity.scope.clone()),
&MemoryBackendConfig {
kind: MemoryBackendKind::Axel,
executable: Some(exe.to_path_buf()),
brain: Some(base.join("shared.r8")),
user_scope: true,
},
);
Arc::get_mut(&mut binding.0).unwrap().repository = Some(identity);
binding
}
#[tokio::test]
#[ignore = "requires SYNAPS_AXEL_TEST_BIN; synthetic repositories and brain only"]
async fn shared_brain_worktrees_moves_aliases_user_notes_and_foreign_isolation() {
let exe =
PathBuf::from(std::env::var_os("SYNAPS_AXEL_TEST_BIN").expect("explicit test service"));
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().canonicalize().unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&base, std::fs::Permissions::from_mode(0o700)).unwrap();
}
let repo = base.join("repo");
let worktree = base.join("worktree");
let foreign = base.join("foreign");
std::fs::create_dir(&repo).unwrap();
std::fs::create_dir(&foreign).unwrap();
git(&repo, &["init", "--quiet"]);
git(
&repo,
&[
"-c",
"user.name=Synthetic",
"-c",
"user.email=synthetic@example.invalid",
"commit",
"--allow-empty",
"-m",
"initial",
"--quiet",
],
);
git(
&repo,
&[
"worktree",
"add",
"--quiet",
"-b",
"feature",
worktree.to_str().unwrap(),
],
);
git(&foreign, &["init", "--quiet"]);
let main = binding(&base, &repo, &exe);
let worker = binding(&base, &worktree, &exe);
let other = binding(&base, &foreign, &exe);
assert_eq!(main.scope().unwrap().key(), worker.scope().unwrap().key());
assert_ne!(main.scope().unwrap().key(), other.scope().unwrap().key());
let saved = main.store(note("shared worktree knowledge")).await.unwrap();
let id = saved.id.as_deref().unwrap();
assert_eq!(worker.fetch(&[id]).await.unwrap()[0], saved);
assert!(other.search(Default::default()).await.unwrap().is_empty());
assert!(other.fetch(&[id]).await.is_err());
assert!(other.forget(id).await.is_err());
assert_eq!(main.fetch(&[id]).await.unwrap()[0], saved);
assert!(
saved.meta.as_ref().unwrap()["_synaps_repository"]["source_worktree_project"].is_string()
);
let inventory = main.rpc("export", json!({"full":true})).await.unwrap();
let export_file = base.join("production-export.json");
std::fs::write(&export_file, serde_json::to_vec(&inventory).unwrap()).unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&export_file, std::fs::Permissions::from_mode(0o600)).unwrap();
}
let restore_dir = base.join("restore");
std::fs::create_dir(&restore_dir).unwrap();
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&restore_dir, std::fs::Permissions::from_mode(0o700)).unwrap();
}
let restore = MemoryBinding::new(
base.clone(),
Ok(main.scope().unwrap().clone()),
&MemoryBackendConfig {
kind: MemoryBackendKind::Axel,
executable: Some(exe.clone()),
brain: Some(restore_dir.join("restore.r8")),
user_scope: false,
},
);
let source = migration::MigrationSource::Export {
path: export_file,
project: main.scope().unwrap().key().into(),
};
let preview = migration::preview_from(&restore, &source).await.unwrap();
migration::apply_from(&restore, &source, &preview.manifest_digest)
.await
.unwrap();
let recovered = restore.fetch(&[id]).await.unwrap().remove(0);
assert_eq!(recovered.content, saved.content);
assert_eq!(recovered.id, saved.id);
assert_eq!(
recovered.meta.as_ref().unwrap()["_synaps_repository"],
saved.meta.as_ref().unwrap()["_synaps_repository"]
);
let legacy = main.with_scope(ProjectScope::for_root(&worktree).unwrap());
assert_ne!(legacy.scope().unwrap().key(), main.scope().unwrap().key());
let old = legacy
.store(note("preexisting worktree note"))
.await
.unwrap();
let old_id = old.id.as_deref().unwrap();
assert!(main.fetch(&[old_id]).await.is_err());
main.rpc("scope_alias", json!({"alias_project":legacy.scope().unwrap().key(),"alias_root":worktree,"canonical_root":repo})).await.unwrap();
assert_eq!(main.fetch(&[old_id]).await.unwrap()[0], old);
assert!(main
.search(ProjectMemoryQuery {
content_contains: Some("preexisting".into()),
..Default::default()
})
.await
.unwrap()
.iter()
.any(|d| d.id == old_id));
worker.forget(old_id).await.unwrap();
assert!(legacy.fetch(&[old_id]).await.is_err());
let user = main.for_user_notes().unwrap();
let global = user.store(note("explicit user preference")).await.unwrap();
let global_id = global.id.as_deref().unwrap();
assert_eq!(
other
.for_user_notes()
.unwrap()
.fetch(&[global_id])
.await
.unwrap()[0],
global
);
assert!(main.fetch(&[global_id]).await.is_err());
assert!(user.history_search("", 1).await.is_err());
assert!(user
.rpc("capture_query", json!({"capture_id":"a".repeat(64)}))
.await
.is_err());
let context = || {
let mut c = crate::tools::test_helpers::create_tool_context();
c.capabilities.memory_backend = Some(worker.clone());
c
};
let found = crate::tools::memory::MemorySearchTool
.execute(json!({"scope":"user","query":"explicit user"}), context())
.await
.unwrap();
assert!(found.contains(global_id));
assert!(crate::tools::memory::MemoryFetchTool
.execute(json!({"scope":"user","ids":[global_id]}), context())
.await
.unwrap()
.contains("explicit user preference"));
let moved = base.join("moved-repo");
std::fs::rename(&repo, &moved).unwrap();
git(&moved, &["worktree", "repair"]);
let reopened = binding(&base, &moved, &exe);
assert_eq!(reopened.scope().unwrap().key(), main.scope().unwrap().key());
assert_eq!(reopened.fetch(&[id]).await.unwrap()[0], saved);
std::fs::create_dir(&repo).unwrap();
git(&repo, &["init", "--quiet"]);
let replacement = binding(&base, &repo, &exe);
assert_ne!(
replacement.scope().unwrap().key(),
reopened.scope().unwrap().key()
);
assert!(replacement
.search(Default::default())
.await
.unwrap()
.is_empty());
assert!(replacement.fetch(&[id]).await.is_err());
assert!(replacement.forget(id).await.is_err());
assert_eq!(reopened.fetch(&[id]).await.unwrap()[0], saved);
git(&moved, &["worktree", "remove", worktree.to_str().unwrap()]);
std::fs::create_dir(&worktree).unwrap();
git(&worktree, &["init", "--quiet"]);
let replacement_worker = binding(&base, &worktree, &exe);
assert_ne!(
replacement_worker.scope().unwrap().key(),
reopened.scope().unwrap().key()
);
assert!(replacement_worker.fetch(&[id]).await.is_err());
assert!(replacement_worker.forget(id).await.is_err());
assert!(!base.join("context-archives").exists());
}
#[test]
fn default_shared_paths_and_user_scope_are_host_bound() {
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().canonicalize().unwrap();
let scope = ProjectScope::for_root(&base).unwrap();
let disabled = MemoryBinding::new(
base.clone(),
Ok(scope.clone()),
&MemoryBackendConfig {
kind: MemoryBackendKind::Axel,
..Default::default()
},
);
assert_eq!(
disabled.brain_path().unwrap(),
base.join("memory/axel/brain.r8")
);
assert!(disabled.for_user_notes().is_err());
assert!(!base.join("memory").exists());
let legacy = MemoryBinding::new(
base.clone(),
Ok(scope),
&MemoryBackendConfig {
user_scope: true,
..Default::default()
},
);
assert!(legacy.for_user_notes().is_err());
}