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>
256 lines
6.7 KiB
Go
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])
|
|
}
|
|
}
|