refactor(cli): Vault::after_manifest_change wrapper (Plan B Phase 4)

Adds the canonical post-mutation funnel: save_manifest_raw + groups.cache
refresh in one method. Converts nine commands/*.rs mutation callsites from
the manual save_manifest + refresh_groups_cache pair to a single
vault.after_manifest_change(&manifest)?. save_manifest renamed to
save_manifest_raw (pub(crate)) so future commands cannot accidentally
bypass the cache refresh. Four of the nine sites (attach.rs add/detach,
import.rs LastPass, trash.rs cmd_trash_empty's per-item save) previously
skipped the cache refresh — the wrapper fixes them. refresh_groups_cache
moves from main.rs to helpers.rs so the read-side warmup callers in
get.rs/list.rs still reach it.
This commit is contained in:
adlee-was-taken
2026-05-09 11:29:52 -04:00
parent 2e41e0bae0
commit 7901c2758d
10 changed files with 60 additions and 35 deletions

View File

@@ -69,7 +69,20 @@ impl UnlockedVault {
Ok(decrypt_manifest(&bytes, &self.master_key)?)
}
pub fn save_manifest(&self, manifest: &Manifest) -> Result<()> {
/// Save the manifest and refresh the plaintext groups.cache. This is the
/// canonical "I just mutated the manifest" funnel — every command that
/// changes the manifest goes through this method, so cache freshness is
/// a compile-time invariant rather than a discipline rule.
pub fn after_manifest_change(&self, manifest: &Manifest) -> Result<()> {
self.save_manifest_raw(manifest)?;
crate::helpers::refresh_groups_cache(&self.root, manifest);
Ok(())
}
/// Encrypt the manifest and atomically write it. Most callers want
/// `after_manifest_change` instead — this method skips the groups.cache
/// refresh, leaving shell completion stale until the next mutation.
pub(crate) fn save_manifest_raw(&self, manifest: &Manifest) -> Result<()> {
let bytes = encrypt_manifest(manifest, &self.master_key)?;
atomic_write(&self.manifest_path(), &bytes)
}
@@ -252,4 +265,21 @@ mod tests {
assert_eq!(v["aead"], "xchacha20poly1305");
assert_eq!(v["salt_path"], ".relicario/salt");
}
#[test]
fn after_manifest_change_writes_manifest_and_groups_cache() {
let dir = tempfile::TempDir::new().unwrap();
let root = dir.path().to_path_buf();
std::fs::create_dir_all(root.join(".relicario")).unwrap();
std::fs::create_dir_all(root.join("items")).unwrap();
let vault = UnlockedVault {
root: root.clone(),
master_key: Zeroizing::new([0u8; 32]),
};
let manifest = Manifest::new();
vault.after_manifest_change(&manifest).unwrap();
assert!(root.join("manifest.enc").exists());
assert!(root.join(".relicario/groups.cache").exists());
}
}