fix(skills): guard pruneSkillBackups against non-DWS directories

Restrict backup pruning to directories whose names match the DWS stamp
format (YYYYmmdd-HHMMSS with optional -N suffix) across all 8 installer
surfaces (Go, npm, 4 shell, 2 PowerShell). Unknown directories in
~/.dws/skill-backups are now preserved. Also fixes Windows coverage test
portability and covers the remaining macOS changed-code gap (retire
warning loop in runUpgrade).
This commit is contained in:
玉澜
2026-08-17 11:42:43 +08:00
parent ae1565c0ff
commit 14c2569cfb
15 changed files with 264 additions and 18 deletions
+49 -3
View File
@@ -44,9 +44,10 @@
* guard, exercised by swapping process.platform
* instead of requiring a Windows host.
* 14. backup retention — ~/.dws/skill-backups keeps the newest 5 stamps.
* 15. multi publish failure — restores the complete previous Skill set.
* 16. multi backup failure — restores every earlier successful backup.
* 17. mono transaction failure — restores every managed multi Skill after
* 15. unknown preservation — prune never removes non-DWS directories in skill-backups.
* 16. multi publish failure — restores the complete previous Skill set.
* 17. multi backup failure — restores every earlier successful backup.
* 18. mono transaction failure — restores every managed multi Skill after
* later backup or mono publish failure.
*
* Requirements: unix host with tar/zip/unzip on PATH (the same tools
@@ -80,6 +81,8 @@ const {
UPSTREAM_AGENTS,
resolvedAgentTargets,
agentTargetDetected,
isSkillBackupStamp,
pruneSkillBackups,
backupAndRemoveSkillDir,
movePathRecoverablySync,
publishCacheAtomically,
@@ -1003,6 +1006,49 @@ scenario("skill backups stay bounded to the newest five stamps", () => {
}
});
scenario("prune preserves unknown directories in skill-backups", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "dws-installjs-prune-unknown-"));
const home = path.join(tmp, "home");
const backupRoot = path.join(home, ".dws", "skill-backups");
try {
for (const stamp of [
"20260101-000001", "20260101-000002", "20260101-000003",
"20260101-000004", "20260101-000005", "20260101-000006", "20260101-000007",
]) {
writeFile(path.join(backupRoot, stamp, "placeholder", "SKILL.md"), `${stamp}\n`);
}
const unknowns = [
"user-personal-backup",
"20260101-00000", // too few digits in the time part
"20260101-000000-abc", // non-numeric suffix
"notes",
"20260101", // missing time portion
];
for (const name of unknowns) {
writeFile(path.join(backupRoot, name, "marker.txt"), `unknown:${name}\n`);
}
pruneSkillBackups(home);
const remaining = fs.readdirSync(backupRoot).sort();
const stamped = remaining.filter((n) => isSkillBackupStamp(n));
assert.equal(stamped.length, 5, `stamped backups = ${stamped.length}, want 5`);
assert.ok(!stamped.includes("20260101-000001"), "oldest stamps pruned first");
assert.ok(!stamped.includes("20260101-000002"), "second-oldest pruned");
assert.ok(stamped.includes("20260101-000005"), "newest kept stamps retained");
for (const name of unknowns) {
assert.ok(remaining.includes(name), `unknown directory ${name} must survive pruning`);
assert.equal(
fs.readFileSync(path.join(backupRoot, name, "marker.txt"), "utf8"),
`unknown:${name}\n`,
`unknown directory ${name} contents must be intact`,
);
}
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});
scenario("multi set publish failure restores the complete previous set", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "dws-installjs-multi-set-"));
const home = path.join(tmp, "home");