Files
dingtalk-workspace-cli/internal/upgrade/paths_test.go
玉澜andCursor 402429ac2a feat(skill): default installs and upgrades to multi-skill layout
Flip the agent-skill default from mono (single dws/ dir) to multi
(per-product dingtalk-* + dws-shared) across all distribution faces,
and fix the upgrade path so it no longer re-installs mono alongside
multi (mono+multi co-existence bug).

- upgrade: LocateSkillsRoot prefers the zip multi/ tree; multi refresh
  removes mono leftovers and stale skills, refreshes the multi cache
- install.sh/ps1/install-skills.sh/npm install.js: multi real-install
  (was print-only), default flipped, mono stays opt-in via DWS_SKILL_MODE
- skill setup: non-interactive default multi; full installs now clean
  stale dingtalk-*/dws-shared with confirm-preview disclosure, filtered
  (-s/-x) installs stay additive
- mutual exclusion is symmetric and includes dws-shared (previously
  leaked through the dingtalk- prefix) on all faces
- install.js: guard empty/corrupt multi trees (fall back to mono),
  validate SKILL.md on the mono branch, guard cache refreshes
- docs: roadmap (8/30 back-schedule), migration plan, distribution
  mechanism, rollout capability, capability completion, architecture
  optimization, wukong comparison (archived; line retired)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 14:13:45 +08:00

256 lines
6.7 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package upgrade
import (
"os"
"path/filepath"
"testing"
)
func TestIsBlacklisted(t *testing.T) {
tests := []struct {
dir string
want bool
}{
{".real/skills", true},
{".real", true},
{".agents/skills", false},
{".claude/skills", false},
{".cursor/skills", false},
{".realtime/skills", false},
}
for _, tt := range tests {
got := isBlacklisted(tt.dir)
if got != tt.want {
t.Errorf("isBlacklisted(%q) = %v, want %v", tt.dir, got, tt.want)
}
}
}
func TestLocateSkillMD(t *testing.T) {
// Flat layout
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("# skill"), 0644)
result := LocateSkillMD(dir)
if result != dir {
t.Errorf("flat layout: LocateSkillMD() = %q, want %q", result, dir)
}
// Nested layout
dir2 := t.TempDir()
os.MkdirAll(filepath.Join(dir2, "dws"), 0755)
os.WriteFile(filepath.Join(dir2, "dws", "SKILL.md"), []byte("# skill"), 0644)
result2 := LocateSkillMD(dir2)
want2 := filepath.Join(dir2, "dws")
if result2 != want2 {
t.Errorf("nested layout: LocateSkillMD() = %q, want %q", result2, want2)
}
// No SKILL.md
dir3 := t.TempDir()
result3 := LocateSkillMD(dir3)
if result3 != "" {
t.Errorf("empty dir: LocateSkillMD() = %q, want empty", result3)
}
}
func TestUpgradeSkillLocations(t *testing.T) {
// Fake HOME: the mono path refreshes ~/.dws/skills/mono best-effort, and
// the test must never touch the real user cache.
homeDir := withFakeHome(t)
skillSrc := t.TempDir()
os.WriteFile(filepath.Join(skillSrc, "SKILL.md"), []byte("# test skill"), 0644)
os.MkdirAll(filepath.Join(skillSrc, "references"), 0755)
os.WriteFile(filepath.Join(skillSrc, "references", "guide.md"), []byte("# guide"), 0644)
result, err := UpgradeSkillLocations(skillSrc)
if err != nil {
t.Fatalf("UpgradeSkillLocations() error = %v", err)
}
succeeded := result.Succeeded()
if len(succeeded) == 0 {
t.Fatal("UpgradeSkillLocations() returned 0 succeeded locations")
}
primaryDest := filepath.Join(homeDir, ".agents", "skills", "dws")
found := false
for _, d := range succeeded {
if d.Dir == primaryDest {
found = true
break
}
}
if !found {
dirs := make([]string, len(succeeded))
for i, d := range succeeded {
dirs[i] = d.Dir
}
t.Errorf("primary location %s not in succeeded list: %v", primaryDest, dirs)
}
skillMDPath := filepath.Join(primaryDest, "SKILL.md")
if _, err := os.Stat(skillMDPath); os.IsNotExist(err) {
t.Errorf("SKILL.md not found at %s", skillMDPath)
}
guidePath := filepath.Join(primaryDest, "references", "guide.md")
if _, err := os.Stat(guidePath); os.IsNotExist(err) {
t.Errorf("references/guide.md not found at %s", guidePath)
}
// Verify that failed list is empty for a normal install
if len(result.Failed()) != 0 {
t.Errorf("expected 0 failures, got %d", len(result.Failed()))
}
// Verify total results contain at least one entry for each known (non-blacklisted) dir
if len(result.Results) == 0 {
t.Error("expected non-empty Results")
}
os.RemoveAll(primaryDest)
}
func TestBlacklistPreventsRealDir(t *testing.T) {
for _, dir := range knownSkillDirs {
if isBlacklisted(dir) {
t.Errorf("knownSkillDirs contains blacklisted entry: %q", dir)
}
}
}
func TestSkillUpgradeResult_SucceededAndFailed(t *testing.T) {
result := &SkillUpgradeResult{
Results: []SkillDirResult{
{Dir: "/a", Status: SkillDirOK},
{Dir: "/b", Status: SkillDirFailed, Err: os.ErrPermission},
{Dir: "/c", Status: SkillDirSkipped},
{Dir: "/d", Status: SkillDirBlacklisted},
{Dir: "/e", Status: SkillDirOK},
},
}
succeeded := result.Succeeded()
if len(succeeded) != 2 {
t.Errorf("Succeeded() len = %d, want 2", len(succeeded))
}
if succeeded[0].Dir != "/a" || succeeded[1].Dir != "/e" {
t.Errorf("Succeeded() dirs = %v, %v", succeeded[0].Dir, succeeded[1].Dir)
}
failed := result.Failed()
if len(failed) != 1 {
t.Errorf("Failed() len = %d, want 1", len(failed))
}
if failed[0].Dir != "/b" {
t.Errorf("Failed()[0].Dir = %q, want /b", failed[0].Dir)
}
if failed[0].Err != os.ErrPermission {
t.Errorf("Failed()[0].Err = %v, want ErrPermission", failed[0].Err)
}
}
func TestSkillUpgradeResult_EmptyResults(t *testing.T) {
result := &SkillUpgradeResult{}
if len(result.Succeeded()) != 0 {
t.Error("empty Succeeded should be nil/empty")
}
if len(result.Failed()) != 0 {
t.Error("empty Failed should be nil/empty")
}
}
func TestSkillDirStatusConstants(t *testing.T) {
if SkillDirOK != 0 {
t.Errorf("SkillDirOK = %d, want 0", SkillDirOK)
}
if SkillDirSkipped != 1 {
t.Errorf("SkillDirSkipped = %d, want 1", SkillDirSkipped)
}
if SkillDirBlacklisted != 2 {
t.Errorf("SkillDirBlacklisted = %d, want 2", SkillDirBlacklisted)
}
if SkillDirFailed != 3 {
t.Errorf("SkillDirFailed = %d, want 3", SkillDirFailed)
}
}
func TestEnsureUpgradeDirectories(t *testing.T) {
// This test verifies the structure is correct without actually modifying $HOME
homeDir, err := os.UserHomeDir()
if err != nil {
t.Skip("cannot get home dir")
}
err = EnsureUpgradeDirectories()
if err != nil {
t.Fatalf("EnsureUpgradeDirectories() error = %v", err)
}
expectedDirs := []string{
filepath.Join(homeDir, ".dws"),
filepath.Join(homeDir, ".dws", "data"),
filepath.Join(homeDir, ".dws", "data", "backups"),
filepath.Join(homeDir, ".dws", "cache"),
filepath.Join(homeDir, ".dws", "cache", "downloads"),
}
for _, d := range expectedDirs {
info, err := os.Stat(d)
if err != nil {
t.Errorf("directory %s should exist: %v", d, err)
continue
}
if !info.IsDir() {
t.Errorf("%s should be a directory", d)
}
}
}
func TestDownloadCacheDir(t *testing.T) {
dir := DownloadCacheDir()
if dir == "" {
t.Error("DownloadCacheDir() returned empty")
}
if !filepath.IsAbs(dir) {
t.Errorf("DownloadCacheDir() = %q, want absolute path", dir)
}
}
func TestBinaryName(t *testing.T) {
name := BinaryName()
if name != "dws" && name != "dws.exe" {
t.Errorf("BinaryName() = %q, want dws or dws.exe", name)
}
}
func TestLocateSkillMD_NestedBeatsFlat(t *testing.T) {
dir := t.TempDir()
// Create both flat and nested layouts; nested should take priority
os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte("flat"), 0644)
os.MkdirAll(filepath.Join(dir, "dws"), 0755)
os.WriteFile(filepath.Join(dir, "dws", "SKILL.md"), []byte("nested"), 0644)
result := LocateSkillMD(dir)
want := filepath.Join(dir, "dws")
if result != want {
t.Errorf("LocateSkillMD() = %q, want nested %q", result, want)
}
}
func TestKnownSkillDirsNotEmpty(t *testing.T) {
if len(knownSkillDirs) == 0 {
t.Error("knownSkillDirs is empty")
}
// First entry should be the primary location
if knownSkillDirs[0] != ".agents/skills" {
t.Errorf("first knownSkillDir = %q, want .agents/skills", knownSkillDirs[0])
}
}