use anyhow::{Context, Result};
use std::path::Path;
use trusty_common::memory_core::store::{PalaceStore, PalaceStoreError};
const DEFAULT_PALACE_ID: &str = "localLLM";
const LEGACY_DEFAULT_NAME: &str = "localLLM";
const NEW_DEFAULT_NAME: &str = "User Memories";
pub fn migrate_default_palace_name(registry_root: &Path) -> Result<()> {
let palace_dir = registry_root.join(DEFAULT_PALACE_ID);
let mut palace = match PalaceStore::load_palace(&palace_dir) {
Ok(p) => p,
Err(PalaceStoreError::NotFound(_)) => return Ok(()),
Err(e) => {
return Err(e)
.with_context(|| format!("load palace metadata at {}", palace_dir.display()))
}
};
if palace.name != LEGACY_DEFAULT_NAME {
return Ok(());
}
palace.name = NEW_DEFAULT_NAME.to_string();
PalaceStore::save_palace(&palace)
.with_context(|| format!("rewrite palace metadata at {}", palace_dir.display()))?;
tracing::info!(
palace_id = DEFAULT_PALACE_ID,
old_name = LEGACY_DEFAULT_NAME,
new_name = NEW_DEFAULT_NAME,
"migrated default palace display name"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
use tempfile::tempdir;
use trusty_common::memory_core::palace::{Palace, PalaceId};
fn persist_palace(registry_root: &Path, id: &str, name: &str) -> Palace {
let data_dir = registry_root.join(id);
std::fs::create_dir_all(&data_dir).expect("create palace data dir");
let palace = Palace {
id: PalaceId::new(id),
name: name.to_string(),
description: None,
created_at: Utc::now(),
data_dir: data_dir.clone(),
};
PalaceStore::save_palace(&palace).expect("persist palace metadata");
palace
}
#[test]
fn migrates_when_legacy_name() {
let tmp = tempdir().unwrap();
let root = tmp.path();
persist_palace(root, DEFAULT_PALACE_ID, LEGACY_DEFAULT_NAME);
migrate_default_palace_name(root).expect("migration runs");
let loaded =
PalaceStore::load_palace(&root.join(DEFAULT_PALACE_ID)).expect("reload palace.json");
assert_eq!(loaded.id.as_str(), DEFAULT_PALACE_ID, "id must be stable");
assert_eq!(
loaded.name, NEW_DEFAULT_NAME,
"display name must be updated"
);
}
#[test]
fn idempotent_when_already_renamed() {
let tmp = tempdir().unwrap();
let root = tmp.path();
persist_palace(root, DEFAULT_PALACE_ID, LEGACY_DEFAULT_NAME);
migrate_default_palace_name(root).expect("first migration");
migrate_default_palace_name(root).expect("second migration is no-op");
let loaded =
PalaceStore::load_palace(&root.join(DEFAULT_PALACE_ID)).expect("reload palace.json");
assert_eq!(loaded.name, NEW_DEFAULT_NAME);
}
#[test]
fn leaves_custom_name_untouched() {
let tmp = tempdir().unwrap();
let root = tmp.path();
persist_palace(root, DEFAULT_PALACE_ID, "My Custom Memories");
migrate_default_palace_name(root).expect("migration runs");
let loaded =
PalaceStore::load_palace(&root.join(DEFAULT_PALACE_ID)).expect("reload palace.json");
assert_eq!(
loaded.name, "My Custom Memories",
"custom names must not be overwritten"
);
}
#[test]
fn pre_renamed_user_memories_is_untouched() {
let tmp = tempdir().unwrap();
let root = tmp.path();
persist_palace(root, DEFAULT_PALACE_ID, NEW_DEFAULT_NAME);
migrate_default_palace_name(root).expect("migration runs");
let loaded =
PalaceStore::load_palace(&root.join(DEFAULT_PALACE_ID)).expect("reload palace.json");
assert_eq!(loaded.name, NEW_DEFAULT_NAME);
}
#[test]
fn missing_palace_is_noop() {
let tmp = tempdir().unwrap();
migrate_default_palace_name(tmp.path()).expect("missing palace is a no-op");
}
#[test]
fn unrelated_palaces_are_not_touched() {
let tmp = tempdir().unwrap();
let root = tmp.path();
persist_palace(root, "other-palace", LEGACY_DEFAULT_NAME);
migrate_default_palace_name(root).expect("migration runs");
let loaded =
PalaceStore::load_palace(&root.join("other-palace")).expect("reload other palace");
assert_eq!(
loaded.name, LEGACY_DEFAULT_NAME,
"non-default palaces must not be touched"
);
}
#[cfg(unix)]
struct RestoreMode(std::path::PathBuf);
#[cfg(unix)]
impl Drop for RestoreMode {
fn drop(&mut self) {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o700));
}
}
#[cfg(unix)]
#[test]
fn unstattable_palace_json_is_an_error_not_a_noop() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempdir().unwrap();
let root = tmp.path();
persist_palace(root, DEFAULT_PALACE_ID, LEGACY_DEFAULT_NAME);
let palace_dir = root.join(DEFAULT_PALACE_ID);
std::fs::set_permissions(&palace_dir, std::fs::Permissions::from_mode(0o000)).unwrap();
let _restore = RestoreMode(palace_dir.clone());
let target = palace_dir.join("palace.json");
match std::fs::metadata(&target) {
Ok(_) => panic!(
"cannot exercise #5549: stat of {} still succeeds with its parent at mode 000. \
Run this suite as a non-root user on a filesystem that honours POSIX \
permission bits.",
target.display()
),
Err(e) => assert_eq!(
e.kind(),
std::io::ErrorKind::PermissionDenied,
"expected the locked palace dir to deny stat of its metadata, got {e}"
),
}
migrate_default_palace_name(root)
.expect_err("a palace.json that cannot be stat'd must not read as no palace at all");
}
}