Compare commits

...

3 Commits

Author SHA1 Message Date
David Wiesen
f7bcbbd5b9 refactor(plugins): simplify stale-version prune guard 2026-05-18 13:30:43 -07:00
David Wiesen
b255ccea15 fix(plugins): fail when stale cache stays active 2026-05-18 13:25:21 -07:00
David Wiesen
f321b4ef0b fix(plugins): keep version upgrades additive 2026-05-18 13:12:24 -07:00
2 changed files with 95 additions and 0 deletions

View File

@@ -286,6 +286,15 @@ fn replace_plugin_root_atomically(
let staged_version_root = staged_root.join(plugin_version);
copy_dir_recursive(source, &staged_version_root)?;
let target_version_root = target_root.join(plugin_version);
if target_root.exists() && !target_version_root.exists() {
fs::rename(&staged_version_root, &target_version_root).map_err(|err| {
PluginStoreError::io("failed to activate updated plugin cache version", err)
})?;
remove_old_plugin_versions(target_root, plugin_version)?;
return Ok(());
}
if target_root.exists() {
let backup_dir = tempfile::Builder::new()
.prefix("plugin-backup-")
@@ -322,6 +331,44 @@ fn replace_plugin_root_atomically(
Ok(())
}
fn remove_old_plugin_versions(
target_root: &Path,
plugin_version: &str,
) -> Result<(), PluginStoreError> {
let Ok(entries) = fs::read_dir(target_root) else {
return Ok(());
};
for entry in entries.filter_map(Result::ok) {
let Ok(file_type) = entry.file_type() else {
continue;
};
if !file_type.is_dir() {
continue;
}
let Ok(version) = entry.file_name().into_string() else {
continue;
};
if version == plugin_version || validate_plugin_version_segment(&version).is_err() {
continue;
}
if fs::remove_dir_all(entry.path()).is_err()
&& old_plugin_version_would_stay_active(&version, plugin_version)
{
return Err(PluginStoreError::Invalid(format!(
"failed to activate updated plugin cache version `{plugin_version}` while `{version}` remains active"
)));
}
}
Ok(())
}
fn old_plugin_version_would_stay_active(old_version: &str, new_version: &str) -> bool {
old_version == DEFAULT_PLUGIN_VERSION || old_version > new_version
}
fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), PluginStoreError> {
fs::create_dir_all(target)
.map_err(|err| PluginStoreError::io("failed to create plugin target directory", err))?;

View File

@@ -268,6 +268,54 @@ fn active_plugin_version_returns_last_sorted_version_when_default_is_missing() {
);
}
#[test]
fn install_with_new_version_keeps_existing_plugin_root_and_prunes_old_versions() {
let tmp = tempdir().unwrap();
let store = PluginStore::new(tmp.path().to_path_buf());
let plugin_id = PluginId::new("sample-plugin".to_string(), "debug".to_string()).unwrap();
write_plugin_with_version(tmp.path(), "v1", "sample-plugin", Some("1.0.0"));
store
.install(
AbsolutePathBuf::try_from(tmp.path().join("v1")).unwrap(),
plugin_id.clone(),
)
.unwrap();
write_plugin_with_version(tmp.path(), "v2", "sample-plugin", Some("2.0.0"));
store
.install(
AbsolutePathBuf::try_from(tmp.path().join("v2")).unwrap(),
plugin_id.clone(),
)
.unwrap();
assert_eq!(
store.active_plugin_version(&plugin_id),
Some("2.0.0".to_string())
);
assert!(
tmp.path()
.join("plugins/cache/debug/sample-plugin/2.0.0")
.is_dir()
);
assert!(
!tmp.path()
.join("plugins/cache/debug/sample-plugin/1.0.0")
.exists()
);
}
#[test]
fn old_plugin_version_would_stay_active_for_local_or_later_versions() {
assert!(old_plugin_version_would_stay_active(
DEFAULT_PLUGIN_VERSION,
"1.0.0"
));
assert!(old_plugin_version_would_stay_active("9.0.0", "1.0.0"));
assert!(!old_plugin_version_would_stay_active("1.0.0", "2.0.0"));
}
#[test]
fn plugin_root_rejects_path_separators_in_key_segments() {
let err = PluginId::parse("../../etc@debug").unwrap_err();