A stamp-shaped directory name is not ownership proof: pruneSkillBackups counted and RemoveAll'd any 20260819-120000-shaped entry under ~/.dws/skill-backups, so a user or tool that created such a directory lost its contents once DWS held five backups, and the Go backup path (MkdirAll) adopted a same-named foreign root outright. The PowerShell installers already implemented the correct contract; every other surface now matches it. Go stamps a freshly created root with the exact marker bytes the install scripts write (.dws-skill-backup = "dws skill backup v1") before any payload moves in, claims the root with mkdir so an existing unproven root bumps to a collision suffix instead of being adopted, and prunes only roots whose marker verifies — unmarked or wrongly worded stamp-shaped directories are foreign data, preserved and never counted against the keep limit. The shell installers (install.sh, install-skills.sh, install-event.sh, install-devapp.sh) and the npm installer apply the same rule in their backup collision loops, with roots recorded as created by the running process exempt from marker re-verification so a mid-run marker permission failure still reuses this run's own root and keeps the sibling payload intact. Regression tests cover every surface: pruning an unmarked/wrongly marked stamp-shaped directory alongside marked ones, refusing to adopt a foreign root (payload moves to a suffixed root, foreign data and its nonexistent marker untouched), same-stamp reuse of a proven root, and marker-write failure cleaning the empty fresh root.
114 lines
4.3 KiB
Go
114 lines
4.3 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0
|
|
|
|
package upgrade
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
|
|
)
|
|
|
|
// TestCrossPlatformCoverageSkillBackupOwnership pins the backup ownership
|
|
// contract: a stamp-shaped directory name alone proves nothing, so a backup
|
|
// root this transaction writes into is either created fresh (mkdir claim,
|
|
// marker stamped before any payload moves in) or an existing root whose
|
|
// marker already verifies. A same-stamp foreign directory is never adopted —
|
|
// writing the marker or the payload into it would turn its contents into
|
|
// prunable DWS backups.
|
|
func TestCrossPlatformCoverageSkillBackupOwnership(t *testing.T) {
|
|
|
|
t.Run("unproven stamp root is never adopted", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
testseam.Swap(t, &upgradeBackupStamp, func() string { return "20260101-120000" })
|
|
foreign := filepath.Join(home, skillBackupSubdir, "20260101-120000")
|
|
if err := os.MkdirAll(foreign, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(foreign, "user-data.txt"), []byte("must survive"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
victim := filepath.Join(home, "skills", "dws")
|
|
seedUpgradeSkill(t, victim, "payload", false)
|
|
backup, err := backupAndRemoveSkillDir(home, victim)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if want := filepath.Join(home, skillBackupSubdir, "20260101-120000-1", "skills-dws"); backup != want {
|
|
t.Fatalf("backup = %s, want suffixed %s", backup, want)
|
|
}
|
|
assertUpgradeSkillContent(t, backup, "payload")
|
|
// The foreign root keeps its data and never receives our marker.
|
|
if data, readErr := os.ReadFile(filepath.Join(foreign, "user-data.txt")); readErr != nil || string(data) != "must survive" {
|
|
t.Fatalf("foreign data = %q, %v", data, readErr)
|
|
}
|
|
if _, statErr := os.Lstat(filepath.Join(foreign, skillBackupMarkerName)); !os.IsNotExist(statErr) {
|
|
t.Fatalf("foreign root must not receive the ownership marker, stat err=%v", statErr)
|
|
}
|
|
if !skillBackupMarkerValid(filepath.Dir(backup)) {
|
|
t.Fatal("the freshly claimed root must carry the ownership marker")
|
|
}
|
|
})
|
|
|
|
t.Run("same-stamp payloads reuse the proven root", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
testseam.Swap(t, &upgradeBackupStamp, func() string { return "20260101-120000" })
|
|
first := filepath.Join(home, "skills", "dws")
|
|
second := filepath.Join(home, "agents", "claude", "dws")
|
|
seedUpgradeSkill(t, first, "first", false)
|
|
seedUpgradeSkill(t, second, "second", false)
|
|
backupA, err := backupAndRemoveSkillDir(home, first)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
backupB, err := backupAndRemoveSkillDir(home, second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if filepath.Dir(backupA) != filepath.Dir(backupB) {
|
|
t.Fatalf("same-stamp backups split roots: %s vs %s", backupA, backupB)
|
|
}
|
|
root := filepath.Dir(backupA)
|
|
if !skillBackupMarkerValid(root) {
|
|
t.Fatal("reused root must keep a verifying marker")
|
|
}
|
|
assertUpgradeSkillContent(t, backupA, "first")
|
|
assertUpgradeSkillContent(t, backupB, "second")
|
|
})
|
|
|
|
t.Run("marker write failure refuses the backup and cleans the empty root", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
victim := filepath.Join(home, "skills", "dws")
|
|
seedUpgradeSkill(t, victim, "payload", false)
|
|
testseam.Swap(t, &skillBackupWriteMarker, func(string) error { return errors.New("marker denied") })
|
|
_, err := backupAndRemoveSkillDir(home, victim)
|
|
if err == nil || !strings.Contains(err.Error(), "备份所有权标记失败") {
|
|
t.Fatalf("marker write failure = %v", err)
|
|
}
|
|
assertUpgradeSkillContent(t, victim, "payload")
|
|
entries, readErr := os.ReadDir(filepath.Join(home, skillBackupSubdir))
|
|
if readErr != nil {
|
|
t.Fatal(readErr)
|
|
}
|
|
if len(entries) != 0 {
|
|
t.Fatalf("empty unowned stamp root must be cleaned, left %v", entries)
|
|
}
|
|
})
|
|
|
|
t.Run("backup root creation failure surfaces", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
victim := filepath.Join(home, "skills", "dws")
|
|
seedUpgradeSkill(t, victim, "payload", false)
|
|
testseam.Swap(t, &skillPathMkdir, func(string, os.FileMode) error { return os.ErrPermission })
|
|
_, err := backupAndRemoveSkillDir(home, victim)
|
|
if !errors.Is(err, os.ErrPermission) {
|
|
t.Fatalf("root creation failure = %v", err)
|
|
}
|
|
assertUpgradeSkillContent(t, victim, "payload")
|
|
})
|
|
}
|