mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(skills): add restore and delete for skill backups
Introduce list/restore/delete commands for skill backups created during uninstall. Restore copies files back to SSOT, saves the DB record, and syncs to the current app with rollback on failure. Delete removes the backup directory after a confirmation dialog. ConfirmDialog gains a configurable zIndex prop to support nested dialog stacking.
This commit is contained in:
@@ -193,6 +193,150 @@ fn uninstall_skill_creates_backup_before_removing_ssot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_skill_backup_restores_files_to_ssot_and_current_app() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let ssot_skill_dir = home.join(".cc-switch").join("skills").join("restore-skill");
|
||||
write_skill(&ssot_skill_dir, "Restore Skill");
|
||||
fs::write(ssot_skill_dir.join("prompt.md"), "restore me").expect("write prompt.md");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
state
|
||||
.db
|
||||
.save_skill(&InstalledSkill {
|
||||
id: "local:restore-skill".to_string(),
|
||||
name: "Restore Skill".to_string(),
|
||||
description: Some("Bring the files back".to_string()),
|
||||
directory: "restore-skill".to_string(),
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
repo_branch: None,
|
||||
readme_url: None,
|
||||
apps: SkillApps {
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
},
|
||||
installed_at: 456,
|
||||
})
|
||||
.expect("save skill");
|
||||
|
||||
let uninstall =
|
||||
SkillService::uninstall(&state.db, "local:restore-skill").expect("uninstall skill");
|
||||
let backup_id = std::path::Path::new(
|
||||
&uninstall
|
||||
.backup_path
|
||||
.expect("backup path should be returned on uninstall"),
|
||||
)
|
||||
.file_name()
|
||||
.expect("backup dir name")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let restored = SkillService::restore_from_backup(&state.db, &backup_id, &AppType::Claude)
|
||||
.expect("restore from backup");
|
||||
|
||||
assert_eq!(restored.directory, "restore-skill");
|
||||
assert!(restored.apps.claude, "restored skill should enable Claude");
|
||||
assert!(
|
||||
!restored.apps.codex && !restored.apps.gemini && !restored.apps.opencode,
|
||||
"restore should only enable the selected app"
|
||||
);
|
||||
assert!(
|
||||
home.join(".cc-switch")
|
||||
.join("skills")
|
||||
.join("restore-skill")
|
||||
.join("prompt.md")
|
||||
.exists(),
|
||||
"restored skill should exist in SSOT"
|
||||
);
|
||||
assert!(
|
||||
home.join(".claude")
|
||||
.join("skills")
|
||||
.join("restore-skill")
|
||||
.join("prompt.md")
|
||||
.exists(),
|
||||
"restored skill should sync to the selected app"
|
||||
);
|
||||
assert!(
|
||||
state
|
||||
.db
|
||||
.get_installed_skill("local:restore-skill")
|
||||
.expect("query restored skill")
|
||||
.is_some(),
|
||||
"restored skill should be written back to the database"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_skill_backup_removes_backup_directory() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let ssot_skill_dir = home
|
||||
.join(".cc-switch")
|
||||
.join("skills")
|
||||
.join("delete-backup-skill");
|
||||
write_skill(&ssot_skill_dir, "Delete Backup Skill");
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
state
|
||||
.db
|
||||
.save_skill(&InstalledSkill {
|
||||
id: "local:delete-backup-skill".to_string(),
|
||||
name: "Delete Backup Skill".to_string(),
|
||||
description: Some("Remove my backup".to_string()),
|
||||
directory: "delete-backup-skill".to_string(),
|
||||
repo_owner: None,
|
||||
repo_name: None,
|
||||
repo_branch: None,
|
||||
readme_url: None,
|
||||
apps: SkillApps {
|
||||
claude: true,
|
||||
codex: false,
|
||||
gemini: false,
|
||||
opencode: false,
|
||||
},
|
||||
installed_at: 789,
|
||||
})
|
||||
.expect("save skill");
|
||||
|
||||
let uninstall =
|
||||
SkillService::uninstall(&state.db, "local:delete-backup-skill").expect("uninstall skill");
|
||||
let backup_path = uninstall
|
||||
.backup_path
|
||||
.expect("backup path should be returned on uninstall");
|
||||
let backup_id = std::path::Path::new(&backup_path)
|
||||
.file_name()
|
||||
.expect("backup dir name")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
assert!(
|
||||
std::path::Path::new(&backup_path).exists(),
|
||||
"backup directory should exist before deletion"
|
||||
);
|
||||
|
||||
SkillService::delete_backup(&backup_id).expect("delete backup");
|
||||
|
||||
assert!(
|
||||
!std::path::Path::new(&backup_path).exists(),
|
||||
"backup directory should be removed"
|
||||
);
|
||||
assert!(
|
||||
SkillService::list_backups()
|
||||
.expect("list backups")
|
||||
.into_iter()
|
||||
.all(|entry| entry.backup_id != backup_id),
|
||||
"deleted backup should no longer appear in backup list"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn migration_snapshot_overrides_multi_source_directory_inference() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
|
||||
Reference in New Issue
Block a user