package scripts_test import ( "archive/tar" "archive/zip" "compress/gzip" "crypto/sha256" "encoding/hex" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/skillprovenance" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/skillstate" ) func assertSkillProvenance(t *testing.T, home, skillDir, name, source string) { t.Helper() state, readable, err := skillstate.Read(home) if err != nil || !readable { t.Fatalf("read unified Skill state: %#v, %v, %v", state, readable, err) } var provenance skillprovenance.Record for _, record := range state.ManagedSkills { if record.Name == name { provenance = record break } } if provenance.Name == "" || provenance.Source != source || provenance.Version == "" { t.Fatalf("Skill provenance %s = %#v", name, provenance) } digest, err := skillprovenance.DigestDir(skillDir) if err != nil { t.Fatalf("digest Skill directory for %s: %v", name, err) } if provenance.Digest != digest { t.Fatalf("Skill provenance digest %s = %q, want %q", name, provenance.Digest, digest) } } type installSourceFixture struct { root string scriptPath string stubRoot string fakeHome string } func newInstallSourceFixture(t *testing.T) *installSourceFixture { t.Helper() repoScript, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.sh")) if err != nil { t.Fatalf("Abs(install.sh) error = %v", err) } scriptData, err := os.ReadFile(repoScript) if err != nil { t.Fatalf("ReadFile(%s) error = %v", repoScript, err) } root := t.TempDir() scriptPath := filepath.Join(root, "scripts", "install.sh") mustWriteFile(t, scriptPath, scriptData, 0o755) // resolve_source_root requires both go.mod and cmd/. The source installer // tests need only that layout plus small representative skill trees. mustWriteFile(t, filepath.Join(root, "go.mod"), []byte("module example.com/dws-install-test\n"), 0o644) mustWriteFile(t, filepath.Join(root, "cmd", ".keep"), nil, 0o644) mustWriteFile(t, filepath.Join(root, "skills", "mono", "SKILL.md"), []byte("# Test skill\n"), 0o644) mustWriteFile(t, filepath.Join(root, "skills", "multi", "dingtalk-test", "SKILL.md"), []byte("# Test split skill\n"), 0o644) mustWriteFile(t, filepath.Join(root, "skills", "multi", "dws-shared", "SKILL.md"), []byte("# Test shared skill\n"), 0o644) stubRoot := filepath.Join(root, "stubs") makeStub := `#!/bin/sh set -eu dir="" while [ "$#" -gt 0 ]; do case "$1" in -C) dir="$2"; shift 2 ;; *) shift ;; esac done [ -n "$dir" ] && printf 'fake-binary\n' > "$dir/dws" ` mustWriteFile(t, filepath.Join(stubRoot, "make"), []byte(makeStub), 0o755) mustWriteFile(t, filepath.Join(stubRoot, "go"), []byte("#!/bin/sh\ntrue\n"), 0o755) return &installSourceFixture{ root: root, scriptPath: scriptPath, stubRoot: stubRoot, fakeHome: filepath.Join(root, "home"), } } func (f *installSourceFixture) env(extra ...string) []string { return f.envWithSkillMode("mono", extra...) } // envWithSkillMode builds the fixture environment. An empty mode omits // DWS_SKILL_MODE entirely so the installer exercises its own default (multi) // resolution; any inherited DWS_SKILL_MODE is filtered out either way. func (f *installSourceFixture) envWithSkillMode(mode string, extra ...string) []string { var env []string for _, kv := range os.Environ() { if strings.HasPrefix(kv, "DWS_SKILL_MODE=") { continue } env = append(env, kv) } env = append(env, "HOME="+f.fakeHome, "PATH="+f.stubRoot+":"+os.Getenv("PATH"), "DWS_VERSION=latest", "DWS_SKILLS_ONLY=0", ) if mode != "" { env = append(env, "DWS_SKILL_MODE="+mode) } return append(env, extra...) } func TestInstallScriptSourceModeInstallsBinary(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") cmd := exec.Command("sh", fixture.scriptPath) cmd.Env = fixture.env( "DWS_INSTALL_DIR="+installDir, "DWS_INSTALL_NAME=dws-test", "DWS_NO_SKILLS=1", ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install.sh error = %v\noutput:\n%s", err, string(output)) } got := string(output) for _, want := range []string{ "Installing dws from source checkout: " + fixture.root, "Install dir: " + installDir, "Binary installed:", installDir + "/dws-test", } { if !strings.Contains(got, want) { t.Fatalf("install output missing %q:\n%s", want, got) } } binaryPath := filepath.Join(installDir, "dws-test") binaryData, err := os.ReadFile(binaryPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", binaryPath, err) } if string(binaryData) != "fake-binary\n" { t.Fatalf("installed binary content = %q, want fake-binary", string(binaryData)) } } func TestInstallScriptSourceModeInstallsSkillsIntoAgentsDir(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") // Gate for index>0 agent dirs (matches build/npm/install.js): parent must exist. if err := os.MkdirAll(filepath.Join(fixture.fakeHome, ".cursor"), 0o755); err != nil { t.Fatalf("MkdirAll(.cursor) error = %v", err) } cmd := exec.Command("sh", fixture.scriptPath) cmd.Env = fixture.env( "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install.sh error = %v\noutput:\n%s", err, string(output)) } skillPath := filepath.Join(fixture.fakeHome, ".agents", "skills", "dws", "SKILL.md") if _, err := os.Stat(skillPath); err != nil { t.Fatalf("Stat(%s) error = %v\noutput:\n%s", skillPath, err, string(output)) } duplicatePath := filepath.Join(fixture.fakeHome, ".cursor", "skills", "dws") if _, err := os.Lstat(duplicatePath); !os.IsNotExist(err) { t.Fatalf("universal Cursor root must not duplicate canonical Skill: Lstat(%s) = %v\noutput:\n%s", duplicatePath, err, string(output)) } } func TestInstallPowerShellScriptInstallsToAgentsDir(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatalf("Abs(install.ps1) error = %v", err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } text := string(data) if !strings.Contains(text, ".agents\\skills") { t.Fatalf("install.ps1 missing .agents\\skills") } if !strings.Contains(text, ".cursor\\skills") { t.Fatalf("install.ps1 missing .cursor\\skills (AGENT_DIRS must match build/npm/install.js)") } } func TestInstallDevappScriptsReportSkillRollbackFailures(t *testing.T) { t.Parallel() checks := []struct { path string required string forbidden string }{ {filepath.Join("..", "..", "scripts", "install-devapp.sh"), "Skill rollback failed; backup retained at", "[ -z \"$backup\" ] || mv \"$backup\" \"$dest\" 2>/dev/null || true"}, {filepath.Join("..", "..", "scripts", "install-devapp.ps1"), "Die \"Skill install failed:", "(skill install skipped:"}, {filepath.Join("..", "..", "scripts", "install-event.sh"), "Skill rollback failed; backup retained at", "[ -z \"$backup\" ] || mv \"$backup\" \"$dest\" 2>/dev/null || true"}, } for _, check := range checks { data, err := os.ReadFile(check.path) if err != nil { t.Fatalf("ReadFile(%s) error = %v", check.path, err) } text := string(data) if !strings.Contains(text, check.required) { t.Errorf("%s missing explicit failure contract %q", check.path, check.required) } if strings.Contains(text, check.forbidden) { t.Errorf("%s still contains silent failure path %q", check.path, check.forbidden) } } } func TestInstallScriptsRemoveJunctionStagesLexically(t *testing.T) { t.Parallel() installPs1 := filepath.Join("..", "..", "scripts", "install.ps1") data, err := os.ReadFile(installPs1) if err != nil { t.Fatalf("ReadFile(%s) error = %v", installPs1, err) } text := string(data) for _, want := range []string{ "function Remove-LinkStageRoot", "Remove-LinkStageRoot -StageRoot $stageRoot", } { if !strings.Contains(text, want) { t.Errorf("install.ps1 missing junction-safe removal contract %q", want) } } // The rollback of published junction paths must go through the lexical // remover: Windows PowerShell 5.1 follows reparse points during // Remove-Item -Recurse and could delete the canonical store's contents. // Assert on the Restore-MultiSkillSet section so identity-anchor // refactors keep the contract without breaking on variable naming. begin := strings.Index(text, "function Restore-MultiSkillSet") if begin < 0 { t.Fatal("install.ps1 missing Restore-MultiSkillSet") } end := strings.Index(text[begin+1:], "\nfunction ") if end < 0 { t.Fatal("install.ps1 Restore-MultiSkillSet section not terminated") } restore := text[begin : begin+1+end] if !strings.Contains(restore, "Remove-SkillPathLexically") { t.Errorf("install.ps1 Restore-MultiSkillSet must remove published paths lexically") } if strings.Contains(restore, "Remove-Item") && strings.Contains(restore, "-Recurse") { t.Errorf("install.ps1 Restore-MultiSkillSet still contains a recursive removal:\n%s", restore) } devappPs1 := filepath.Join("..", "..", "scripts", "install-devapp.ps1") devappData, err := os.ReadFile(devappPs1) if err != nil { t.Fatalf("ReadFile(%s) error = %v", devappPs1, err) } for _, want := range []string{ "function Remove-DevLinkStageRoot", "Remove-DevLinkStageRoot $stageRoot", } { if !strings.Contains(string(devappData), want) { t.Errorf("install-devapp.ps1 missing junction-safe removal contract %q", want) } } } func TestInstallEventScriptDegradesFailedAgentLoudly(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { t.Skip("POSIX shell installer test is for unix-like hosts") } if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { t.Skipf("unsupported test os %s", runtime.GOOS) } if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { t.Skipf("unsupported test arch %s", runtime.GOARCH) } root := t.TempDir() fakeHome := filepath.Join(root, "home") installDir := filepath.Join(root, "bin") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz" if err := os.MkdirAll(releaseDir, 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", releaseDir, err) } writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{ "dws": "fake-event-binary\n", }) writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{ "multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n", "multi/dingtalk-shared/SKILL.md": "shared prerequisite\n", "multi/dingtalk-misc/SKILL.md": "clean misc oa routing\n", "mono/SKILL.md": "mono skill user_im_message_receive_o2o\n", }) writeInstallerFixtureChecksums(t, releaseDir) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) // A regular file where an agent skill base belongs makes that one agent // target uninstallable, while later agents (.zcode) stay reachable. mustWriteFile(t, filepath.Join(fakeHome, ".aider-desk", "skills"), []byte("not a directory\n"), 0o644) mustWriteFile(t, filepath.Join(fakeHome, ".zcode", "keep.txt"), []byte("zcode home\n"), 0o644) scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, "PATH="+stubRoot+":"+os.Getenv("PATH"), "EVENT_VERSION=v1.0.51", "DWS_INSTALL_DIR="+installDir, "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME="+assetName, ) output, err := cmd.CombinedOutput() if err == nil { t.Fatalf("install-event.sh should fail loudly when an agent target fails:\n%s", string(output)) } got := string(output) for _, want := range []string{ "Agent 目标安装失败,已跳过", "dingtalk-event 分发到 Agent 目录失败", } { if !strings.Contains(got, want) { t.Fatalf("install-event output missing degrade report %q:\n%s", want, got) } } // The failed agent must not abort the loop: later agents still get links. if _, err := os.Stat(filepath.Join(fakeHome, ".zcode", "skills", "dingtalk-event", "SKILL.md")); err != nil { t.Fatalf("later agent link missing after per-agent degrade: %v\noutput:\n%s", err, got) } if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event", "SKILL.md")); err != nil { t.Fatalf("canonical Skill missing: %v", err) } } func TestInstallPowerShellJunctionsResolvePhysicalRoot(t *testing.T) { // Prefer the advertised Windows PowerShell 5.1 irm|iex surface on Windows; // pwsh 7 can mask junction removal and resolution differences. powerShellNames := []string{"pwsh"} if runtime.GOOS == "windows" { powerShellNames = []string{"powershell", "pwsh"} } pwsh := "" for _, name := range powerShellNames { if candidate, lookupErr := exec.LookPath(name); lookupErr == nil { pwsh = candidate break } } if pwsh == "" { t.Skip("PowerShell is not available") } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") // Probe Test-SamePhysicalSkillRoot with both link flavors: junctions need // no privilege on Windows but silently no-op on non-Windows pwsh builds; // symlinks need no privilege on Unix but may fail without Developer Mode // on Windows. Exit 4 marks "no link flavor testable on this host". probe := prefix + ` $canonical = Join-Path $env:DWS_TEST_HOME '.agents\skills\dingtalk-chat' New-Item -ItemType Directory -Path $canonical -Force | Out-Null $other = Join-Path $env:DWS_TEST_HOME '.agents\skills\dingtalk-shared' New-Item -ItemType Directory -Path $other -Force | Out-Null $linkParent = Join-Path $env:DWS_TEST_HOME '.zcode\skills' New-Item -ItemType Directory -Path $linkParent -Force | Out-Null $asserted = 0 $junction = Join-Path $linkParent 'dingtalk-chat' New-Item -ItemType Junction -Path $junction -Target $canonical -ErrorAction SilentlyContinue | Out-Null if (Test-Path -LiteralPath $junction) { $asserted = 1 if (!(Test-SamePhysicalSkillRoot -Left $junction -Right $canonical)) { exit 3 } $junctionChain = Join-Path $linkParent 'dingtalk-chat-chain' New-Item -ItemType Junction -Path $junctionChain -Target $junction -ErrorAction Stop | Out-Null if (!(Test-SamePhysicalSkillRoot -Left $junctionChain -Right $canonical)) { exit 5 } } $symlink = Join-Path $linkParent 'dingtalk-shared' New-Item -ItemType SymbolicLink -Path $symlink -Target $other -ErrorAction SilentlyContinue | Out-Null if (Test-Path -LiteralPath $symlink) { $asserted = 1 if (!(Test-SamePhysicalSkillRoot -Left $symlink -Right $other)) { exit 3 } $symlinkChain = Join-Path $linkParent 'dingtalk-shared-chain' New-Item -ItemType SymbolicLink -Path $symlinkChain -Target $symlink -ErrorAction Stop | Out-Null if (!(Test-SamePhysicalSkillRoot -Left $symlinkChain -Right $other)) { exit 5 } } if ($asserted -eq 0) { exit 4 } if (Test-SamePhysicalSkillRoot -Left $canonical -Right $other) { exit 3 } if (Test-Path -LiteralPath $junction) { exit 0 } exit 4 ` probePath := filepath.Join(t.TempDir(), "junction-probe.ps1") mustWriteFile(t, probePath, []byte(probe), 0o644) home := t.TempDir() probeCmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", probePath) probeCmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home) probeOutput, probeErr := probeCmd.CombinedOutput() if probeErr != nil { exitCode := 0 if exitErr, ok := probeErr.(*exec.ExitError); ok { exitCode = exitErr.ExitCode() } if exitCode != 4 { t.Fatalf("link must resolve to its physical target (lexical Resolve-Path regression):\n%s", string(probeOutput)) } return // junctions unavailable on this host (non-Windows pwsh): probed via symlink only } // A rerun over an already-linked agent base must be idempotent: the // junction is recognized as published and never re-backed-up. install := prefix + ` if (!(Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_MULTI -Root $env:DWS_TEST_HOME)) { exit 2 } exit 0 ` installPath := filepath.Join(t.TempDir(), "junction-idempotent.ps1") mustWriteFile(t, installPath, []byte(install), 0o644) multi := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(multi, "dingtalk-chat", "SKILL.md"), []byte("new chat\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".zcode", "v2", "config.json"), []byte("{}\n"), 0o644) runInstall := func() string { cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", installPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_MULTI="+multi) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("PowerShell junction idempotency harness failed: %v\n%s", err, string(output)) } return string(output) } countBackups := func() int { entries, err := os.ReadDir(filepath.Join(home, ".dws", "skill-backups")) if err != nil { return 0 } return len(entries) } runInstall() first := countBackups() runInstall() if second := countBackups(); second != first { t.Fatalf("rerun re-backed-up already-published junctions (backup churn): first=%d second=%d", first, second) } linkData, err := os.ReadFile(filepath.Join(home, ".zcode", "skills", "dingtalk-chat", "SKILL.md")) if err != nil || string(linkData) != "new chat\n" { t.Fatalf("linked ZCode Skill content mismatch: %v %q", err, string(linkData)) } } func TestInstallPowerShellPublicationRacePreservesConcurrentDirectory(t *testing.T) { powerShellNames := []string{"pwsh"} if runtime.GOOS == "windows" { powerShellNames = []string{"powershell", "pwsh"} } pwsh := "" for _, name := range powerShellNames { if candidate, err := exec.LookPath(name); err == nil { pwsh = candidate break } } if pwsh == "" { t.Skip("PowerShell is not available") } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") harness := prefix + ` $root = $env:DWS_TEST_HOME $stage = Join-Path $root 'stage-payload' $dest = Join-Path $root 'agent\skills\dingtalk-chat' New-Item -ItemType Directory -Path $stage -Force | Out-Null New-Item -ItemType Directory -Path $dest -Force | Out-Null Set-Content -LiteralPath (Join-Path $dest 'user-data.txt') -Value 'keep' try { Move-SkillPath -Source $stage -Destination $dest exit 11 } catch {} if (!(Test-Path -LiteralPath $stage -PathType Container)) { exit 12 } if (Test-Path -LiteralPath (Join-Path $dest 'stage-payload')) { exit 13 } if (!(Test-Path -LiteralPath (Join-Path $dest 'user-data.txt') -PathType Leaf)) { exit 14 } # Simulate a later failure after publication, with another process replacing # our link before rollback. Identity-aware rollback must retain its directory. $canonical = Join-Path $root '.agents\skills\dingtalk-chat' New-Item -ItemType Directory -Path $canonical -Force | Out-Null $publishedPath = Join-Path $root 'published-link' $linkType = if ([System.Environment]::OSVersion.Platform -eq [System.PlatformID]::Win32NT) { 'Junction' } else { 'SymbolicLink' } New-Item -ItemType $linkType -Path $publishedPath -Target $canonical -ErrorAction Stop | Out-Null $record = New-PublishedSkillLinkRecord -Path $publishedPath Remove-SkillPathLexically -Path $publishedPath New-Item -ItemType Directory -Path $publishedPath -Force | Out-Null Set-Content -LiteralPath (Join-Path $publishedPath 'concurrent-user-data.txt') -Value 'keep' $restored = Restore-MultiSkillSet -Published @($record) -Backups @() if ($restored) { exit 15 } if (!(Test-Path -LiteralPath (Join-Path $publishedPath 'concurrent-user-data.txt') -PathType Leaf)) { exit 16 } # Copied mono/multi publications use the same identity-protected rollback. $copySource = Join-Path $root 'copy-source' $copyPath = Join-Path $root 'published-copy' New-Item -ItemType Directory -Path $copySource -Force | Out-Null Set-Content -LiteralPath (Join-Path $copySource 'SKILL.md') -Value 'transaction copy' Copy-SkillPathLexically -Source $copySource -Destination $copyPath $copyRecord = New-PublishedSkillCopyRecord -Path $copyPath -Source $copySource Remove-SkillPathLexically -Path $copyPath New-Item -ItemType Directory -Path $copyPath -Force | Out-Null Set-Content -LiteralPath (Join-Path $copyPath 'concurrent-copy-data.txt') -Value 'keep' $copyRestored = Restore-MultiSkillSet -Published @($copyRecord) -Backups @() if ($copyRestored) { exit 17 } if (!(Test-Path -LiteralPath (Join-Path $copyPath 'concurrent-copy-data.txt') -PathType Leaf)) { exit 18 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "publication-race.ps1") mustWriteFile(t, harnessPath, []byte(harness), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+t.TempDir()) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell publication race must retain concurrent directory: %v\n%s", err, string(output)) } } func TestInstallScriptsUseGitHubReleaseSkillsAsset(t *testing.T) { t.Parallel() for _, rel := range []string{ filepath.Join("..", "..", "scripts", "install.sh"), filepath.Join("..", "..", "scripts", "install-event.sh"), filepath.Join("..", "..", "scripts", "install-devapp.sh"), filepath.Join("..", "..", "scripts", "install.ps1"), filepath.Join("..", "..", "scripts", "install-devapp.ps1"), filepath.Join("..", "..", "scripts", "install-skills.sh"), } { scriptPath, err := filepath.Abs(rel) if err != nil { t.Fatalf("Abs(%s) error = %v", rel, err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } text := string(data) if !strings.Contains(text, "releases/download") || !strings.Contains(text, "dws-skills.zip") { t.Fatalf("%s should download dws-skills.zip from GitHub Releases", scriptPath) } if !strings.Contains(text, "checksums.txt") || !strings.Contains(strings.ToLower(text), "sha256") { t.Fatalf("%s should verify dws-skills.zip against release checksums", scriptPath) } if strings.Contains(text, "archive/refs/heads/main.tar.gz") || strings.Contains(text, "archive/refs/tags/") { t.Fatalf("%s should not download skills from repository archive refs", scriptPath) } } } func TestInstallEventScriptStaticExpectations(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } text := string(data) for _, want := range []string{ "DingTalk-Real-AI/dingtalk-workspace-cli", "releases/latest", "EVENT_VERSION", "DWS_SKILLS_ONLY", "dingtalk-event", "dingtalk-shared", "dingtalk-misc", "user_im_message_receive_o2o", ".config/opencode/skills", "$HOME/.dws/skills/multi/$EVENT_SKILL_NAME", "$HOME/.dws/skills/mono", } { if !strings.Contains(text, want) { t.Fatalf("install-event.sh missing %q", want) } } for _, avoid := range []string{ "releases?per_page=30", "select(.tag_name", "dingtalk-dev", "client-secret", "--as app", } { if strings.Contains(text, avoid) { t.Fatalf("install-event.sh should not expose old app/dev install content %q", avoid) } } } func TestInstallEventScriptInstallsBinaryAndEventSkills(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { t.Skip("POSIX shell installer test is for unix-like hosts") } root := t.TempDir() fakeHome := filepath.Join(root, "home") installDir := filepath.Join(root, "bin") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz" if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { t.Skipf("unsupported test arch %s", runtime.GOARCH) } if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { t.Skipf("unsupported test os %s", runtime.GOOS) } if err := os.MkdirAll(releaseDir, 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", releaseDir, err) } writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{ "dws": "fake-event-binary\n", }) writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{ "multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n", "multi/dingtalk-shared/SKILL.md": "shared prerequisite\n", "multi/dingtalk-misc/SKILL.md": "clean misc oa routing\n", "mono/SKILL.md": "mono skill user_im_message_receive_o2o\n", "SKILL.md": "legacy mono root\n", }) writeInstallerFixtureChecksums(t, releaseDir) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) for _, dir := range []string{ filepath.Join(fakeHome, ".codex"), filepath.Join(fakeHome, ".config", "opencode"), } { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", dir, err) } } // Simulate both upgrade shapes: an old standalone Event skill and the // short-lived folded Event-in-misc layout. An unrelated sibling must stay. mustWriteFile(t, filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event", "SKILL.md"), []byte("old standalone event\n"), 0o644) mustWriteFile(t, filepath.Join(fakeHome, ".agents", "skills", "dingtalk-misc", "SKILL.md"), []byte("old misc dws event\n"), 0o644) mustWriteFile(t, filepath.Join(fakeHome, ".agents", "skills", "dingtalk-misc", "references", "event.md"), []byte("folded event docs\n"), 0o644) mustWriteFile(t, filepath.Join(fakeHome, ".agents", "skills", "dingtalk-chat", "SKILL.md"), []byte("keep sibling\n"), 0o644) scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, "PATH="+stubRoot+":"+os.Getenv("PATH"), "EVENT_VERSION=v1.0.51", "DWS_INSTALL_DIR="+installDir, "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME="+assetName, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install-event.sh error = %v\noutput:\n%s", err, string(output)) } repeat := exec.Command("sh", scriptPath) repeat.Env = cmd.Env if repeatOutput, repeatErr := repeat.CombinedOutput(); repeatErr != nil { t.Fatalf("second install-event.sh run should be idempotent: %v\noutput:\n%s", repeatErr, string(repeatOutput)) } got := string(output) for _, want := range []string{ "Version: v1.0.51", "Skill dingtalk-event", "Skill dingtalk-shared", "Skill dingtalk-misc", "Skill dws", "dws event consume user_im_message_receive_o2o", } { if !strings.Contains(got, want) { t.Fatalf("install-event output missing %q:\n%s", want, got) } } binaryData, err := os.ReadFile(filepath.Join(installDir, "dws")) if err != nil { t.Fatalf("ReadFile(installed dws) error = %v", err) } if string(binaryData) != "fake-event-binary\n" { t.Fatalf("installed binary content = %q", string(binaryData)) } expectedSkills := map[string]string{ ".agents/skills/dingtalk-event/SKILL.md": "user_im_message_receive_o2o", ".agents/skills/dingtalk-shared/SKILL.md": "shared prerequisite", ".agents/skills/dingtalk-misc/SKILL.md": "clean misc oa routing", ".agents/skills/dws/SKILL.md": "user_im_message_receive_o2o", ".dws/skills/multi/dingtalk-event/SKILL.md": "user_im_message_receive_o2o", ".dws/skills/multi/dingtalk-shared/SKILL.md": "shared prerequisite", ".dws/skills/multi/dingtalk-misc/SKILL.md": "clean misc oa routing", ".dws/skills/mono/SKILL.md": "user_im_message_receive_o2o", } for _, duplicate := range []string{ filepath.Join(fakeHome, ".codex", "skills", "dingtalk-event"), filepath.Join(fakeHome, ".config", "opencode", "skills", "dingtalk-event"), filepath.Join(fakeHome, ".codex", "skills", "dws"), } { if _, err := os.Lstat(duplicate); !os.IsNotExist(err) { t.Fatalf("universal Agent duplicate remains at %s: %v", duplicate, err) } } for rel, marker := range expectedSkills { p := filepath.Join(fakeHome, filepath.FromSlash(rel)) data, err := os.ReadFile(p) if err != nil { t.Fatalf("ReadFile(%s) error = %v\noutput:\n%s", p, err, got) } if !strings.Contains(string(data), marker) { t.Fatalf("%s does not contain %q: %q", p, marker, string(data)) } } if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-misc", "references", "event.md")); !os.IsNotExist(err) { t.Fatalf("folded misc event reference should be removed, stat err=%v", err) } sibling, err := os.ReadFile(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-chat", "SKILL.md")) if err != nil || string(sibling) != "keep sibling\n" { t.Fatalf("unrelated sibling changed: data=%q err=%v", sibling, err) } if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", "dingtalk-dev")); !os.IsNotExist(err) { t.Fatalf("dingtalk-dev should not be installed by install-event.sh, stat err=%v", err) } } func TestInstallEventScriptSkillsOnlySkipsBinary(t *testing.T) { t.Parallel() root := t.TempDir() fakeHome := filepath.Join(root, "home") installDir := filepath.Join(root, "bin") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") if err := os.MkdirAll(releaseDir, 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", releaseDir, err) } writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{ "multi/dingtalk-event/SKILL.md": "event skill user_im_message_receive_o2o\n", "multi/dingtalk-shared/SKILL.md": "shared prerequisite\n", "multi/dingtalk-misc/SKILL.md": "clean misc oa routing\n", "mono/SKILL.md": "mono skill user_im_message_receive_o2o\n", }) writeInstallerFixtureChecksums(t, releaseDir) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, "PATH="+stubRoot+":"+os.Getenv("PATH"), "EVENT_VERSION=v1.0.51", "DWS_INSTALL_DIR="+installDir, "DWS_SKILLS_ONLY=1", "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME=unused", ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install-event.sh skills-only error = %v\noutput:\n%s", err, string(output)) } if _, err := os.Stat(filepath.Join(installDir, "dws")); !os.IsNotExist(err) { t.Fatalf("DWS_SKILLS_ONLY=1 should not install binary, stat err=%v\noutput:\n%s", err, string(output)) } for _, name := range []string{"dingtalk-event", "dingtalk-shared", "dingtalk-misc", "dws"} { if _, err := os.Stat(filepath.Join(fakeHome, ".agents", "skills", name, "SKILL.md")); err != nil { t.Fatalf("skills-only should install %s: %v\noutput:\n%s", name, err, string(output)) } } } func TestInstallEventScriptPreflightFailureDoesNotChangeSkills(t *testing.T) { t.Parallel() root := t.TempDir() fakeHome := filepath.Join(root, "home") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") // Deliberately omit dingtalk-misc. The installer must resolve all members // from this one bundle before replacing any installed or cached directory. writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{ "multi/dingtalk-event/SKILL.md": "new event\n", "multi/dingtalk-shared/SKILL.md": "new shared\n", "mono/SKILL.md": "new mono\n", }) writeInstallerFixtureChecksums(t, releaseDir) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) existing := filepath.Join(fakeHome, ".agents", "skills", "dingtalk-event", "SKILL.md") mustWriteFile(t, existing, []byte("old event stays\n"), 0o644) scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, "PATH="+stubRoot+":"+os.Getenv("PATH"), "EVENT_VERSION=v1.0.51", "DWS_SKILLS_ONLY=1", "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME=unused", ) output, err := cmd.CombinedOutput() if err == nil { t.Fatalf("install-event.sh should fail when bundle misses misc:\n%s", string(output)) } if !strings.Contains(string(output), "dingtalk-misc not found") { t.Fatalf("preflight error should name missing misc:\n%s", string(output)) } data, readErr := os.ReadFile(existing) if readErr != nil || string(data) != "old event stays\n" { t.Fatalf("preflight failure changed installed event: data=%q err=%v", data, readErr) } if _, statErr := os.Stat(filepath.Join(fakeHome, ".dws", "skills")); !os.IsNotExist(statErr) { t.Fatalf("preflight failure should not populate cache, stat err=%v", statErr) } } func TestInstallEventChecksumMismatchPreservesExistingSkills(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("POSIX shell installer test is for unix-like hosts") } root := t.TempDir() home := filepath.Join(root, "home") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") writeZip(t, filepath.Join(releaseDir, "dws-skills.zip"), map[string]string{ "multi/dingtalk-event/SKILL.md": "new event\n", "multi/dingtalk-shared/SKILL.md": "new shared\n", "multi/dingtalk-misc/SKILL.md": "new misc\n", "mono/SKILL.md": "new mono\n", }) mustWriteFile(t, filepath.Join(releaseDir, "checksums.txt"), []byte(strings.Repeat("0", 64)+" dws-skills.zip\n"), 0o644) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) existing := filepath.Join(home, ".agents", "skills", "dingtalk-event", "SKILL.md") mustWriteFile(t, existing, []byte("old event stays\n"), 0o644) scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatal(err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+home, "PATH="+stubRoot+":"+os.Getenv("PATH"), "EVENT_VERSION=v1.0.51", "DWS_SKILLS_ONLY=1", "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME=unused", ) output, runErr := cmd.CombinedOutput() if runErr == nil || !strings.Contains(string(output), "checksum mismatch") { t.Fatalf("checksum mismatch result = %v\n%s", runErr, output) } if got, err := os.ReadFile(existing); err != nil || string(got) != "old event stays\n" { t.Fatalf("existing Skill after checksum mismatch = %q, %v", got, err) } } func TestInstallEventScriptNoSkillsOnlyInstallsBinary(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { t.Skip("POSIX shell installer test is for unix-like hosts") } if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { t.Skipf("unsupported test arch %s", runtime.GOARCH) } if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { t.Skipf("unsupported test os %s", runtime.GOOS) } root := t.TempDir() fakeHome := filepath.Join(root, "home") installDir := filepath.Join(root, "bin") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz" writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{ "dws": "fake-event-binary\n", }) writeInstallerFixtureChecksums(t, releaseDir) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) for _, name := range []string{"dingtalk-event", "dingtalk-shared", "dingtalk-misc"} { mustWriteFile(t, filepath.Join(fakeHome, ".agents", "skills", name, "SKILL.md"), []byte("keep "+name+"\n"), 0o644) } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, "PATH="+stubRoot+":"+os.Getenv("PATH"), "EVENT_VERSION=v1.0.51", "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=1", "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME="+assetName, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install-event.sh no-skills error = %v\noutput:\n%s", err, string(output)) } if _, err := os.Stat(filepath.Join(installDir, "dws")); err != nil { t.Fatalf("DWS_NO_SKILLS=1 should install binary: %v\noutput:\n%s", err, string(output)) } for _, name := range []string{"dingtalk-event", "dingtalk-shared", "dingtalk-misc"} { data, err := os.ReadFile(filepath.Join(fakeHome, ".agents", "skills", name, "SKILL.md")) if err != nil || string(data) != "keep "+name+"\n" { t.Fatalf("DWS_NO_SKILLS=1 changed %s: data=%q err=%v\noutput:\n%s", name, data, err, string(output)) } } } func TestInstallEventScriptDefaultsToLatestStableRelease(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { t.Skip("POSIX shell installer test is for unix-like hosts") } if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { t.Skipf("unsupported test arch %s", runtime.GOARCH) } if runtime.GOOS != "darwin" && runtime.GOOS != "linux" { t.Skipf("unsupported test os %s", runtime.GOOS) } root := t.TempDir() fakeHome := filepath.Join(root, "home") installDir := filepath.Join(root, "bin") releaseDir := filepath.Join(root, "release") stubRoot := filepath.Join(root, "stubs") assetName := "dws-" + runtime.GOOS + "-" + runtime.GOARCH + ".tar.gz" writeTarGz(t, filepath.Join(releaseDir, assetName), map[string]string{ "dws": "fake-event-binary\n", }) writeInstallerFixtureChecksums(t, releaseDir) writeFakeCurl(t, filepath.Join(stubRoot, "curl")) writeFakeGH(t, filepath.Join(stubRoot, "gh"), "v1.0.51") scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-event.sh")) if err != nil { t.Fatalf("Abs(install-event.sh) error = %v", err) } cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, "PATH="+stubRoot+":"+os.Getenv("PATH"), "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=1", "FAKE_RELEASE_DIR="+releaseDir, "FAKE_ASSET_NAME="+assetName, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install-event.sh latest release error = %v\noutput:\n%s", err, string(output)) } if !strings.Contains(string(output), "Version: v1.0.51") { t.Fatalf("install-event.sh did not resolve the latest stable version:\n%s", string(output)) } } func TestInstallScriptsUseFlattenedSkillsSourceRoot(t *testing.T) { t.Parallel() checks := []struct { relPath string want string avoid string }{ { relPath: filepath.Join("..", "..", "scripts", "install.sh"), want: `skill_src="${root}/skills/mono"`, avoid: `skill_src="${root}/skills/${SKILL_NAME}"`, }, { relPath: filepath.Join("..", "..", "scripts", "install.ps1"), want: `$skillSrc = Join-Path (Join-Path $Root "skills") "mono"`, avoid: `$skillSrc = Join-Path $Root "skills\$SkillName"`, }, } for _, tc := range checks { scriptPath, err := filepath.Abs(tc.relPath) if err != nil { t.Fatalf("Abs(%s) error = %v", tc.relPath, err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } text := string(data) if !strings.Contains(text, tc.want) { t.Fatalf("%s missing flattened skills root %q", scriptPath, tc.want) } if strings.Contains(text, tc.avoid) { t.Fatalf("%s still references legacy nested skills root %q", scriptPath, tc.avoid) } } } func TestInstallScriptsExposeSkillModeSelection(t *testing.T) { t.Parallel() shPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.sh")) if err != nil { t.Fatalf("Abs(install.sh) error = %v", err) } shData, err := os.ReadFile(shPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", shPath, err) } shText := string(shData) // install.sh must honor DWS_SKILL_MODE, expose mono/multi, and check TTY via [ -t 0 ]. for _, want := range []string{ "DWS_SKILL_MODE", "mono", "multi", "[ -t 0 ]", "dws skill setup --mode multi", } { if !strings.Contains(shText, want) { t.Fatalf("install.sh missing %q (needed for skill mode selection)", want) } } ps1Path, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatalf("Abs(install.ps1) error = %v", err) } ps1Data, err := os.ReadFile(ps1Path) if err != nil { t.Fatalf("ReadFile(%s) error = %v", ps1Path, err) } ps1Text := string(ps1Data) for _, want := range []string{ "DWS_SKILL_MODE", "mono", "multi", "IsInputRedirected", "dws skill setup --mode multi", } { if !strings.Contains(ps1Text, want) { t.Fatalf("install.ps1 missing %q (needed for skill mode selection)", want) } } } func TestBuildEntrypointsUseStripLdflags(t *testing.T) { t.Parallel() checks := []struct { relPath string want string }{ { relPath: filepath.Join("..", "..", "scripts", "install.ps1"), want: `go build -ldflags="-s -w" -o $tmpBin "$Root/cmd"`, }, { relPath: filepath.Join("..", "..", "scripts", "dev", "build.sh"), want: `-ldflags="-s -w"`, }, } for _, tc := range checks { scriptPath, err := filepath.Abs(tc.relPath) if err != nil { t.Fatalf("Abs(%s) error = %v", tc.relPath, err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } if !strings.Contains(string(data), tc.want) { t.Fatalf("%s missing stripped ldflags build invocation %q", scriptPath, tc.want) } } } func TestPolicyBinaryChecksReuseBuiltDWS(t *testing.T) { t.Parallel() for _, relPath := range []string{ filepath.Join("..", "..", "scripts", "policy", "check-command-surface.sh"), filepath.Join("..", "..", "scripts", "policy", "check-schema-binary.sh"), } { scriptPath, err := filepath.Abs(relPath) if err != nil { t.Fatalf("Abs(%s) error = %v", relPath, err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } text := string(data) if !strings.Contains(text, `BIN="${DWS_BIN:-$ROOT/dws}"`) { t.Fatalf("%s does not reuse DWS_BIN", scriptPath) } if strings.Contains(text, `go build -o "$tmp/dws" ./cmd`) || strings.Contains(text, `go build -ldflags="-s -w"`) { t.Fatalf("%s unexpectedly rebuilds dws", scriptPath) } } } // TestInstallScriptsCacheMultiSkills verifies install.sh / install.ps1 / // install-skills.sh / build/npm/install.js all carry the wiring that caches // the multi/ tree to ~/.dws/skills/multi/ during install. This is what lets // `dws skill setup --mode multi` find a source on a fresh machine. func TestInstallScriptsCacheMultiSkills(t *testing.T) { t.Parallel() checks := []struct { relPath string wants []string }{ { relPath: filepath.Join("..", "..", "scripts", "install.sh"), wants: []string{ "cache_multi_skills", "${HOME}/.dws/skills/multi", "cache_mono_skills", }, }, { relPath: filepath.Join("..", "..", "scripts", "install.ps1"), wants: []string{ "Cache-MultiSkills", ".dws\\skills\\multi", "Cache-MonoSkills", }, }, { relPath: filepath.Join("..", "..", "scripts", "install-skills.sh"), wants: []string{ "${DWS_CACHE_ROOT}/skills/multi", "${DWS_CACHE_ROOT}/skills/mono", }, }, { relPath: filepath.Join("..", "..", "build", "npm", "install.js"), wants: []string{ "cacheUserSkills", ".dws", "\"multi\"", "\"mono\"", }, }, } for _, tc := range checks { scriptPath, err := filepath.Abs(tc.relPath) if err != nil { t.Fatalf("Abs(%s) error = %v", tc.relPath, err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatalf("ReadFile(%s) error = %v", scriptPath, err) } text := string(data) for _, want := range tc.wants { if !strings.Contains(text, want) { t.Fatalf("%s missing %q (needed for multi-skill caching)", scriptPath, want) } } } } // TestInstallScriptCachesMultiEndToEnd runs install.sh in source-checkout mode // with a fake HOME, then verifies that ~/.dws/skills/multi/ ends up populated // with the per-product skills from skills/multi/. func TestInstallScriptCachesMultiEndToEnd(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") cmd := exec.Command("sh", fixture.scriptPath) cmd.Env = fixture.env( "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install.sh error = %v\noutput:\n%s", err, string(output)) } // Verify multi cache was populated. We expect dingtalk-* subdirs. multiCache := filepath.Join(fixture.fakeHome, ".dws", "skills", "multi") entries, err := os.ReadDir(multiCache) if err != nil { t.Fatalf("ReadDir(%s) error = %v\noutput:\n%s", multiCache, err, string(output)) } foundDingtalk := 0 for _, e := range entries { if e.IsDir() && strings.HasPrefix(e.Name(), "dingtalk-") { foundDingtalk++ } } if foundDingtalk == 0 { t.Fatalf("no dingtalk-* entries under %s: %v\noutput:\n%s", multiCache, entries, string(output)) } // And verify mono cache. monoCacheSkill := filepath.Join(fixture.fakeHome, ".dws", "skills", "mono", "SKILL.md") if _, err := os.Stat(monoCacheSkill); err != nil { t.Fatalf("missing mono cache SKILL.md at %s: %v", monoCacheSkill, err) } } // seedAgentHome pre-creates /.agents/skills//SKILL.md with the // given content, simulating a pre-existing skill installation. func seedAgentHome(t *testing.T, fakeHome, name, content string) string { t.Helper() p := filepath.Join(fakeHome, ".agents", "skills", name, "SKILL.md") mustWriteFile(t, p, []byte(content), 0o644) return p } func runInstallScript(t *testing.T, scriptPath string, env []string) string { t.Helper() cmd := exec.Command("sh", scriptPath) cmd.Env = env output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("install.sh error = %v\noutput:\n%s", err, string(output)) } return string(output) } // TestInstallScriptSourceModeDefaultMultiInstall exercises the default (no // DWS_SKILL_MODE, non-TTY) path: multi must win, the mono leftover (dws/) and // a stale dingtalk-* skill absent from the bundle must be removed, and // dws-shared must land alongside the product skill. func TestInstallScriptSourceModeDefaultMultiInstall(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") seedAgentHome(t, fixture.fakeHome, "dws", "old mono\n") seedAgentHome(t, fixture.fakeHome, "dingtalk-stale", "stale\n") if err := skillstate.Write(fixture.fakeHome, skillstate.State{ManagedSkills: []skillprovenance.Record{{Name: "dingtalk-stale"}}}); err != nil { t.Fatal(err) } seedAgentHome(t, fixture.fakeHome, "dingtalk-custom", "market skill\n") seedAgentHome(t, fixture.fakeHome, "other-skill", "not dws\n") output := runInstallScript(t, fixture.scriptPath, fixture.envWithSkillMode("", "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", )) if !strings.Contains(output, "Installing agent skills (multi) from local source") { t.Fatalf("expected multi install branch, output:\n%s", output) } base := filepath.Join(fixture.fakeHome, ".agents", "skills") for _, name := range []string{"dingtalk-test", "dws-shared"} { if _, err := os.Stat(filepath.Join(base, name, "SKILL.md")); err != nil { t.Errorf("multi skill %q not installed: %v\noutput:\n%s", name, err, output) } } for _, gone := range []string{"dws", "dingtalk-stale"} { if _, err := os.Stat(filepath.Join(base, gone)); !os.IsNotExist(err) { t.Errorf("%q should be removed by the multi install, stat err=%v\noutput:\n%s", gone, err, output) } } // Removals must be reversible: the displaced dirs land under // ~/.dws/skill-backups/ instead of being destroyed. if findSkillBackup(fixture.fakeHome, "dws", "old mono\n") == "" { t.Errorf("old mono dws/ should be backed up under .dws/skill-backups, output:\n%s", output) } if findSkillBackup(fixture.fakeHome, "dingtalk-stale", "stale\n") == "" { t.Errorf("stale dingtalk-* should be backed up under .dws/skill-backups, output:\n%s", output) } if _, err := os.Stat(filepath.Join(base, "other-skill", "SKILL.md")); err != nil { t.Errorf("non-DWS skill must be preserved: %v", err) } if data, err := os.ReadFile(filepath.Join(base, "dingtalk-custom", "SKILL.md")); err != nil || string(data) != "market skill\n" { t.Errorf("unregistered market/user dingtalk-* skill must be preserved: data=%q err=%v", string(data), err) } assertSkillProvenance(t, fixture.fakeHome, filepath.Join(base, "dingtalk-test"), "dingtalk-test", "install.sh") } // TestInstallScriptSourceModeEmptyMultiFallsBackToMono pins the empty-bundle // guard: a multi/ tree without any */SKILL.md must fall back to the mono // branch (with a warning) instead of wiping the user's existing skills and // installing nothing. The outcome must equal a normal mono install: dws/ is // replaced with the new mono content and dingtalk-* leftovers are removed by // mono mutual exclusion — the failure mode being guarded against is the // empty-state wipe. func TestInstallScriptSourceModeEmptyMultiFallsBackToMono(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") // Corrupt the multi tree: subdirs exist but no SKILL.md anywhere. if err := os.Remove(filepath.Join(fixture.root, "skills", "multi", "dingtalk-test", "SKILL.md")); err != nil { t.Fatalf("Remove(dingtalk-test/SKILL.md) error = %v", err) } if err := os.RemoveAll(filepath.Join(fixture.root, "skills", "multi", "dws-shared")); err != nil { t.Fatalf("RemoveAll(dws-shared) error = %v", err) } seedAgentHome(t, fixture.fakeHome, "dws", "old mono\n") seedAgentHome(t, fixture.fakeHome, "dingtalk-aitable", "pre-state official\n") seedAgentHome(t, fixture.fakeHome, "dingtalk-keep", "keep\n") seedAgentHome(t, fixture.fakeHome, "other-skill", "not dws\n") output := runInstallScript(t, fixture.scriptPath, fixture.envWithSkillMode("multi", "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", )) if !strings.Contains(output, "falling back to mono") { t.Fatalf("expected mono fallback warning, output:\n%s", output) } base := filepath.Join(fixture.fakeHome, ".agents", "skills") data, err := os.ReadFile(filepath.Join(base, "dws", "SKILL.md")) if err != nil || !strings.Contains(string(data), "# Test skill") { t.Fatalf("mono dws/ not (re)installed from skills/mono (data=%q, err=%v) — empty multi must not wipe skills\noutput:\n%s", string(data), err, output) } // An unregistered dingtalk-* directory has unknown ownership and must survive. if _, err := os.Stat(filepath.Join(base, "dingtalk-keep", "SKILL.md")); err != nil { t.Errorf("unregistered dingtalk-keep should survive mono fallback: %v", err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-aitable")); !os.IsNotExist(err) { t.Errorf("pre-state official dingtalk-aitable should be migrated during mono fallback: %v", err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-test")); !os.IsNotExist(err) { t.Errorf("dingtalk-test must not be installed from the empty multi tree, stat err=%v", err) } if _, err := os.Stat(filepath.Join(base, "other-skill", "SKILL.md")); err != nil { t.Errorf("non-DWS skill must be preserved: %v", err) } } // TestInstallScriptSourceModeMonoMultiMonoExclusion runs mono → multi → mono // against the same fake HOME and asserts mutual exclusion in both directions, // including the dws-shared shared bundle. func TestInstallScriptSourceModeMonoMultiMonoExclusion(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") base := filepath.Join(fixture.fakeHome, ".agents", "skills") run := func(mode string) string { t.Helper() return runInstallScript(t, fixture.scriptPath, fixture.envWithSkillMode(mode, "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", )) } assertPresent := func(rel string) { t.Helper() if _, err := os.Stat(filepath.Join(base, rel, "SKILL.md")); err != nil { t.Errorf("%s/SKILL.md should exist: %v", rel, err) } } assertAbsent := func(rel string) { t.Helper() if _, err := os.Stat(filepath.Join(base, rel)); !os.IsNotExist(err) { t.Errorf("%s should be absent, stat err=%v", rel, err) } } run("mono") assertPresent("dws") assertAbsent("dingtalk-test") assertAbsent("dws-shared") out := run("multi") if !strings.Contains(out, "Installing agent skills (multi)") { t.Fatalf("expected multi branch, output:\n%s", out) } assertAbsent("dws") assertPresent("dingtalk-test") assertPresent("dws-shared") run("mono") assertPresent("dws") assertAbsent("dingtalk-test") assertAbsent("dws-shared") // Every mutual-exclusion removal along the mono→multi→mono chain must // surface as a backup under ~/.dws/skill-backups/ with the old content // intact, never as a silent hard delete. if got := findSkillBackup(fixture.fakeHome, "dws", "# Test skill\n"); got == "" { t.Errorf("mono dws/ replaced by the multi run should be backed up") } if got := findSkillBackup(fixture.fakeHome, "dingtalk-test", "# Test split skill\n"); got == "" { t.Errorf("dingtalk-test replaced by the final mono run should be backed up") } if got := findSkillBackup(fixture.fakeHome, "dws-shared", "# Test shared skill\n"); got == "" { t.Errorf("dws-shared replaced by the final mono run should be backed up") } } // findSkillBackup searches fakeHome/.dws/skill-backups recursively for a // directory named name whose SKILL.md equals content, returning its path // ("" when absent). The layout is // with // optional -N collision suffixes on the stamp. func findSkillBackup(fakeHome, name, content string) string { root := filepath.Join(fakeHome, ".dws", "skill-backups") var found string _ = filepath.Walk(root, func(p string, info os.FileInfo, err error) error { if err != nil || found != "" || !info.IsDir() || info.Name() != name { return nil } if data, err := os.ReadFile(filepath.Join(p, "SKILL.md")); err == nil && string(data) == content { found = p } return nil }) return found } // TestInstallScriptBackupFailureKeepsOriginalDir pins the fail-safe contract: // when the backup destination cannot be created (skill-backups pre-created as // a regular file), the installer must keep every pre-existing skill directory // untouched and skip the affected Agent target — a backup failure must never // degrade into a hard delete or leave a mixed mono + multi layout. func TestInstallScriptBackupFailureKeepsOriginalDir(t *testing.T) { t.Parallel() fixture := newInstallSourceFixture(t) installDir := filepath.Join(fixture.root, "bin") base := filepath.Join(fixture.fakeHome, ".agents", "skills") // Poison the backup root: mkdir -p / cannot succeed. mustWriteFile(t, filepath.Join(fixture.fakeHome, ".dws", "skill-backups"), []byte("not a directory\n"), 0o644) seedAgentHome(t, fixture.fakeHome, "dws", "old mono\n") monoCmd := exec.Command("sh", fixture.scriptPath) monoCmd.Env = fixture.envWithSkillMode("mono", "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", ) monoOutput, monoErr := monoCmd.CombinedOutput() if monoErr == nil { t.Fatalf("mono install unexpectedly succeeded after backup failure:\n%s", monoOutput) } out := string(monoOutput) if !strings.Contains(out, "保留原目录") { t.Fatalf("expected backup-failure warning in mono output:\n%s", out) } data, err := os.ReadFile(filepath.Join(base, "dws", "SKILL.md")) if err != nil || string(data) != "old mono\n" { t.Fatalf("mono run must keep the original dws/ untouched on backup failure (data=%q, err=%v)\noutput:\n%s", string(data), err, out) } cmd := exec.Command("sh", fixture.scriptPath) cmd.Env = fixture.envWithSkillMode("multi", "DWS_INSTALL_DIR="+installDir, "DWS_NO_SKILLS=0", ) output, runErr := cmd.CombinedOutput() if runErr == nil { t.Fatalf("multi install unexpectedly succeeded after backup failure:\n%s", output) } out = string(output) if !strings.Contains(out, "保留原目录") { t.Fatalf("expected backup-failure warning in multi output:\n%s", out) } data, err = os.ReadFile(filepath.Join(base, "dws", "SKILL.md")) if err != nil || string(data) != "old mono\n" { t.Fatalf("multi run must keep the mono leftover dws/ untouched on backup failure (data=%q, err=%v)\noutput:\n%s", string(data), err, out) } for _, name := range []string{"dingtalk-test", "dws-shared"} { if _, err := os.Stat(filepath.Join(base, name)); !os.IsNotExist(err) { t.Fatalf("multi run must not install %s after cleanup backup failure, stat err=%v\noutput:\n%s", name, err, out) } } } func TestInstallSkillsShellBackupFailureWritesNoMultiSkills(t *testing.T) { scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-skills.sh")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatal("install-skills.sh final main invocation not found") } library := filepath.Join(t.TempDir(), "install-skills-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() root := filepath.Join(t.TempDir(), "project") base := filepath.Join(root, ".agents", "skills") multi := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(base, "dws", "SKILL.md"), []byte("old mono\n"), 0o644) mustWriteFile(t, filepath.Join(multi, "dingtalk-test", "SKILL.md"), []byte("new multi\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".dws", "skill-backups"), []byte("not a directory\n"), 0o644) harness := `. "$DWS_TEST_LIBRARY" install_multi_skills_to_root "$DWS_TEST_MULTI" "$DWS_TEST_ROOT" ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_MULTI="+multi, "DWS_TEST_ROOT="+root, ) output, runErr := cmd.CombinedOutput() if runErr == nil { t.Fatalf("install-skills harness unexpectedly succeeded after backup failure:\n%s", output) } if !strings.Contains(string(output), "所有检测到的 Agent 目标均失败") { t.Fatalf("install-skills aggregate failure missing:\n%s", output) } if data, err := os.ReadFile(filepath.Join(base, "dws", "SKILL.md")); err != nil || string(data) != "old mono\n" { t.Fatalf("mono changed after backup failure (data=%q, err=%v)", string(data), err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-test")); !os.IsNotExist(err) { t.Fatalf("multi installed after backup failure, stat err=%v", err) } } func TestInstallSkillsShellMvCrossFilesystemBackupAndRestore(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("real cross-filesystem mv fixture uses Linux /dev/shm") } sharedMemoryRoot, err := os.MkdirTemp("/dev/shm", "dws-shell-exdev-") if err != nil { t.Skipf("/dev/shm is unavailable: %v", err) } t.Cleanup(func() { _ = os.RemoveAll(sharedMemoryRoot) }) scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-skills.sh")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatal("install-skills.sh final main invocation not found") } library := filepath.Join(t.TempDir(), "install-skills-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) source := filepath.Join(t.TempDir(), "dingtalk-chat") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("old skill\n"), 0o640) if err := os.Symlink("SKILL.md", filepath.Join(source, "skill-link")); err != nil { t.Fatal(err) } if err := os.Symlink("missing.md", filepath.Join(source, "dangling-link")); err != nil { t.Fatal(err) } probe := filepath.Join(sharedMemoryRoot, "rename-probe") if err := os.Rename(source, probe); err == nil { if restoreErr := os.Rename(probe, source); restoreErr != nil { t.Fatal(restoreErr) } t.Skip("/dev/shm and the test temp directory share a filesystem") } else if !strings.Contains(strings.ToLower(err.Error()), "cross-device") { t.Skipf("could not establish a cross-filesystem fixture: %v", err) } home := filepath.Join(sharedMemoryRoot, "home") manifest := filepath.Join(sharedMemoryRoot, "backups.manifest") harness := `. "$DWS_TEST_LIBRARY" backup_and_remove_skill_dir "$DWS_TEST_SOURCE" backup=$DWS_LAST_SKILL_BACKUP [ -n "$backup" ] && [ -d "$backup" ] && [ ! -e "$DWS_TEST_SOURCE" ] printf '%s\n%s\n' "$DWS_TEST_SOURCE" "$backup" > "$DWS_TEST_MANIFEST" restore_multi_skill_set /dev/null "$DWS_TEST_MANIFEST" ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, "DWS_TEST_MANIFEST="+manifest, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("cross-filesystem shell backup/restore failed: %v\n%s", err, output) } if got, err := os.ReadFile(filepath.Join(source, "SKILL.md")); err != nil || string(got) != "old skill\n" { t.Fatalf("restored shell content = %q, %v", got, err) } for name, want := range map[string]string{"skill-link": "SKILL.md", "dangling-link": "missing.md"} { info, err := os.Lstat(filepath.Join(source, name)) if err != nil || info.Mode()&os.ModeSymlink == 0 { t.Fatalf("shell %s must remain a symlink: %v, %v", name, info, err) } if got, err := os.Readlink(filepath.Join(source, name)); err != nil || got != want { t.Fatalf("shell %s target = %q, %v; want %q", name, got, err, want) } } } func TestInstallSkillsShellPreservesUnregisteredDingtalkSkill(t *testing.T) { scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-skills.sh")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatal("install-skills.sh final main invocation not found") } library := filepath.Join(t.TempDir(), "install-skills-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() root := filepath.Join(t.TempDir(), "project") base := filepath.Join(root, ".agents", "skills") multi := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(base, "dingtalk-custom", "SKILL.md"), []byte("market skill\n"), 0o644) mustWriteFile(t, filepath.Join(base, "dingtalk-aitable", "SKILL.md"), []byte("pre-state official\n"), 0o644) mustWriteFile(t, filepath.Join(base, "dingtalk-retired", "SKILL.md"), []byte("retired\n"), 0o644) if err := skillstate.Write(home, skillstate.State{ManagedSkills: []skillprovenance.Record{{Name: "dingtalk-retired"}}}); err != nil { t.Fatal(err) } mustWriteFile(t, filepath.Join(multi, "dingtalk-test", "SKILL.md"), []byte("new multi\n"), 0o644) harness := `. "$DWS_TEST_LIBRARY" install_multi_skills_to_root "$DWS_TEST_MULTI" "$DWS_TEST_ROOT" ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_MULTI="+multi, "DWS_TEST_ROOT="+root, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("install-skills harness failed: %v\n%s", err, output) } if got, err := os.ReadFile(filepath.Join(base, "dingtalk-custom", "SKILL.md")); err != nil || string(got) != "market skill\n" { t.Fatalf("unregistered market/user dingtalk-* Skill changed: data=%q err=%v", got, err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-retired")); !os.IsNotExist(err) { t.Fatalf("centrally managed retired DWS Skill must be removed: %v", err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-aitable")); !os.IsNotExist(err) { t.Fatalf("pre-state official DWS Skill must be removed: %v", err) } assertSkillProvenance(t, home, filepath.Join(base, "dingtalk-test"), "dingtalk-test", "install-skills.sh") } func TestInstallerShellUsesCanonicalRootWithoutCodexDuplicate(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { t.Run(scriptName, func(t *testing.T) { scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(home, ".codex", "config.toml"), []byte("model=test\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dws", "multi", "dingtalk-chat", "SKILL.md"), []byte("old nested\n"), 0o644) mustWriteFile(t, filepath.Join(source, "dingtalk-chat", "SKILL.md"), []byte("new chat\n"), 0o644) installCall := `install_multi_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_multi_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } cmd := exec.Command("sh", "-c", `. "$DWS_TEST_LIBRARY" `+installCall+` `) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s Codex-root harness failed: %v\n%s", scriptName, err, output) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dingtalk-chat", "SKILL.md")); err != nil { t.Fatalf("%s canonical Skill missing: %v", scriptName, err) } for _, duplicate := range []string{ filepath.Join(home, ".codex", "skills", "dingtalk-chat"), filepath.Join(home, ".agents", "skills", "dws", "multi", "dingtalk-chat", "SKILL.md"), } { if _, err := os.Stat(duplicate); !os.IsNotExist(err) { t.Fatalf("%s generic duplicate remains at %s: %v", scriptName, duplicate, err) } } }) } } func TestInstallerShellLinksZCodeRootToCanonical(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { t.Run(scriptName, func(t *testing.T) { scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(home, ".zcode", "v2", "config.json"), []byte("{}\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dws", "multi", "dingtalk-chat", "SKILL.md"), []byte("old nested\n"), 0o644) mustWriteFile(t, filepath.Join(source, "dingtalk-chat", "SKILL.md"), []byte("new chat\n"), 0o644) installCall := `install_multi_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_multi_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } cmd := exec.Command("sh", "-c", `. "$DWS_TEST_LIBRARY" `+installCall+` `) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s ZCode-root harness failed: %v\n%s", scriptName, err, output) } if _, err := os.Stat(filepath.Join(home, ".zcode", "skills", "dingtalk-chat", "SKILL.md")); err != nil { t.Fatalf("%s linked ZCode Skill missing: %v", scriptName, err) } if info, err := os.Lstat(filepath.Join(home, ".zcode", "skills", "dingtalk-chat")); err != nil || info.Mode()&os.ModeSymlink == 0 { t.Fatalf("%s ZCode target is not a canonical link: %#v, %v", scriptName, info, err) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dingtalk-chat", "SKILL.md")); err != nil { t.Fatalf("%s canonical Skill missing: %v", scriptName, err) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dws")); !os.IsNotExist(err) { t.Fatalf("%s generic duplicate remains: %v", scriptName, err) } }) } } func TestInstallerShellLinkPublicationRacePreservesConcurrentDirectories(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName t.Run(scriptName, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() base := filepath.Join(home, ".zcode", "skills") first := filepath.Join(base, "dingtalk-first") second := filepath.Join(base, "dingtalk-second") mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dingtalk-first", "SKILL.md"), []byte("first\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dingtalk-second", "SKILL.md"), []byte("second\n"), 0o644) bundle := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(bundle, "dingtalk-first", "SKILL.md"), []byte("first\n"), 0o644) mustWriteFile(t, filepath.Join(bundle, "dingtalk-second", "SKILL.md"), []byte("second\n"), 0o644) // Publication now moves a staged symlink instead of using `ln -P` // (BusyBox has no -P), and multi mode requires the bundle source so // that only bundle skills are linked. Inject the race on that move. harness := `. "$DWS_TEST_LIBRARY" mv() { if [ "$2" = "$DWS_TEST_SECOND" ]; then rm -f "$DWS_TEST_FIRST" mkdir -p "$DWS_TEST_FIRST" "$DWS_TEST_SECOND" printf '%s\n' first-user-data > "$DWS_TEST_FIRST/user.txt" printf '%s\n' second-user-data > "$DWS_TEST_SECOND/user.txt" fi command mv "$@" } if link_canonical_skills_to_base "$HOME" "$DWS_TEST_BASE" multi "$DWS_TEST_BUNDLE"; then exit 2 fi identity_dest="$DWS_TEST_BASE/identity-link" identity_anchor="$HOME/identity-anchor" identity_manifest="$HOME/identity.manifest" command ln -s ../same-target "$identity_anchor" command ln -P "$identity_anchor" "$identity_dest" identity_inode="$(skill_link_inode "$identity_anchor")" printf '%s\n%s\n%s\n' "$identity_dest" ../same-target "$identity_inode" > "$identity_manifest" rm -f "$identity_dest" command ln -s ../same-target "$identity_dest" if restore_linked_skill_set "$identity_manifest" /dev/null; then exit 3 fi rm -f "$identity_anchor" ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_BASE="+base, "DWS_TEST_BUNDLE="+bundle, "DWS_TEST_FIRST="+first, "DWS_TEST_SECOND="+second, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("%s race harness failed: %v\n%s", scriptName, err, output) } if !strings.Contains(string(output), "跳过回滚已被并发修改的 Skill 路径: "+first) { t.Fatalf("%s did not report identity-protected rollback:\n%s", scriptName, output) } identityDest := filepath.Join(base, "identity-link") if target, err := os.Readlink(identityDest); err != nil || target != "../same-target" { t.Fatalf("%s replacement link was removed: target=%q, err=%v", scriptName, target, err) } for path, want := range map[string]string{ filepath.Join(first, "user.txt"): "first-user-data\n", filepath.Join(second, "user.txt"): "second-user-data\n", } { got, err := os.ReadFile(path) if err != nil || string(got) != want { t.Fatalf("%s concurrent data at %s = %q, err=%v", scriptName, path, got, err) } } for _, dir := range []string{first, second} { entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("%s read concurrent directory %s: %v", scriptName, dir, err) } if len(entries) != 1 || entries[0].Name() != "user.txt" { t.Fatalf("%s transaction artifacts remain in %s: %v", scriptName, dir, entries) } } if matches, err := filepath.Glob(filepath.Join(base, ".dws-link-set.*")); err != nil || len(matches) != 0 { t.Fatalf("%s staging leftovers = %v, err=%v", scriptName, matches, err) } }) } } func TestInstallerShellUsesUpstreamXDGAndCustomAgentRoots(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { t.Run(scriptName, func(t *testing.T) { scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() tmp := t.TempDir() xdg := filepath.Join(tmp, "xdg config") autohand := filepath.Join(tmp, "autohand home") codex := filepath.Join(tmp, "codex home") source := filepath.Join(tmp, "multi") mustWriteFile(t, filepath.Join(source, "dingtalk-chat", "SKILL.md"), []byte("new chat\n"), 0o644) mustWriteFile(t, filepath.Join(xdg, "goose", "config.yaml"), []byte("enabled: true\n"), 0o644) mustWriteFile(t, filepath.Join(xdg, "agents", "skills", "dingtalk-chat", "SKILL.md"), []byte("old amp copy\n"), 0o644) mustWriteFile(t, filepath.Join(autohand, "config.json"), []byte("{}\n"), 0o644) mustWriteFile(t, filepath.Join(codex, "config.toml"), []byte("model=test\n"), 0o644) mustWriteFile(t, filepath.Join(codex, "skills", "dingtalk-chat", "SKILL.md"), []byte("old codex copy\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".qoderwork", "config.json"), []byte("{}\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".amp", "skills", "dingtalk-chat", "SKILL.md"), []byte("old DWS path\n"), 0o644) installCall := `install_multi_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_multi_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } cmd := exec.Command("sh", "-c", `. "$DWS_TEST_LIBRARY" `+installCall+` `) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, "XDG_CONFIG_HOME="+xdg, "AUTOHAND_HOME="+autohand, "CODEX_HOME="+codex, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s upstream-root harness failed: %v\n%s", scriptName, err, output) } for _, linked := range []string{ filepath.Join(xdg, "goose", "skills", "dingtalk-chat"), filepath.Join(autohand, "skills", "dingtalk-chat"), filepath.Join(home, ".qoderwork", "skills", "dingtalk-chat"), } { info, err := os.Lstat(linked) if err != nil || info.Mode()&os.ModeSymlink == 0 { t.Fatalf("%s is not a canonical symlink: %#v, %v", linked, info, err) } target, err := os.Readlink(linked) if err != nil || filepath.IsAbs(target) { t.Fatalf("%s link must be relative: target=%q err=%v", linked, target, err) } } for _, retired := range []string{ filepath.Join(xdg, "agents", "skills", "dingtalk-chat"), filepath.Join(codex, "skills", "dingtalk-chat"), filepath.Join(home, ".amp", "skills", "dingtalk-chat"), } { if _, err := os.Lstat(retired); !os.IsNotExist(err) { t.Fatalf("%s universal/legacy duplicate remains: %v", retired, err) } } }) } } func TestInstallerShellPinsCompleteUpstreamAgentRegistry(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { t.Run(scriptName, func(t *testing.T) { scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) cmd := exec.Command("sh", "-c", `. "$DWS_TEST_LIBRARY" upstream_agent_registry `) cmd.Env = append(os.Environ(), "DWS_TEST_LIBRARY="+library) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("%s registry failed: %v\n%s", scriptName, err, output) } lines := strings.Fields(strings.TrimSpace(string(output))) if len(lines) != 76 { t.Fatalf("%s registry has %d Agents, want 76", scriptName, len(lines)) } ids := map[string]bool{} roots := map[string]bool{} universal, nonUniversal, noGlobal, canonicalDirect := 0, 0, 0, 0 for _, line := range lines { parts := strings.Split(line, "|") if len(parts) != 3 || ids[parts[0]] { t.Fatalf("%s invalid or duplicate registry record %q", scriptName, line) } ids[parts[0]] = true switch parts[1] { case "U": universal++ case "N": nonUniversal++ default: t.Fatalf("%s invalid classification in %q", scriptName, line) } switch parts[2] { case "-": noGlobal++ case ".agents/skills": canonicalDirect++ default: roots[parts[2]] = true } } if universal != 19 || nonUniversal != 57 || noGlobal != 2 || canonicalDirect != 6 || len(roots) != 65 { t.Fatalf("%s registry classification U=%d N=%d no-global=%d canonical=%d roots=%d", scriptName, universal, nonUniversal, noGlobal, canonicalDirect, len(roots)) } }) } } func TestInstallPowerShellUsesCanonicalRootWithoutCodexDuplicate(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` if (!(Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_MULTI -Root $env:DWS_TEST_HOME)) { exit 2 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-codex-root.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) home := t.TempDir() multi := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(home, ".codex", "config.toml"), []byte("model=test\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dws", "multi", "dingtalk-chat", "SKILL.md"), []byte("old nested\n"), 0o644) mustWriteFile(t, filepath.Join(multi, "dingtalk-chat", "SKILL.md"), []byte("new chat\n"), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_MULTI="+multi) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell Codex-root harness failed: %v\n%s", err, output) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dingtalk-chat", "SKILL.md")); err != nil { t.Fatalf("PowerShell canonical Skill missing: %v", err) } if _, err := os.Lstat(filepath.Join(home, ".codex", "skills", "dingtalk-chat")); !os.IsNotExist(err) { t.Fatalf("PowerShell Codex duplicate remains: %v", err) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dws")); !os.IsNotExist(err) { t.Fatalf("PowerShell generic duplicate remains: %v", err) } } func TestInstallPowerShellLinksZCodeRootToCanonical(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` if (!(Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_MULTI -Root $env:DWS_TEST_HOME)) { exit 2 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-zcode-root.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) home := t.TempDir() multi := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(home, ".zcode", "v2", "config.json"), []byte("{}\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".agents", "skills", "dws", "multi", "dingtalk-chat", "SKILL.md"), []byte("old nested\n"), 0o644) mustWriteFile(t, filepath.Join(multi, "dingtalk-chat", "SKILL.md"), []byte("new chat\n"), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_MULTI="+multi) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell ZCode-root harness failed: %v\n%s", err, output) } if _, err := os.Stat(filepath.Join(home, ".zcode", "skills", "dingtalk-chat", "SKILL.md")); err != nil { t.Fatalf("PowerShell linked ZCode Skill missing: %v", err) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dingtalk-chat", "SKILL.md")); err != nil { t.Fatalf("PowerShell canonical Skill missing: %v", err) } if _, err := os.Stat(filepath.Join(home, ".agents", "skills", "dws")); !os.IsNotExist(err) { t.Fatalf("PowerShell generic duplicate remains: %v", err) } } func TestInstallPowerShellBackupFailureWritesNoMultiSkills(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` $ok = Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_MULTI -Root $env:DWS_TEST_HOME if ($ok) { exit 2 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) home := t.TempDir() base := filepath.Join(home, ".agents", "skills") multi := filepath.Join(t.TempDir(), "multi") mustWriteFile(t, filepath.Join(base, "dws", "SKILL.md"), []byte("old mono\n"), 0o644) mustWriteFile(t, filepath.Join(multi, "dingtalk-test", "SKILL.md"), []byte("new multi\n"), 0o644) mustWriteFile(t, filepath.Join(home, ".dws", "skill-backups"), []byte("not a directory\n"), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_MULTI="+multi, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell harness failed: %v\n%s", err, output) } if data, err := os.ReadFile(filepath.Join(base, "dws", "SKILL.md")); err != nil || string(data) != "old mono\n" { t.Fatalf("mono changed after PowerShell backup failure (data=%q, err=%v)", string(data), err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-test")); !os.IsNotExist(err) { t.Fatalf("PowerShell installed multi after backup failure, stat err=%v", err) } } func TestInstallPowerShellMultiMonoSwitchEndToEnd(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` if (!(Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_MULTI -Root $env:DWS_TEST_HOME)) { exit 2 } if (!(Install-SkillsToHomes -SkillSrc $env:DWS_TEST_MONO -Root $env:DWS_TEST_HOME)) { exit 3 } if (!(Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_MULTI -Root $env:DWS_TEST_HOME)) { exit 4 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-switch-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) home := t.TempDir() base := filepath.Join(home, ".agents", "skills") multi := filepath.Join(t.TempDir(), "multi") mono := filepath.Join(t.TempDir(), "mono") mustWriteFile(t, filepath.Join(multi, "dingtalk-test", "SKILL.md"), []byte("new multi\n"), 0o644) mustWriteFile(t, filepath.Join(multi, "dingtalk-shared", "SKILL.md"), []byte("shared\n"), 0o644) mustWriteFile(t, filepath.Join(mono, "SKILL.md"), []byte("new mono\n"), 0o644) mustWriteFile(t, filepath.Join(base, "user-owned", "SKILL.md"), []byte("keep\n"), 0o644) mustWriteFile(t, filepath.Join(base, "dingtalk-custom", "SKILL.md"), []byte("market skill\n"), 0o644) mustWriteFile(t, filepath.Join(base, "dingtalk-aitable", "SKILL.md"), []byte("pre-state official\n"), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_MULTI="+multi, "DWS_TEST_MONO="+mono, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("PowerShell multi -> mono -> multi harness failed: %v\n%s", err, output) } for _, name := range []string{"dingtalk-test", "dingtalk-shared"} { if _, err := os.Stat(filepath.Join(base, name, "SKILL.md")); err != nil { t.Fatalf("PowerShell final multi layout missing %s: %v\n%s", name, err, output) } } if _, err := os.Stat(filepath.Join(base, "dws")); !os.IsNotExist(err) { t.Fatalf("PowerShell final multi layout retained mono dws/: %v\n%s", err, output) } if got, err := os.ReadFile(filepath.Join(base, "user-owned", "SKILL.md")); err != nil || string(got) != "keep\n" { t.Fatalf("PowerShell switch changed non-DWS Skill: data=%q err=%v", got, err) } if got, err := os.ReadFile(filepath.Join(base, "dingtalk-custom", "SKILL.md")); err != nil || string(got) != "market skill\n" { t.Fatalf("PowerShell switch changed unregistered market/user dingtalk-* Skill: data=%q err=%v", got, err) } if _, err := os.Stat(filepath.Join(base, "dingtalk-aitable")); !os.IsNotExist(err) { t.Fatalf("PowerShell switch retained pre-state official Skill: %v", err) } assertSkillProvenance(t, home, filepath.Join(base, "dingtalk-test"), "dingtalk-test", "install.ps1") if matches, err := filepath.Glob(filepath.Join(home, ".dws", "skill-backups", "*", "*")); err != nil || len(matches) == 0 { t.Fatalf("PowerShell switch created no recoverable backups: matches=%v err=%v\n%s", matches, err, output) } } func TestInstallerShellCacheCopyFailurePreservesOldCache(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName t.Run(scriptName, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "multi") cache := filepath.Join(home, ".dws", "skills", "multi") mustWriteFile(t, filepath.Join(source, "dingtalk-new", "SKILL.md"), []byte("new cache\n"), 0o644) mustWriteFile(t, filepath.Join(cache, "dingtalk-old", "SKILL.md"), []byte("old cache\n"), 0o644) harness := `. "$DWS_TEST_LIBRARY" cp() { return 1; } if publish_skill_cache "$DWS_TEST_SOURCE" "$DWS_TEST_CACHE"; then exit 2 fi ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, "DWS_TEST_CACHE="+cache, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s cache harness failed: %v\n%s", scriptName, err, output) } if got, err := os.ReadFile(filepath.Join(cache, "dingtalk-old", "SKILL.md")); err != nil || string(got) != "old cache\n" { t.Fatalf("%s old cache = %q, err = %v", scriptName, got, err) } if _, err := os.Stat(filepath.Join(cache, "dingtalk-new")); !os.IsNotExist(err) { t.Fatalf("%s published new cache after copy failure: %v", scriptName, err) } if matches, err := filepath.Glob(filepath.Join(filepath.Dir(cache), ".multi.tmp.*")); err != nil || len(matches) != 0 { t.Fatalf("%s staging leftovers = %v, err = %v", scriptName, matches, err) } }) } } func TestInstallerShellManagedSkillNamesAreLiteral(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName t.Run(scriptName, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() statePath := filepath.Join(home, ".dws", "skills-state.json") mustWriteFile(t, statePath, []byte(`{ "managed_skills": [ {"name":"dingtalk-user[1]"}, {"name": "dingtalk-user+2"} ] } `), 0o600) base := filepath.Join(home, ".agents", "skills") unregistered := filepath.Join(base, "dingtalk-.*") compact := filepath.Join(base, "dingtalk-user[1]") spaced := filepath.Join(base, "dingtalk-user+2") for _, dir := range []string{unregistered, compact, spaced} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } } harness := `. "$DWS_TEST_LIBRARY" if is_managed_multi_skill_dir "$DWS_TEST_UNREGISTERED"; then exit 2 fi is_managed_multi_skill_dir "$DWS_TEST_COMPACT" || exit 3 is_managed_multi_skill_dir "$DWS_TEST_SPACED" || exit 4 printf '{"other":[{"name":"dingtalk-user[1]"}]}\n' > "$DWS_TEST_STATE" if is_managed_multi_skill_dir "$DWS_TEST_COMPACT"; then exit 5 fi ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_STATE="+statePath, "DWS_TEST_UNREGISTERED="+unregistered, "DWS_TEST_COMPACT="+compact, "DWS_TEST_SPACED="+spaced, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s literal ownership harness failed: %v\n%s", scriptName, err, output) } }) } } func TestInstallerShellMultiCopyFailureReturnsFailure(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName t.Run(scriptName, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "multi") base := filepath.Join(home, ".agents", "skills") mustWriteFile(t, filepath.Join(source, "dingtalk-test", "SKILL.md"), []byte("new skill\n"), 0o644) installCall := `install_multi_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_multi_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } harness := `. "$DWS_TEST_LIBRARY" cp() { return 1; } if ` + installCall + `; then exit 2 fi ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("%s copy-failure harness failed: %v\n%s", scriptName, err, output) } if strings.Contains(string(output), "✅ Skills") { t.Fatalf("%s reported success after copy failure:\n%s", scriptName, output) } if !strings.Contains(string(output), "所有检测到的 Agent 目标均失败") { t.Fatalf("%s did not report aggregate install failure:\n%s", scriptName, output) } if _, err := os.Stat(filepath.Join(base, "dingtalk-test", "SKILL.md")); !os.IsNotExist(err) { t.Fatalf("%s left a completed Skill after copy failure: %v", scriptName, err) } }) } } func TestInstallerShellMultiTransactionFailuresRestoreOldSet(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName for _, failureKind := range []string{"backup", "publish"} { failureKind := failureKind t.Run(scriptName+"/"+failureKind, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "multi") base := filepath.Join(home, ".agents", "skills") first := filepath.Join(base, "dingtalk-first") second := filepath.Join(base, "dingtalk-second") mustWriteFile(t, filepath.Join(source, "dingtalk-first", "SKILL.md"), []byte("new first\n"), 0o644) mustWriteFile(t, filepath.Join(source, "dingtalk-second", "SKILL.md"), []byte("new second\n"), 0o644) mustWriteFile(t, filepath.Join(first, "SKILL.md"), []byte("old first\n"), 0o644) mustWriteFile(t, filepath.Join(second, "SKILL.md"), []byte("old second\n"), 0o644) installCall := `install_multi_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_multi_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } harness := `. "$DWS_TEST_LIBRARY" mv() { if [ "$DWS_TEST_FAILURE_KIND" = backup ] && [ "${1%/}" = "$DWS_TEST_SECOND" ]; then return 1 fi if [ "$DWS_TEST_FAILURE_KIND" = publish ]; then case "$1" in */.dws-multi-set.*/dingtalk-second) return 1 ;; esac fi command mv "$@" } if ` + installCall + `; then exit 2 fi ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, "DWS_TEST_SECOND="+second, "DWS_TEST_FAILURE_KIND="+failureKind, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s %s-failure harness failed: %v\n%s", scriptName, failureKind, err, output) } if got, err := os.ReadFile(filepath.Join(first, "SKILL.md")); err != nil || string(got) != "old first\n" { t.Fatalf("%s first Skill after %s failure = %q, err=%v", scriptName, failureKind, got, err) } if got, err := os.ReadFile(filepath.Join(second, "SKILL.md")); err != nil || string(got) != "old second\n" { t.Fatalf("%s second Skill after %s failure = %q, err=%v", scriptName, failureKind, got, err) } if matches, err := filepath.Glob(filepath.Join(base, ".dws-multi-set.*")); err != nil || len(matches) != 0 { t.Fatalf("%s staging leftovers after %s failure = %v, err=%v", scriptName, failureKind, matches, err) } }) } } } func TestInstallerShellMonoCopyFailureReturnsFailure(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName t.Run(scriptName, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "mono") base := filepath.Join(home, ".agents", "skills") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("new mono\n"), 0o644) installCall := `install_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } harness := `. "$DWS_TEST_LIBRARY" cp() { return 1; } if ` + installCall + `; then exit 2 fi ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("%s mono copy-failure harness failed: %v\n%s", scriptName, err, output) } if strings.Contains(string(output), "✅ Skills") { t.Fatalf("%s reported mono success after copy failure:\n%s", scriptName, output) } if !strings.Contains(string(output), "未安装任何 mono Skill") { t.Fatalf("%s did not report aggregate mono install failure:\n%s", scriptName, output) } if _, err := os.Stat(filepath.Join(base, "dws", "SKILL.md")); !os.IsNotExist(err) { t.Fatalf("%s left a completed mono Skill after copy failure: %v", scriptName, err) } }) } } func TestInstallerShellMonoTransactionFailuresRestoreOldSet(t *testing.T) { for _, scriptName := range []string{"install.sh", "install-skills.sh"} { scriptName := scriptName for _, failureKind := range []string{"backup", "publish"} { failureKind := failureKind t.Run(scriptName+"/"+failureKind, func(t *testing.T) { t.Parallel() scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", scriptName)) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "\nmain\n") if cut < 0 { t.Fatalf("%s final main invocation not found", scriptName) } library := filepath.Join(t.TempDir(), scriptName+"-lib.sh") mustWriteFile(t, library, data[:cut], 0o755) home := t.TempDir() source := filepath.Join(t.TempDir(), "mono") base := filepath.Join(home, ".agents", "skills") first := filepath.Join(base, "dingtalk-aitable") second := filepath.Join(base, "dingtalk-calendar") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("new mono\n"), 0o644) mustWriteFile(t, filepath.Join(first, "SKILL.md"), []byte("old first\n"), 0o644) mustWriteFile(t, filepath.Join(second, "SKILL.md"), []byte("old second\n"), 0o644) installCall := `install_skills_to_homes "$DWS_TEST_SOURCE"` if scriptName == "install-skills.sh" { installCall = `install_skills_to_root "$DWS_TEST_SOURCE" "$HOME"` } harness := `. "$DWS_TEST_LIBRARY" mv() { if [ "$DWS_TEST_FAILURE_KIND" = backup ] && [ "${1%/}" = "$DWS_TEST_SECOND" ]; then return 1 fi if [ "$DWS_TEST_FAILURE_KIND" = publish ]; then case "$1" in */.dws-mono-set.*/dws) return 1 ;; esac fi command mv "$@" } if ` + installCall + `; then exit 2 fi ` cmd := exec.Command("sh", "-c", harness) cmd.Env = append(os.Environ(), "HOME="+home, "DWS_TEST_LIBRARY="+library, "DWS_TEST_SOURCE="+source, "DWS_TEST_SECOND="+second, "DWS_TEST_FAILURE_KIND="+failureKind, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("%s mono %s-failure harness failed: %v\n%s", scriptName, failureKind, err, output) } if got, err := os.ReadFile(filepath.Join(first, "SKILL.md")); err != nil || string(got) != "old first\n" { t.Fatalf("%s first multi Skill after mono %s failure = %q, err=%v", scriptName, failureKind, got, err) } if got, err := os.ReadFile(filepath.Join(second, "SKILL.md")); err != nil || string(got) != "old second\n" { t.Fatalf("%s second multi Skill after mono %s failure = %q, err=%v", scriptName, failureKind, got, err) } if _, err := os.Stat(filepath.Join(base, "dws")); !os.IsNotExist(err) { t.Fatalf("%s exposed mono dws after %s failure: %v", scriptName, failureKind, err) } if matches, err := filepath.Glob(filepath.Join(base, ".dws-mono-set.*")); err != nil || len(matches) != 0 { t.Fatalf("%s mono staging leftovers after %s failure = %v, err=%v", scriptName, failureKind, matches, err) } }) } } } func TestInstallPowerShellMultiTransactionFailuresRestoreOldSet(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } for _, failureKind := range []string{"backup", "publish"} { failureKind := failureKind t.Run(failureKind, func(t *testing.T) { home := t.TempDir() base := filepath.Join(home, ".agents", "skills") source := filepath.Join(t.TempDir(), "multi") first := filepath.Join(base, "dingtalk-first") second := filepath.Join(base, "dingtalk-second") mustWriteFile(t, filepath.Join(source, "dingtalk-first", "SKILL.md"), []byte("new first\n"), 0o644) mustWriteFile(t, filepath.Join(source, "dingtalk-second", "SKILL.md"), []byte("new second\n"), 0o644) mustWriteFile(t, filepath.Join(first, "SKILL.md"), []byte("old first\n"), 0o644) mustWriteFile(t, filepath.Join(second, "SKILL.md"), []byte("old second\n"), 0o644) prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` function Move-SkillPath { param([string]$Source, [string]$Destination) if ($env:DWS_TEST_FAILURE_KIND -eq "backup" -and $Source -eq $env:DWS_TEST_SECOND) { throw "injected second backup failure" } if ($env:DWS_TEST_FAILURE_KIND -eq "publish" -and $Source -match "[\\/].dws-multi-set-[^\\/]+[\\/]dingtalk-second$") { throw "injected second publish failure" } Microsoft.PowerShell.Management\Move-Item -LiteralPath $Source -Destination $Destination -ErrorAction Stop } $ok = Install-MultiSkillsToHomes -MultiSrc $env:DWS_TEST_SOURCE -Root $env:DWS_TEST_HOME if ($ok) { exit 2 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-transaction-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source, "DWS_TEST_SECOND="+second, "DWS_TEST_FAILURE_KIND="+failureKind, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell %s-failure harness failed: %v\n%s", failureKind, err, output) } if got, err := os.ReadFile(filepath.Join(first, "SKILL.md")); err != nil || string(got) != "old first\n" { t.Fatalf("PowerShell first Skill after %s failure = %q, err=%v", failureKind, got, err) } if got, err := os.ReadFile(filepath.Join(second, "SKILL.md")); err != nil || string(got) != "old second\n" { t.Fatalf("PowerShell second Skill after %s failure = %q, err=%v", failureKind, got, err) } if matches, err := filepath.Glob(filepath.Join(base, ".dws-multi-set-*")); err != nil || len(matches) != 0 { t.Fatalf("PowerShell staging leftovers after %s failure = %v, err=%v", failureKind, matches, err) } }) } } func TestInstallPowerShellCrossDeviceMoveContract(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") for _, phase := range []string{"success", "copy", "verify", "remove", "permission"} { phase := phase t.Run(phase, func(t *testing.T) { root := t.TempDir() home := filepath.Join(root, "home") source := filepath.Join(root, "external", "skill") destination := filepath.Join(home, ".dws", "skill-backups", "stamp", "skill") restored := filepath.Join(root, "external", "restored-skill") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("old skill\n"), 0o640) linksCreated := false if phase == "success" { if err := os.Symlink("SKILL.md", filepath.Join(source, "skill-link")); err == nil { if err := os.Symlink("missing.md", filepath.Join(source, "dangling-link")); err != nil { t.Fatal(err) } linksCreated = true } else if runtime.GOOS != "windows" { t.Fatal(err) } } harness := prefix + ` $script:OriginalCopySkillPathLexically = ${function:Copy-SkillPathLexically} $script:OriginalAssertSkillPathCopy = ${function:Assert-SkillPathCopy} $script:OriginalRemoveSkillPathLexically = ${function:Remove-SkillPathLexically} function New-CrossDeviceError { return [System.IO.IOException]::new("injected cross-device rename", -2147024879) } function Move-SkillPath { param([string]$Source, [string]$Destination) if ($env:DWS_TEST_PHASE -eq "permission" -and $Source -eq $env:DWS_TEST_SOURCE) { throw [System.UnauthorizedAccessException]::new("permission denied") } if (($Source -eq $env:DWS_TEST_SOURCE -and $Destination -eq $env:DWS_TEST_DESTINATION) -or ($Source -eq $env:DWS_TEST_DESTINATION -and $Destination -eq $env:DWS_TEST_RESTORED)) { throw (New-CrossDeviceError) } Microsoft.PowerShell.Management\Move-Item -LiteralPath $Source -Destination $Destination -ErrorAction Stop } function Copy-SkillPathLexically { param([string]$Source, [string]$Destination) if ($env:DWS_TEST_PHASE -eq "copy") { throw "copy failed" } if ($env:DWS_TEST_PHASE -eq "permission") { [System.IO.File]::WriteAllText($env:DWS_TEST_COPY_MARKER, "called") } & $script:OriginalCopySkillPathLexically -Source $Source -Destination $Destination } function Assert-SkillPathCopy { param([string]$Source, [string]$Destination) if ($env:DWS_TEST_PHASE -eq "verify") { throw "verify failed" } & $script:OriginalAssertSkillPathCopy -Source $Source -Destination $Destination } function Remove-SkillPathLexically { param([string]$Path) if ($env:DWS_TEST_PHASE -eq "remove" -and $Path -eq $env:DWS_TEST_SOURCE) { throw "remove failed" } & $script:OriginalRemoveSkillPathLexically -Path $Path } try { Move-SkillPathRecoverably -Source $env:DWS_TEST_SOURCE -Destination $env:DWS_TEST_DESTINATION if ($env:DWS_TEST_PHASE -ne "success") { exit 2 } Move-SkillPathRecoverably -Source $env:DWS_TEST_DESTINATION -Destination $env:DWS_TEST_RESTORED exit 0 } catch { if ($env:DWS_TEST_PHASE -eq "success") { Write-Error $_; exit 3 } if ($env:DWS_TEST_PHASE -eq "copy" -and $_ -notmatch "copy failed") { Write-Error $_; exit 4 } if ($env:DWS_TEST_PHASE -eq "verify" -and $_ -notmatch "verify failed") { Write-Error $_; exit 5 } if ($env:DWS_TEST_PHASE -eq "remove" -and $_ -notmatch "均保留") { Write-Error $_; exit 6 } if ($env:DWS_TEST_PHASE -eq "permission" -and $_ -notmatch "permission denied") { Write-Error $_; exit 7 } exit 0 } ` harnessPath := filepath.Join(t.TempDir(), "install-cross-device-harness.ps1") mustWriteFile(t, harnessPath, []byte(harness), 0o644) copyMarker := filepath.Join(root, "copy-called") cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source, "DWS_TEST_DESTINATION="+destination, "DWS_TEST_RESTORED="+restored, "DWS_TEST_COPY_MARKER="+copyMarker, "DWS_TEST_PHASE="+phase, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell %s EXDEV harness failed: %v\n%s", phase, err, output) } if phase == "success" { if got, err := os.ReadFile(filepath.Join(restored, "SKILL.md")); err != nil || string(got) != "old skill\n" { t.Fatalf("restored content = %q, %v", got, err) } if _, err := os.Lstat(source); !os.IsNotExist(err) { t.Fatalf("source must be removed: %v", err) } if _, err := os.Lstat(destination); !os.IsNotExist(err) { t.Fatalf("backup must be consumed by rollback: %v", err) } if linksCreated { for name, want := range map[string]string{"skill-link": "SKILL.md", "dangling-link": "missing.md"} { info, err := os.Lstat(filepath.Join(restored, name)) if err != nil || info.Mode()&os.ModeSymlink == 0 { t.Fatalf("%s must remain a symlink: %v, %v", name, info, err) } if got, err := os.Readlink(filepath.Join(restored, name)); err != nil || got != want { t.Fatalf("%s target = %q, %v; want %q", name, got, err, want) } } } return } if got, err := os.ReadFile(filepath.Join(source, "SKILL.md")); err != nil || string(got) != "old skill\n" { t.Fatalf("source after %s failure = %q, %v", phase, got, err) } if phase == "remove" { if got, err := os.ReadFile(filepath.Join(destination, "SKILL.md")); err != nil || string(got) != "old skill\n" { t.Fatalf("published backup after remove failure = %q, %v", got, err) } } else if _, err := os.Lstat(destination); !os.IsNotExist(err) { t.Fatalf("destination after %s failure = %v", phase, err) } if phase == "permission" { if _, err := os.Stat(copyMarker); !os.IsNotExist(err) { t.Fatalf("permission error must not trigger copy fallback: %v", err) } } if matches, err := filepath.Glob(filepath.Join(filepath.Dir(destination), ".skill.cross-device-*")); err != nil || len(matches) != 0 { t.Fatalf("PowerShell %s staging leftovers = %v, %v", phase, matches, err) } }) } } func TestInstallDevappPowerShellCrossDeviceBackup(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-devapp.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.Index(string(data), "# Read the releases list") if cut < 0 { t.Fatal("install-devapp.ps1 release section not found") } root := t.TempDir() home := filepath.Join(root, "home") source := filepath.Join(root, "external", "dingtalk-misc") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("old dev skill\n"), 0o640) linksCreated := false if err := os.Symlink("SKILL.md", filepath.Join(source, "skill-link")); err == nil { if err := os.Symlink("missing.md", filepath.Join(source, "dangling-link")); err != nil { t.Fatal(err) } linksCreated = true } else if runtime.GOOS != "windows" { t.Fatal(err) } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` function Move-DevSkillPath([string]$Source, [string]$Destination) { if ($Source -eq $env:DWS_TEST_SOURCE) { throw [System.IO.IOException]::new("injected cross-device rename", -2147024879) } Microsoft.PowerShell.Management\Move-Item -LiteralPath $Source -Destination $Destination -ErrorAction Stop } Backup-DevSkill $env:DWS_TEST_SOURCE if (Test-PathLexically $env:DWS_TEST_SOURCE) { exit 2 } ` harnessPath := filepath.Join(t.TempDir(), "install-devapp-cross-device-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell devapp EXDEV backup failed: %v\n%s", err, output) } backup := findSkillBackup(home, "dingtalk-misc", "old dev skill\n") if backup == "" { t.Fatal("PowerShell devapp backup missing") } if linksCreated { for name, want := range map[string]string{"skill-link": "SKILL.md", "dangling-link": "missing.md"} { info, err := os.Lstat(filepath.Join(backup, name)) if err != nil || info.Mode()&os.ModeSymlink == 0 { t.Fatalf("devapp %s must remain a symlink: %v, %v", name, info, err) } if got, err := os.Readlink(filepath.Join(backup, name)); err != nil || got != want { t.Fatalf("devapp %s target = %q, %v; want %q", name, got, err, want) } } } } func TestInstallDevappPowerShellPublishFailureRestoresAcrossDevice(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-devapp.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.Index(string(data), "# Read the releases list") if cut < 0 { t.Fatal("install-devapp.ps1 release section not found") } root := t.TempDir() home := filepath.Join(root, "home") source := filepath.Join(root, "bundle", "dingtalk-misc") destination := filepath.Join(root, "external", "dingtalk-misc") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("new dev skill\n"), 0o640) mustWriteFile(t, filepath.Join(destination, "SKILL.md"), []byte("old dev skill\n"), 0o644) prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` function Move-DevSkillPath([string]$Source, [string]$Destination) { if ($Source -match "[\\/].dws-dev-copy-[^\\/]+[\\/]payload$") { throw "injected publish failure" } if ($Source -match "[\\/]skill-backups[\\/]" -and $Destination -eq $env:DWS_TEST_DESTINATION) { throw [System.IO.IOException]::new("injected cross-device rollback", -2147024879) } Microsoft.PowerShell.Management\Move-Item -LiteralPath $Source -Destination $Destination -ErrorAction Stop } try { Publish-DevSkillCopy $env:DWS_TEST_SOURCE $env:DWS_TEST_DESTINATION exit 2 } catch { if ($_ -notmatch "injected publish failure") { Write-Error $_; exit 3 } } ` harnessPath := filepath.Join(t.TempDir(), "install-devapp-publish-rollback-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source, "DWS_TEST_DESTINATION="+destination, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell devapp publish rollback failed: %v\n%s", err, output) } if got, err := os.ReadFile(filepath.Join(destination, "SKILL.md")); err != nil || string(got) != "old dev skill\n" { t.Fatalf("restored devapp content = %q, %v", got, err) } if matches, err := filepath.Glob(filepath.Join(filepath.Dir(destination), ".dws-dev-copy-*")); err != nil || len(matches) != 0 { t.Fatalf("devapp publish staging leftovers = %v, %v", matches, err) } } func TestInstallDevappPowerShellPublicationRacePreservesConcurrentDirectory(t *testing.T) { powerShellNames := []string{"pwsh"} if runtime.GOOS == "windows" { powerShellNames = []string{"powershell", "pwsh"} } pwsh := "" for _, name := range powerShellNames { if candidate, err := exec.LookPath(name); err == nil { pwsh = candidate break } } if pwsh == "" { t.Skip("PowerShell is not available") } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install-devapp.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.Index(string(data), "# Read the releases list") if cut < 0 { t.Fatal("install-devapp.ps1 release section not found") } root := t.TempDir() home := filepath.Join(root, "home") source := filepath.Join(root, "bundle", "dingtalk-misc") destination := filepath.Join(root, "external", "dingtalk-misc") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("new dev skill\n"), 0o640) mustWriteFile(t, filepath.Join(destination, "SKILL.md"), []byte("old dev skill\n"), 0o644) prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") harness := prefix + ` $script:OriginalBackupDevSkill = ${function:Backup-DevSkill} function Backup-DevSkill([string]$path, [ref]$BackupPath) { & $script:OriginalBackupDevSkill $path $BackupPath # Simulate another installer recreating the destination after the old # content was backed up but before this transaction publishes its stage. New-Item -ItemType Directory -Path $path -Force -ErrorAction Stop | Out-Null Set-Content -LiteralPath (Join-Path $path 'concurrent-user-data.txt') -Value 'keep' } try { Publish-DevSkillCopy $env:DWS_TEST_SOURCE $env:DWS_TEST_DESTINATION exit 2 } catch { if (!$_.Exception.Data['DWSRollbackFailed']) { Write-Error $_; exit 3 } } if (!(Test-Path -LiteralPath (Join-Path $env:DWS_TEST_DESTINATION 'concurrent-user-data.txt') -PathType Leaf)) { exit 4 } if (Test-Path -LiteralPath (Join-Path $env:DWS_TEST_DESTINATION 'payload')) { exit 5 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-devapp-publication-race.ps1") mustWriteFile(t, harnessPath, []byte(harness), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source, "DWS_TEST_DESTINATION="+destination, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell devapp publication race must retain concurrent directory: %v\n%s", err, output) } if got, err := os.ReadFile(filepath.Join(destination, "concurrent-user-data.txt")); err != nil || strings.TrimSpace(string(got)) != "keep" { t.Fatalf("concurrent devapp data = %q, %v", got, err) } if backup := findSkillBackup(home, "dingtalk-misc", "old dev skill\n"); backup == "" { t.Fatal("original devapp backup must be retained after publication race") } } func TestInstallPowerShellMonoTransactionFailuresRestoreOldSet(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } for _, failureKind := range []string{"backup", "publish"} { failureKind := failureKind t.Run(failureKind, func(t *testing.T) { home := t.TempDir() base := filepath.Join(home, ".agents", "skills") source := filepath.Join(t.TempDir(), "mono") first := filepath.Join(base, "dingtalk-aitable") second := filepath.Join(base, "dingtalk-calendar") mustWriteFile(t, filepath.Join(source, "SKILL.md"), []byte("new mono\n"), 0o644) mustWriteFile(t, filepath.Join(first, "SKILL.md"), []byte("old first\n"), 0o644) mustWriteFile(t, filepath.Join(second, "SKILL.md"), []byte("old second\n"), 0o644) prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` function Move-SkillPath { param([string]$Source, [string]$Destination) if ($env:DWS_TEST_FAILURE_KIND -eq "backup" -and $Source -eq $env:DWS_TEST_SECOND) { throw "injected second backup failure" } if ($env:DWS_TEST_FAILURE_KIND -eq "publish" -and $Source -match "[\\/].dws-mono-set-[^\\/]+[\\/]dws$") { throw "injected mono publish failure" } Microsoft.PowerShell.Management\Move-Item -LiteralPath $Source -Destination $Destination -ErrorAction Stop } $ok = Install-SkillsToHomes -SkillSrc $env:DWS_TEST_SOURCE -Root $env:DWS_TEST_HOME if ($ok) { exit 2 } exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-mono-transaction-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source, "DWS_TEST_SECOND="+second, "DWS_TEST_FAILURE_KIND="+failureKind, ) if output, err := cmd.CombinedOutput(); err != nil { t.Fatalf("PowerShell mono %s-failure harness failed: %v\n%s", failureKind, err, output) } if got, err := os.ReadFile(filepath.Join(first, "SKILL.md")); err != nil || string(got) != "old first\n" { t.Fatalf("PowerShell first multi Skill after mono %s failure = %q, err=%v", failureKind, got, err) } if got, err := os.ReadFile(filepath.Join(second, "SKILL.md")); err != nil || string(got) != "old second\n" { t.Fatalf("PowerShell second multi Skill after mono %s failure = %q, err=%v", failureKind, got, err) } if _, err := os.Stat(filepath.Join(base, "dws")); !os.IsNotExist(err) { t.Fatalf("PowerShell exposed mono dws after %s failure: %v", failureKind, err) } if matches, err := filepath.Glob(filepath.Join(base, ".dws-mono-set-*")); err != nil || len(matches) != 0 { t.Fatalf("PowerShell mono staging leftovers after %s failure = %v, err=%v", failureKind, matches, err) } }) } } func TestInstallPowerShellCacheCopyFailurePreservesOldCache(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { if runtime.GOOS == "windows" { pwsh, err = exec.LookPath("powershell") } if err != nil { t.Skip("PowerShell is not available") } } scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "install.ps1")) if err != nil { t.Fatal(err) } data, err := os.ReadFile(scriptPath) if err != nil { t.Fatal(err) } cut := strings.LastIndex(string(data), "# ── Main") if cut < 0 { t.Fatal("install.ps1 main section not found") } prefix := strings.ReplaceAll(string(data[:cut]), "$HOME", "$env:DWS_TEST_HOME") prefix += ` function Copy-DirRecursive { throw "injected cache copy failure" } Cache-MultiSkills -Source $env:DWS_TEST_SOURCE exit 0 ` harnessPath := filepath.Join(t.TempDir(), "install-cache-harness.ps1") mustWriteFile(t, harnessPath, []byte(prefix), 0o644) home := t.TempDir() source := filepath.Join(t.TempDir(), "multi") cache := filepath.Join(home, ".dws", "skills", "multi") mustWriteFile(t, filepath.Join(source, "dingtalk-new", "SKILL.md"), []byte("new cache\n"), 0o644) mustWriteFile(t, filepath.Join(cache, "dingtalk-old", "SKILL.md"), []byte("old cache\n"), 0o644) cmd := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", harnessPath) cmd.Env = append(os.Environ(), "DWS_TEST_HOME="+home, "DWS_TEST_SOURCE="+source, ) output, err := cmd.CombinedOutput() if err != nil { t.Fatalf("PowerShell cache harness failed: %v\n%s", err, output) } if !strings.Contains(string(output), "未覆盖原缓存") { t.Fatalf("PowerShell cache failure warning missing:\n%s", output) } if got, err := os.ReadFile(filepath.Join(cache, "dingtalk-old", "SKILL.md")); err != nil || string(got) != "old cache\n" { t.Fatalf("PowerShell old cache = %q, err = %v", got, err) } if _, err := os.Stat(filepath.Join(cache, "dingtalk-new")); !os.IsNotExist(err) { t.Fatalf("PowerShell published new cache after copy failure: %v", err) } if matches, err := filepath.Glob(filepath.Join(filepath.Dir(cache), ".multi.tmp-*")); err != nil || len(matches) != 0 { t.Fatalf("PowerShell staging leftovers = %v, err = %v", matches, err) } } func writeTarGz(t *testing.T, path string, files map[string]string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(path), err) } f, err := os.Create(path) if err != nil { t.Fatalf("Create(%s) error = %v", path, err) } gz := gzip.NewWriter(f) tw := tar.NewWriter(gz) for name, content := range files { hdr := &tar.Header{ Name: name, Mode: 0o755, Size: int64(len(content)), } if err := tw.WriteHeader(hdr); err != nil { t.Fatalf("WriteHeader(%s) error = %v", name, err) } if _, err := tw.Write([]byte(content)); err != nil { t.Fatalf("Write(%s) error = %v", name, err) } } if err := tw.Close(); err != nil { t.Fatalf("tar Close error = %v", err) } if err := gz.Close(); err != nil { t.Fatalf("gzip Close error = %v", err) } if err := f.Close(); err != nil { t.Fatalf("Close(%s) error = %v", path, err) } } func writeZip(t *testing.T, path string, files map[string]string) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(path), err) } f, err := os.Create(path) if err != nil { t.Fatalf("Create(%s) error = %v", path, err) } zw := zip.NewWriter(f) for name, content := range files { w, err := zw.Create(name) if err != nil { t.Fatalf("zip Create(%s) error = %v", name, err) } if _, err := w.Write([]byte(content)); err != nil { t.Fatalf("zip Write(%s) error = %v", name, err) } } if err := zw.Close(); err != nil { t.Fatalf("zip Close error = %v", err) } if err := f.Close(); err != nil { t.Fatalf("Close(%s) error = %v", path, err) } } func writeFakeCurl(t *testing.T, path string) { t.Helper() const script = `#!/bin/sh set -eu out="" url="" while [ "$#" -gt 0 ]; do case "$1" in -o) out="$2"; shift 2 ;; -*) shift ;; *) url="$1"; shift ;; esac done [ -n "$out" ] || { echo "fake curl: missing -o" >&2; exit 1; } case "$url" in *"/${FAKE_ASSET_NAME}") cp "$FAKE_RELEASE_DIR/$FAKE_ASSET_NAME" "$out" ;; *"/dws-skills.zip") cp "$FAKE_RELEASE_DIR/dws-skills.zip" "$out" ;; *"/checksums.txt") cp "$FAKE_RELEASE_DIR/checksums.txt" "$out" ;; *) echo "fake curl: unexpected URL $url" >&2; exit 1 ;; esac ` mustWriteFile(t, path, []byte(script), 0o755) } func writeInstallerFixtureChecksums(t *testing.T, releaseDir string) { t.Helper() entries, err := os.ReadDir(releaseDir) if err != nil { t.Fatalf("ReadDir(%s) error = %v", releaseDir, err) } var lines []string for _, entry := range entries { if entry.IsDir() || entry.Name() == "checksums.txt" { continue } data, err := os.ReadFile(filepath.Join(releaseDir, entry.Name())) if err != nil { t.Fatalf("ReadFile(%s) error = %v", entry.Name(), err) } digest := sha256.Sum256(data) lines = append(lines, hex.EncodeToString(digest[:])+" "+entry.Name()) } mustWriteFile(t, filepath.Join(releaseDir, "checksums.txt"), []byte(strings.Join(lines, "\n")+"\n"), 0o644) } func writeFakeGH(t *testing.T, path, version string) { t.Helper() script := "#!/bin/sh\nprintf '%s\\n' '" + version + "'\n" mustWriteFile(t, path, []byte(script), 0o755) } func mustWriteFile(t *testing.T, path string, data []byte, mode os.FileMode) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatalf("MkdirAll(%s) error = %v", filepath.Dir(path), err) } if err := os.WriteFile(path, data, mode); err != nil { t.Fatalf("WriteFile(%s) error = %v", path, err) } }