252 lines
8.4 KiB
Go
252 lines
8.4 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"
|
|
)
|
|
|
|
func writeSkillCacheFixture(t *testing.T, dir, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func readSkillCacheFixture(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(filepath.Join(dir, "SKILL.md"))
|
|
if err != nil {
|
|
t.Fatalf("read cache fixture: %v", err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
// TestCrossPlatformCoverageRefreshSkillCacheTransaction pins the cache refresh
|
|
// transaction on every platform: copying is staged before the old cache moves,
|
|
// and a publish failure restores the prior usable cache.
|
|
func TestCrossPlatformCoverageRefreshSkillCacheTransaction(t *testing.T) {
|
|
newSource := func(t *testing.T) string {
|
|
t.Helper()
|
|
src := t.TempDir()
|
|
writeSkillCacheFixture(t, src, "new")
|
|
return src
|
|
}
|
|
cachePath := func(home string) string {
|
|
return filepath.Join(home, ".dws", "skills", "multi")
|
|
}
|
|
assertOldCache := func(t *testing.T, home string) {
|
|
t.Helper()
|
|
if got := readSkillCacheFixture(t, cachePath(home)); got != "old" {
|
|
t.Fatalf("cache content = %q, want old", got)
|
|
}
|
|
}
|
|
|
|
t.Run("replace existing cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); err != nil {
|
|
t.Fatalf("refreshSkillCache() error = %v", err)
|
|
}
|
|
if got := readSkillCacheFixture(t, cachePath(home)); got != "new" {
|
|
t.Fatalf("cache content = %q, want new", got)
|
|
}
|
|
entries, err := os.ReadDir(filepath.Dir(cachePath(home)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 1 || entries[0].Name() != "multi" {
|
|
t.Fatalf("cache parent entries = %v, want only multi", entries)
|
|
}
|
|
})
|
|
|
|
t.Run("publish new cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); err != nil {
|
|
t.Fatalf("refreshSkillCache() error = %v", err)
|
|
}
|
|
if got := readSkillCacheFixture(t, cachePath(home)); got != "new" {
|
|
t.Fatalf("cache content = %q, want new", got)
|
|
}
|
|
})
|
|
|
|
t.Run("parent creation failure", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
failure := errors.New("mkdir denied")
|
|
testseam.Swap(t, &upgradeMkdirAll, func(string, os.FileMode) error { return failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
})
|
|
|
|
t.Run("staging creation failure preserves old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("temp denied")
|
|
testseam.Swap(t, &upgradeMkdirTemp, func(string, string) (string, error) { return "", failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("copy failure preserves old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("copy denied")
|
|
testseam.Swap(t, &upgradeCopyDir, func(string, string) error { return failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("cache stat failure preserves old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("stat denied")
|
|
testseam.Swap(t, &upgradeStat, func(string) (os.FileInfo, error) { return nil, failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("new cache publish failure cleans staging", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
failure := errors.New("rename denied")
|
|
testseam.Swap(t, &upgradeRename, func(string, string) error { return failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
entries, err := os.ReadDir(filepath.Dir(cachePath(home)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 0 {
|
|
t.Fatalf("cache parent entries = %v, want empty", entries)
|
|
}
|
|
})
|
|
|
|
t.Run("rollback staging creation failure preserves old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("rollback temp denied")
|
|
original := upgradeMkdirTemp
|
|
calls := 0
|
|
testseam.Swap(t, &upgradeMkdirTemp, func(dir, pattern string) (string, error) {
|
|
calls++
|
|
if calls == 2 {
|
|
return "", failure
|
|
}
|
|
return original(dir, pattern)
|
|
})
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("rollback preparation failure preserves old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("remove denied")
|
|
testseam.Swap(t, &upgradeRemoveAll, func(string) error { return failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("old cache move failure preserves old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("old move denied")
|
|
testseam.Swap(t, &upgradeRename, func(string, string) error { return failure })
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("publish failure restores old cache", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
failure := errors.New("publish denied")
|
|
original := upgradeRename
|
|
calls := 0
|
|
testseam.Swap(t, &upgradeRename, func(src, dst string) error {
|
|
calls++
|
|
if calls == 2 {
|
|
return failure
|
|
}
|
|
return original(src, dst)
|
|
})
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); !errors.Is(err, failure) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want %v", err, failure)
|
|
}
|
|
assertOldCache(t, home)
|
|
})
|
|
|
|
t.Run("restore failure reports both errors and retains backup", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
publishFailure := errors.New("publish denied")
|
|
restoreFailure := errors.New("restore denied")
|
|
original := upgradeRename
|
|
calls := 0
|
|
testseam.Swap(t, &upgradeRename, func(src, dst string) error {
|
|
calls++
|
|
switch calls {
|
|
case 2:
|
|
return publishFailure
|
|
case 3:
|
|
return restoreFailure
|
|
default:
|
|
return original(src, dst)
|
|
}
|
|
})
|
|
err := refreshSkillCache(home, "multi", newSource(t))
|
|
if !errors.Is(err, publishFailure) || !strings.Contains(err.Error(), restoreFailure.Error()) {
|
|
t.Fatalf("refreshSkillCache() error = %v, want publish and restore errors", err)
|
|
}
|
|
matches, globErr := filepath.Glob(filepath.Join(filepath.Dir(cachePath(home)), ".multi.old-*", "SKILL.md"))
|
|
if globErr != nil || len(matches) != 1 {
|
|
t.Fatalf("rollback cache matches = %v, err = %v", matches, globErr)
|
|
}
|
|
if got := readSkillCacheFixture(t, filepath.Dir(matches[0])); got != "old" {
|
|
t.Fatalf("rollback cache content = %q, want old", got)
|
|
}
|
|
})
|
|
|
|
t.Run("post publish cleanup is best effort", func(t *testing.T) {
|
|
home := t.TempDir()
|
|
writeSkillCacheFixture(t, cachePath(home), "old")
|
|
original := upgradeRemoveAll
|
|
calls := 0
|
|
testseam.Swap(t, &upgradeRemoveAll, func(path string) error {
|
|
calls++
|
|
if calls == 2 {
|
|
return errors.New("cleanup denied")
|
|
}
|
|
return original(path)
|
|
})
|
|
if err := refreshSkillCache(home, "multi", newSource(t)); err != nil {
|
|
t.Fatalf("refreshSkillCache() error = %v", err)
|
|
}
|
|
if got := readSkillCacheFixture(t, cachePath(home)); got != "new" {
|
|
t.Fatalf("cache content = %q, want new", got)
|
|
}
|
|
})
|
|
}
|