use std::path::Path;
use trusty_common::palace_resolve::git_remote_origin;
pub(crate) fn maybe_register_palace_alias(
project_path: &Path,
git_remote: Option<&str>,
registry_dir: Option<&Path>,
) {
if trusty_common::palace_override_from_env().is_some() {
return;
}
if let Some(root) = trusty_common::palace_resolve::find_project_root(project_path)
&& matches!(
trusty_common::palace_resolve::read_project_pin(&root),
Ok(Some(_)) | Err(_)
)
{
return;
}
let probed = match git_remote {
Some(_) => None,
None => git_remote_origin(project_path),
};
let Some(remote) = git_remote.map(str::to_string).or(probed) else {
return;
};
let Some(owner_repo) = trusty_common::owner_repo_from_git_remote(&remote) else {
return;
};
let Some(bare) = trusty_common::repo_slug_from_git_remote(&remote) else {
return;
};
if owner_repo == bare {
return;
}
let resolved = match registry_dir {
Some(dir) => Ok(dir.to_path_buf()),
None => trusty_common::palace_alias::default_palace_registry_dir(),
};
let registry_dir = match resolved {
Ok(dir) => dir,
Err(e) => {
tracing::debug!(
error = %e,
"skipping palace-alias registration: cannot resolve registry dir"
);
return;
}
};
let owner_repo_exists = registry_dir.join(&owner_repo).join("palace.json").exists();
let bare_exists = registry_dir.join(&bare).join("palace.json").exists();
if owner_repo_exists || !bare_exists {
return;
}
match trusty_common::palace_alias::PalaceAliasStore::register_alias(
®istry_dir,
&owner_repo,
&bare,
) {
Ok(()) => tracing::info!(
alias = %owner_repo,
target = %bare,
"registered palace-level alias to heal claude-mpm memory parity (#1939)"
),
Err(e) => tracing::warn!(
alias = %owner_repo,
target = %bare,
error = %e,
"failed to register palace-level alias (#1939); session will pin owner-repo unaliased"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use tempfile::tempdir;
use trusty_common::palace_alias::PalaceAliasStore;
const REMOTE: &str = "git@github.com:bobmatnyc/trusty-tools.git";
const OWNER_REPO: &str = "bobmatnyc-trusty-tools";
const BARE: &str = "trusty-tools";
struct EnvGuard {
key: &'static str,
prior: Option<String>,
}
impl EnvGuard {
fn set(key: &'static str, value: &Path) -> Self {
let prior = std::env::var(key).ok();
unsafe { std::env::set_var(key, value) };
Self { key, prior }
}
fn clear(key: &'static str) -> Self {
let prior = std::env::var(key).ok();
unsafe { std::env::remove_var(key) };
Self { key, prior }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match &self.prior {
Some(v) => unsafe { std::env::set_var(self.key, v) },
None => unsafe { std::env::remove_var(self.key) },
}
}
}
fn make_palace(registry_dir: &Path, id: &str) {
let dir = registry_dir.join(id);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("palace.json"), b"{}").unwrap();
}
fn registry_dir(data_override: &Path) -> PathBuf {
data_override.join("trusty-memory")
}
#[test]
#[serial_test::serial]
fn creates_alias_for_split_brain() {
let data = tempdir().unwrap();
let _override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", data.path());
let reg = registry_dir(data.path());
make_palace(®, BARE);
maybe_register_palace_alias(Path::new("/unused"), Some(REMOTE), None);
assert_eq!(
PalaceAliasStore::resolve_alias(®, OWNER_REPO)
.unwrap()
.as_deref(),
Some(BARE),
"split-brain must register {OWNER_REPO} -> {BARE}"
);
}
#[test]
#[serial_test::serial]
fn noop_when_owner_repo_exists() {
let data = tempdir().unwrap();
let _override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", data.path());
let reg = registry_dir(data.path());
make_palace(®, BARE);
make_palace(®, OWNER_REPO);
maybe_register_palace_alias(Path::new("/unused"), Some(REMOTE), None);
assert_eq!(
PalaceAliasStore::resolve_alias(®, OWNER_REPO).unwrap(),
None,
"no alias should be registered when the owner-repo palace exists"
);
}
#[test]
#[serial_test::serial]
fn noop_when_no_bare_palace() {
let data = tempdir().unwrap();
let _override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", data.path());
let reg = registry_dir(data.path());
std::fs::create_dir_all(®).unwrap();
maybe_register_palace_alias(Path::new("/unused"), Some(REMOTE), None);
assert_eq!(
PalaceAliasStore::resolve_alias(®, OWNER_REPO).unwrap(),
None,
"no alias should be registered when no bare palace exists"
);
}
#[test]
#[serial_test::serial]
fn noop_when_project_is_pinned() {
let data = tempdir().unwrap();
let project = tempdir().unwrap();
let _override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", data.path());
let pin_dir = project.path().join(".trusty-tools");
std::fs::create_dir_all(&pin_dir).unwrap();
std::fs::write(
pin_dir.join("trusty-memory.yaml"),
"schema_version: 1\npalace: trusty-tools\n",
)
.unwrap();
let reg = registry_dir(data.path());
make_palace(®, BARE);
maybe_register_palace_alias(project.path(), Some(REMOTE), None);
assert_eq!(
PalaceAliasStore::resolve_alias(®, OWNER_REPO).unwrap(),
None,
"a committed pin must suppress alias registration even on a split-brain"
);
}
#[test]
#[serial_test::serial]
fn noop_when_override_set() {
let data = tempdir().unwrap();
let _override = EnvGuard::set("TRUSTY_MEMORY_PALACE", Path::new("operator-choice"));
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", data.path());
let reg = registry_dir(data.path());
make_palace(®, BARE);
maybe_register_palace_alias(Path::new("/unused"), Some(REMOTE), None);
assert_eq!(
PalaceAliasStore::resolve_alias(®, OWNER_REPO).unwrap(),
None,
"an operator override must suppress alias registration"
);
}
#[test]
#[serial_test::serial]
fn a_named_registry_dir_is_the_only_one_written() {
let ambient = tempdir().unwrap();
let named = tempdir().unwrap();
let _override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", ambient.path());
let ambient_reg = registry_dir(ambient.path());
make_palace(&ambient_reg, BARE);
make_palace(named.path(), BARE);
maybe_register_palace_alias(Path::new("/unused"), Some(REMOTE), Some(named.path()));
assert_eq!(
PalaceAliasStore::resolve_alias(named.path(), OWNER_REPO)
.unwrap()
.as_deref(),
Some(BARE),
"the named registry must receive the alias"
);
assert_eq!(
PalaceAliasStore::resolve_alias(&ambient_reg, OWNER_REPO).unwrap(),
None,
"the ambient registry must be untouched when a registry dir is named"
);
}
#[test]
#[serial_test::serial]
fn noop_when_the_default_registry_dir_cannot_resolve() {
let _override = EnvGuard::clear("TRUSTY_MEMORY_PALACE");
let _data = EnvGuard::set("TRUSTY_DATA_DIR_OVERRIDE", Path::new("relative-data"));
assert!(trusty_common::palace_alias::default_palace_registry_dir().is_err());
maybe_register_palace_alias(Path::new("/unused"), Some(REMOTE), None);
assert!(
!Path::new("relative-data").exists(),
"a refused registry dir must not be created relative to the cwd"
);
}
}