mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 11:43:57 +08:00
fix(pi): treat stale skill cleanup as post-commit state
This commit is contained in:
@@ -84,7 +84,7 @@ impl PiSkillDeploymentService {
|
|||||||
existing,
|
existing,
|
||||||
Some(true),
|
Some(true),
|
||||||
)?;
|
)?;
|
||||||
cleanup_stale_deployments(db, skill, &destination_key)?;
|
cleanup_stale_deployments_after_commit(db, skill, &destination_key);
|
||||||
} else {
|
} else {
|
||||||
remove_all_recorded_deployments(db, skill, Some(false))?;
|
remove_all_recorded_deployments(db, skill, Some(false))?;
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ impl PiSkillDeploymentService {
|
|||||||
Some(true),
|
Some(true),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
cleanup_stale_deployments(db, skill, &destination_key)?;
|
cleanup_stale_deployments_after_commit(db, skill, &destination_key);
|
||||||
} else {
|
} else {
|
||||||
remove_all_recorded_deployments(db, skill, Some(false))?;
|
remove_all_recorded_deployments(db, skill, Some(false))?;
|
||||||
}
|
}
|
||||||
@@ -411,7 +411,8 @@ fn reconcile_skill_unlocked(db: &Arc<Database>, skill: &InstalledSkill) -> Resul
|
|||||||
existing,
|
existing,
|
||||||
None,
|
None,
|
||||||
)?;
|
)?;
|
||||||
cleanup_stale_deployments(db, skill, &destination_key)
|
cleanup_stale_deployments_after_commit(db, skill, &destination_key);
|
||||||
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
remove_all_recorded_deployments(db, skill, None)
|
remove_all_recorded_deployments(db, skill, None)
|
||||||
}
|
}
|
||||||
@@ -430,6 +431,25 @@ fn cleanup_stale_deployments(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `deploy` atomically commits the new native destination, its ownership
|
||||||
|
/// ledger, and (when requested) desired state before old-root cleanup begins.
|
||||||
|
/// A drifted old root must remain visible as stale ownership evidence, but it
|
||||||
|
/// must not turn that committed publication into an error: several callers
|
||||||
|
/// compensate a returned error by deleting the Skill's SSOT/database row.
|
||||||
|
fn cleanup_stale_deployments_after_commit(
|
||||||
|
db: &Arc<Database>,
|
||||||
|
skill: &InstalledSkill,
|
||||||
|
current_destination_key: &str,
|
||||||
|
) {
|
||||||
|
if let Err(error) = cleanup_stale_deployments(db, skill, current_destination_key) {
|
||||||
|
log::warn!(
|
||||||
|
"Pi Skill '{}' was published at its current agent root, but stale deployment cleanup \
|
||||||
|
remains pending: {error}",
|
||||||
|
skill.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_all_recorded_deployments(
|
fn remove_all_recorded_deployments(
|
||||||
db: &Arc<Database>,
|
db: &Arc<Database>,
|
||||||
skill: &InstalledSkill,
|
skill: &InstalledSkill,
|
||||||
@@ -1238,4 +1258,96 @@ mod tests {
|
|||||||
destination_key(&new_destination)
|
destination_key(&new_destination)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
fn drifted_old_root_is_post_commit_status_not_a_failed_new_publication() {
|
||||||
|
struct EnvGuard {
|
||||||
|
key: &'static str,
|
||||||
|
previous: Option<std::ffi::OsString>,
|
||||||
|
}
|
||||||
|
impl EnvGuard {
|
||||||
|
fn set(key: &'static str, value: &Path) -> Self {
|
||||||
|
let previous = std::env::var_os(key);
|
||||||
|
std::env::set_var(key, value);
|
||||||
|
Self { key, previous }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for EnvGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
match self.previous.take() {
|
||||||
|
Some(value) => std::env::set_var(self.key, value),
|
||||||
|
None => std::env::remove_var(self.key),
|
||||||
|
}
|
||||||
|
if self.key == "CC_SWITCH_TEST_HOME" {
|
||||||
|
let _ = crate::settings::reload_settings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp = tempfile::tempdir().expect("tempdir");
|
||||||
|
let _home = EnvGuard::set("CC_SWITCH_TEST_HOME", temp.path());
|
||||||
|
crate::settings::reload_settings().expect("reload settings");
|
||||||
|
let old_root = temp.path().join("old-pi");
|
||||||
|
let new_root = temp.path().join("new-pi");
|
||||||
|
let _pi_root = EnvGuard::set("PI_CODING_AGENT_DIR", &old_root);
|
||||||
|
let source = SkillService::get_ssot_dir()
|
||||||
|
.expect("SSOT")
|
||||||
|
.join("drifted-relocation");
|
||||||
|
fs::create_dir_all(&source).expect("source");
|
||||||
|
fs::write(
|
||||||
|
source.join("SKILL.md"),
|
||||||
|
"---\nname: drifted-relocation\ndescription: relocation test\n---\n",
|
||||||
|
)
|
||||||
|
.expect("manifest");
|
||||||
|
let mut skill = InstalledSkill {
|
||||||
|
id: "local:drifted-relocation".to_string(),
|
||||||
|
name: "Drifted relocation".to_string(),
|
||||||
|
description: Some("relocation test".to_string()),
|
||||||
|
directory: "drifted-relocation".to_string(),
|
||||||
|
repo_owner: None,
|
||||||
|
repo_name: None,
|
||||||
|
repo_branch: None,
|
||||||
|
readme_url: None,
|
||||||
|
apps: SkillApps::only(&AppType::Pi),
|
||||||
|
installed_at: 1,
|
||||||
|
content_hash: None,
|
||||||
|
updated_at: 1,
|
||||||
|
};
|
||||||
|
let db = Arc::new(Database::memory().expect("database"));
|
||||||
|
db.save_skill(&skill).expect("save skill");
|
||||||
|
PiSkillDeploymentService::reconcile_skill(&db, &skill).expect("old deployment");
|
||||||
|
let old_destination = old_root.join("skills").join(&skill.directory);
|
||||||
|
remove_path(&old_destination).expect("replace old deployment");
|
||||||
|
fs::create_dir_all(&old_destination).expect("foreign old destination");
|
||||||
|
fs::write(old_destination.join("external.txt"), "external").expect("external drift");
|
||||||
|
|
||||||
|
std::env::set_var("PI_CODING_AGENT_DIR", &new_root);
|
||||||
|
PiSkillDeploymentService::toggle(&db, &mut skill, true)
|
||||||
|
.expect("the new publication is already committed");
|
||||||
|
|
||||||
|
let new_destination = new_root.join("skills").join(&skill.directory);
|
||||||
|
assert!(fs::symlink_metadata(&new_destination).is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_to_string(old_destination.join("external.txt")).expect("external survives"),
|
||||||
|
"external"
|
||||||
|
);
|
||||||
|
let stored = db
|
||||||
|
.get_installed_skill(&skill.id)
|
||||||
|
.expect("read desired state")
|
||||||
|
.expect("skill remains");
|
||||||
|
assert!(stored.apps.pi);
|
||||||
|
assert_eq!(
|
||||||
|
db.get_pi_skill_deployments(&skill.id)
|
||||||
|
.expect("ledger")
|
||||||
|
.len(),
|
||||||
|
2,
|
||||||
|
"old drift evidence and the committed new deployment must both remain"
|
||||||
|
);
|
||||||
|
let status =
|
||||||
|
PiSkillDeploymentService::inspect_all(&db).expect("inspect")[&skill.id].clone();
|
||||||
|
assert_eq!(status.ownership, PiSkillOwnership::Stale);
|
||||||
|
assert!(status.effectively_discovered);
|
||||||
|
assert!(status.issue.is_some_and(|issue| issue.contains("previous")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user