Files

1032 lines
34 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0
package plugin
import (
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/pipeline"
)
func writePluginManifest(t *testing.T, dir string, m Manifest) {
t.Helper()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
data, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "plugin.json"), data, 0o644); err != nil {
t.Fatal(err)
}
}
const fakeGitProgram = `package main
import (
"os"
"path/filepath"
)
func main() {
mode := os.Getenv("DWS_PLUGIN_TEST_GIT_MODE")
if mode == "clone-fail" {
os.Exit(1)
}
if len(os.Args) < 2 {
os.Exit(2)
}
dest := os.Args[len(os.Args)-1]
if err := os.MkdirAll(filepath.Join(dest, ".git"), 0o755); err != nil {
os.Exit(3)
}
var manifest string
switch mode {
case "missing":
return
case "malformed":
manifest = "{"
case "invalid":
manifest = "{\"name\":\"x\",\"version\":\"1.0.0\"}"
case "build":
manifest = "{\"name\":\"git-build\",\"version\":\"1.0.0\",\"build\":{\"command\":\"true\"}}"
case "valid":
manifest = "{\"name\":\"git-valid\",\"version\":\"1.0.0\"}"
default:
manifest = "{\"name\":\"git-plugin\",\"version\":\"1.0.0\",\"type\":\"user\"}"
}
if err := os.WriteFile(filepath.Join(dest, "plugin.json"), []byte(manifest), 0o644); err != nil {
os.Exit(4)
}
if err := os.WriteFile(filepath.Join(dest, "content.txt"), []byte("cloned"), 0o644); err != nil {
os.Exit(5)
}
}
`
func buildFakeGit(t *testing.T) string {
t.Helper()
source := filepath.Join(t.TempDir(), "fake_git.go")
if err := os.WriteFile(source, []byte(fakeGitProgram), 0o600); err != nil {
t.Fatal(err)
}
binDir := t.TempDir()
name := "git"
if runtime.GOOS == "windows" {
name += ".exe"
}
cmd := exec.Command("go", "build", "-o", filepath.Join(binDir, name), source)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("build fake git: %v\n%s", err, output)
}
return binDir
}
func TestCrossPlatformCoveragePluginConverterEdges(t *testing.T) {
root := filepath.Join(t.TempDir(), "user", "sample-plugin")
t.Setenv("PLUGIN_TOKEN", "secret")
p := &Plugin{
Manifest: Manifest{
Name: "sample-plugin",
Description: "sample",
MCPServers: map[string]*MCPServer{
"missing": {Type: "stdio"},
"stdio": {
Type: "stdio",
Command: "$" + "{DWS_PLUGIN_ROOT}/bin/server",
Args: []string{"--data", "$" + "{DWS_PLUGIN_DATA}", "$" + "{PLUGIN_TOKEN}"},
Env: map[string]string{"ROOT": "$" + "{DWS_PLUGIN_ROOT}"},
},
"http-default": {
Type: "streamable-http",
Endpoint: "https://example.test",
CLI: json.RawMessage("{"),
},
"http-overlay": {
Type: "streamable-http",
Endpoint: "https://other.test",
CLI: json.RawMessage("{\"id\":\"custom\",\"command\":\"run\"}"),
Headers: map[string]string{"Authorization": "Bearer $" + "{PLUGIN_TOKEN}"},
},
},
},
Root: root,
}
if got := p.StdioClients(nil); len(got) != 1 || got[0].Key != "stdio" || got[0].Client == nil {
t.Fatalf("StdioClients(nil) = %#v", got)
}
if got := p.StdioClients(&UserContext{UserID: "u1", CorpID: "c1"}); len(got) != 1 {
t.Fatalf("StdioClients(user) length = %d", len(got))
}
descriptors := p.ToServerDescriptors()
if len(descriptors) != 1 {
t.Fatalf("descriptors length = %d", len(descriptors))
}
seenOverlay := false
for _, d := range descriptors {
switch d.Key {
case "http-overlay":
seenOverlay = d.CLI.ID == "custom" && d.CLI.Command == "run" &&
d.AuthHeaders["Authorization"] == "Bearer secret"
}
}
if !seenOverlay {
t.Fatalf("valid descriptor overlay not covered: %#v", descriptors)
}
dataDir := filepath.Join(filepath.Dir(filepath.Dir(root)), "data")
if got := expandPluginVars("$"+"{DWS_PLUGIN_ROOT}|$"+"{DWS_PLUGIN_DATA}|$"+"{PLUGIN_TOKEN}", root); got != root+"|"+dataDir+"|secret" {
t.Fatalf("expandPluginVars = %q", got)
}
}
func TestCrossPlatformCoverageHookAdapterEdges(t *testing.T) {
phases := map[string]pipeline.Phase{
" PRE-PARSE ": pipeline.PreParse,
"post-parse": pipeline.PostParse,
"pre-request": pipeline.PreRequest,
"post-response": pipeline.PostResponse,
"unknown": pipeline.PreRequest,
}
for raw, want := range phases {
h := NewHookAdapter("plug", HookEntry{Phase: raw, Command: "true", Timeout: 1})
if h.Phase() != want {
t.Errorf("phase %q = %v, want %v", raw, h.Phase(), want)
}
if !strings.HasPrefix(h.Name(), "plugin-hook:plug/") {
t.Errorf("Name = %q", h.Name())
}
}
ctx := &pipeline.Context{Command: "chat.send", Params: map[string]any{"x": 1}, Args: []string{"a"}}
tests := []struct {
name string
entry HookEntry
context *pipeline.Context
wantErr bool
}{
{"match success", HookEntry{Phase: "pre-request", Matcher: "chat.*", Command: "read input; test -n \"$input\""}, ctx, false},
{"no match", HookEntry{Matcher: "mail.*", Command: "exit 2"}, ctx, false},
{"invalid matcher", HookEntry{Matcher: "[", Command: "exit 2"}, ctx, false},
{"ordinary failure", HookEntry{Command: "echo failed; exit 1"}, ctx, false},
{"abort", HookEntry{Command: "echo aborted; exit 2"}, ctx, true},
{"marshal failure", HookEntry{Command: "exit 2"}, &pipeline.Context{Params: map[string]any{"bad": func() {}}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := NewHookAdapter("plug", tt.entry).Handle(tt.context)
if (err != nil) != tt.wantErr {
t.Fatalf("Handle error = %v, wantErr %v", err, tt.wantErr)
}
})
}
h := NewHookAdapter("plug", HookEntry{Command: "sleep 1", Timeout: 0})
h.timeout = time.Millisecond
if err := h.Handle(ctx); err != nil {
t.Fatalf("timeout is non-fatal: %v", err)
}
}
func TestCrossPlatformCoverageManifestEdges(t *testing.T) {
dir := t.TempDir()
if _, err := ParseManifest(filepath.Join(dir, "missing.json")); err == nil {
t.Fatal("expected missing manifest error")
}
badPath := filepath.Join(dir, "bad.json")
if err := os.WriteFile(badPath, []byte("{"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := ParseManifest(badPath); err == nil {
t.Fatal("expected malformed manifest error")
}
valid := Manifest{Name: "valid-plugin", Version: "v1.2.3-beta"}
absoluteCommand := filepath.Join(t.TempDir(), "bin", "server")
for name, mutate := range map[string]func(*Manifest){
"absolute stdio command": func(m *Manifest) {
m.MCPServers = map[string]*MCPServer{"x": {Type: "stdio", Command: absoluteCommand}}
},
"unsupported server": func(m *Manifest) {
m.MCPServers = map[string]*MCPServer{"x": {Type: "other"}}
},
"unsafe hooks": func(m *Manifest) { m.Hooks = "../hooks.json" },
} {
t.Run(name, func(t *testing.T) {
m := valid
mutate(&m)
if err := m.Validate("dev"); err == nil {
t.Fatal("expected validation error")
}
})
}
if err := valid.Validate("dev"); err != nil {
t.Fatalf("valid dev manifest: %v", err)
}
if err := validateSafePath("skills"); err != nil {
t.Fatal(err)
}
if isValidSemver("1.2") || isValidSemver("1.x.3") || !isValidSemver("v1.2.3-beta") {
t.Fatal("semver validation edge failed")
}
if got := compareSemver("2.0.0", "1.9.9"); got != 1 {
t.Fatalf("major comparison = %d", got)
}
if got := compareSemver("1.2.0", "1.1.9"); got != 1 {
t.Fatalf("minor comparison = %d", got)
}
if got := compareSemver("1.2.3", "1.2.4"); got != -1 {
t.Fatalf("patch comparison = %d", got)
}
if got := compareSemver("1.2.3", "1.2.3"); got != 0 {
t.Fatalf("equal comparison = %d", got)
}
if a, b, c := parseSemver("bad"); a != 0 || b != 0 || c != 0 {
t.Fatalf("parse invalid = %d.%d.%d", a, b, c)
}
p := &Plugin{Manifest: Manifest{Name: "valid-plugin"}, Root: dir}
if hooks, err := p.LoadHooks(); err != nil || hooks != nil {
t.Fatalf("empty hooks = %#v, %v", hooks, err)
}
p.Manifest.Hooks = "hooks.json"
if hooks, err := p.LoadHooks(); err != nil || hooks != nil {
t.Fatalf("missing hooks = %#v, %v", hooks, err)
}
if err := os.WriteFile(filepath.Join(dir, "hooks.json"), []byte("{"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := p.LoadHooks(); err == nil {
t.Fatal("expected malformed hooks error")
}
if err := os.WriteFile(filepath.Join(dir, "hooks.json"), []byte("{\"hooks\":[{\"phase\":\"pre-request\",\"command\":\"true\"}]}"), 0o644); err != nil {
t.Fatal(err)
}
if hooks, err := p.LoadHooks(); err != nil || len(hooks.Hooks) != 1 {
t.Fatalf("valid hooks = %#v, %v", hooks, err)
}
if err := os.Remove(filepath.Join(dir, "hooks.json")); err != nil {
t.Fatal(err)
}
if err := os.Mkdir(filepath.Join(dir, "hooks.json"), 0o755); err != nil {
t.Fatal(err)
}
if _, err := p.LoadHooks(); err == nil {
t.Fatal("expected hooks read error")
}
if got := (&Plugin{Root: dir}).SkillsDir(); got != filepath.Join(dir, "skills") {
t.Fatalf("default SkillsDir = %q", got)
}
p.Manifest.Skills = "custom"
if got := p.SkillsDir(); got != filepath.Join(dir, "custom") {
t.Fatalf("custom SkillsDir = %q", got)
}
}
func TestCrossPlatformCoverageLoaderDiscoveryLifecycle(t *testing.T) {
oldHome := pluginUserHomeDir
t.Cleanup(func() { pluginUserHomeDir = oldHome })
t.Setenv("DWS_CONFIG_DIR", "")
home := t.TempDir()
pluginUserHomeDir = func() (string, error) { return home, nil }
defaultLoader := NewLoader("1.2.3")
if defaultLoader.PluginsDir != filepath.Join(home, ".dws", "plugins") {
t.Fatalf("NewLoader path = %q", defaultLoader.PluginsDir)
}
pluginUserHomeDir = func() (string, error) { return "", errors.New("home") }
if got := NewLoader("dev").PluginsDir; got != filepath.Join(".dws", "plugins") {
t.Fatalf("NewLoader error path = %q", got)
}
customConfig := t.TempDir()
t.Setenv("DWS_CONFIG_DIR", customConfig)
if got := NewLoader("dev").PluginsDir; got != filepath.Join(customConfig, "plugins") {
t.Fatalf("NewLoader custom config path = %q", got)
}
t.Setenv("DWS_CONFIG_DIR", "")
root := t.TempDir()
l := &Loader{PluginsDir: root, CLIVersion: "1.0.0"}
userDir := filepath.Join(root, "user")
direct := filepath.Join(userDir, "direct-plugin")
workspace := filepath.Join(userDir, "acme")
nested := filepath.Join(workspace, "nested-plugin")
invalid := filepath.Join(userDir, "invalid-plugin")
incompatible := filepath.Join(userDir, "future-plugin")
writePluginManifest(t, direct, Manifest{Name: "direct-plugin", Version: "1.0.0", Description: "direct"})
writePluginManifest(t, nested, Manifest{Name: "nested-plugin", Version: "1.0.0", Description: "nested"})
writePluginManifest(t, invalid, Manifest{Name: "x", Version: "1.0.0"})
writePluginManifest(t, incompatible, Manifest{Name: "future-plugin", Version: "1.0.0", MinCLIVersion: "9.0.0"})
if err := os.WriteFile(filepath.Join(userDir, "not-a-dir"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(workspace, "not-a-dir"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
brokenNested := filepath.Join(workspace, "broken-plugin")
if err := os.MkdirAll(brokenNested, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(brokenNested, "plugin.json"), []byte("{"), 0o644); err != nil {
t.Fatal(err)
}
l.saveSettings(&Settings{EnabledPlugins: map[string]bool{
"direct-plugin": false,
"acme/nested-plugin": true,
}})
got := l.LoadUser()
if len(got) != 1 || got[0].Manifest.Name != "nested-plugin" {
t.Fatalf("LoadUser = %#v", got)
}
infos := l.ListInstalled()
if len(infos) != 4 {
t.Fatalf("ListInstalled user count = %d: %#v", len(infos), infos)
}
foundDirect := false
foundNested := false
for _, info := range infos {
if info.Name == "direct-plugin" {
foundDirect = !info.Enabled && info.Type == "user" && info.Description == "direct"
}
if info.Name == "acme/nested-plugin" {
foundNested = info.Enabled && info.Type == "user" && info.Description == "nested"
}
}
if !foundDirect || !foundNested {
t.Fatalf("installed info missing: %#v", infos)
}
if err := l.SetEnabled("missing-plugin", true); err == nil {
t.Fatal("expected missing SetEnabled error")
}
if err := l.SetEnabled("direct-plugin", true); err != nil {
t.Fatal(err)
}
if got := l.LoadUser(); len(got) != 2 {
t.Fatalf("enabled LoadUser = %#v", got)
}
if err := l.SetEnabled("acme/nested-plugin", false); err != nil {
t.Fatal(err)
}
if !l.loadSettings().EnabledPlugins["direct-plugin"] {
t.Fatal("direct plugin was not enabled")
}
if err := l.RemovePlugin("missing-plugin", false); err == nil {
t.Fatal("expected missing RemovePlugin error")
}
dataDir := filepath.Join(root, configPluginDataDirForTest(), "direct-plugin")
if err := os.MkdirAll(dataDir, 0o755); err != nil {
t.Fatal(err)
}
if err := l.RemovePlugin("direct-plugin", true); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(dataDir); err != nil {
t.Fatalf("keepData removed data: %v", err)
}
if err := l.RemovePlugin("acme/nested-plugin", false); err != nil {
t.Fatal(err)
}
plain := &Loader{PluginsDir: filepath.Join(t.TempDir(), "plugins")}
if plain.settingsPath() != filepath.Join(filepath.Dir(plain.PluginsDir), "settings.json") {
t.Fatalf("production settingsPath = %q", plain.settingsPath())
}
invalidSettings := &Loader{PluginsDir: t.TempDir()}
if err := os.WriteFile(invalidSettings.settingsPath(), []byte("{"), 0o644); err != nil {
t.Fatal(err)
}
if settings := invalidSettings.loadSettings(); settings == nil || settings.EnabledPlugins != nil {
t.Fatalf("invalid settings = %#v", settings)
}
}
func configPluginDataDirForTest() string {
return "data"
}
func TestCrossPlatformCoverageLoaderDevAndConfigEdges(t *testing.T) {
root := t.TempDir()
l := &Loader{PluginsDir: root, CLIVersion: "1.0.0"}
if got := l.LoadDev(); got != nil {
t.Fatalf("empty LoadDev = %#v", got)
}
missing := filepath.Join(t.TempDir(), "missing")
bad := filepath.Join(t.TempDir(), "bad")
valid := filepath.Join(t.TempDir(), "valid")
writePluginManifest(t, bad, Manifest{Name: "x", Version: "1.0.0"})
writePluginManifest(t, valid, Manifest{Name: "valid-plugin", Version: "1.0.0", Description: "dev"})
l.saveSettings(&Settings{DevPlugins: map[string]string{
"missing-plugin": missing,
"bad-plugin": bad,
"valid-plugin": valid,
}})
if got := l.LoadDev(); len(got) != 1 || got[0].Root != valid {
t.Fatalf("LoadDev = %#v", got)
}
infos := l.ListInstalled()
if len(infos) != 2 {
t.Fatalf("ListInstalled dev entries = %#v", infos)
}
if err := l.UnregisterDevPlugin("valid-plugin"); err != nil {
t.Fatal(err)
}
if l.UnsetPluginConfig("missing", "key") {
t.Fatal("unset missing plugin succeeded")
}
l.saveSettings(&Settings{PluginConfigs: map[string]map[string]any{
"plug": {"number": 1, "text": "value", "empty": "", "PATH": "/bad"},
}})
if _, ok := l.GetPluginConfig("plug", "number"); ok {
t.Fatal("non-string config returned")
}
if got := l.ListPluginConfig("plug"); len(got) != 3 || got["text"] != "value" {
t.Fatalf("ListPluginConfig mixed = %#v", got)
}
if got := l.ListPluginConfig("missing"); len(got) != 0 {
t.Fatalf("missing configs = %#v", got)
}
key := "DWS_PLUGIN_EDGE_CONFIG"
t.Setenv(key, "existing")
settings := l.loadSettings()
settings.PluginConfigs["plug"][key] = "replacement"
settings.PluginConfigs["plug"]["DWS_PLUGIN_EDGE_NEW"] = "new"
settings.PluginConfigs["plug"]["not-string"] = 7
l.saveSettings(settings)
t.Cleanup(func() { _ = os.Unsetenv("DWS_PLUGIN_EDGE_NEW") })
l.InjectPluginConfigEnv()
if os.Getenv(key) != "existing" || os.Getenv("DWS_PLUGIN_EDGE_NEW") != "new" {
t.Fatalf("config env injection failed: %q %q", os.Getenv(key), os.Getenv("DWS_PLUGIN_EDGE_NEW"))
}
empty := &Loader{PluginsDir: t.TempDir()}
empty.InjectPluginConfigEnv()
empty.saveSettings(nil)
}
func TestCrossPlatformCoverageLoaderInstallBuildAndGit(t *testing.T) {
root := t.TempDir()
l := &Loader{PluginsDir: root, CLIVersion: "1.0.0"}
if _, err := l.InstallFromDir(filepath.Join(t.TempDir(), "missing")); err == nil {
t.Fatal("expected missing install error")
}
invalid := t.TempDir()
writePluginManifest(t, invalid, Manifest{Name: "x", Version: "1.0.0"})
if _, err := l.InstallFromDir(invalid); err == nil {
t.Fatal("expected invalid install error")
}
src := t.TempDir()
builtOutput := filepath.Join("bin", "server")
buildCommand := "mkdir -p bin && printf ok > bin/server"
if runtime.GOOS == "windows" {
builtOutput += ".exe"
buildCommand = "if not exist bin mkdir bin && echo ok>bin\\server.exe"
}
writePluginManifest(t, src, Manifest{
Name: "built-plugin",
Version: "1.0.0",
Build: &BuildConfig{
Command: buildCommand,
Output: builtOutput,
},
})
if err := os.WriteFile(filepath.Join(src, "payload.txt"), []byte("new"), 0o644); err != nil {
t.Fatal(err)
}
stale := filepath.Join(root, "user", "built-plugin", "stale.txt")
if err := os.MkdirAll(filepath.Dir(stale), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil {
t.Fatal(err)
}
installed, err := l.InstallFromDir(src)
if err != nil {
t.Fatalf("InstallFromDir: %v", err)
}
if installed.Manifest.Name != "built-plugin" {
t.Fatalf("installed = %#v", installed)
}
if _, err := os.Stat(stale); !os.IsNotExist(err) {
t.Fatalf("stale file remains: %v", err)
}
info, err := os.Stat(filepath.Join(installed.Root, builtOutput))
if err != nil {
t.Fatalf("built output is missing: %v", err)
}
if runtime.GOOS != "windows" && info.Mode()&0o111 == 0 {
t.Fatalf("built output is not executable: %v %#o", err, info.Mode())
}
buildFailure := t.TempDir()
writePluginManifest(t, buildFailure, Manifest{
Name: "failed-plugin", Version: "1.0.0",
Build: &BuildConfig{Command: "exit 1"},
})
if _, err := l.InstallFromDir(buildFailure); err == nil {
t.Fatal("expected build failure")
}
if _, err := os.Stat(filepath.Join(root, "user", "failed-plugin")); !os.IsNotExist(err) {
t.Fatalf("failed build destination remains: %v", err)
}
noBuild := t.TempDir()
writePluginManifest(t, noBuild, Manifest{Name: "plain-plugin", Version: "1.0.0"})
if err := BuildPlugin(noBuild); err != nil {
t.Fatal(err)
}
if err := BuildPlugin(filepath.Join(t.TempDir(), "missing")); err == nil {
t.Fatal("expected BuildPlugin parse error")
}
successfulBuildCommand := "true"
outputBuildCommand := "printf ok > output"
if runtime.GOOS == "windows" {
successfulBuildCommand = "exit /b 0"
outputBuildCommand = "echo ok>output"
}
writePluginManifest(t, noBuild, Manifest{
Name: "plain-plugin", Version: "1.0.0",
Build: &BuildConfig{Command: outputBuildCommand, Output: "output"},
})
if err := BuildPlugin(noBuild); err != nil {
t.Fatal(err)
}
absoluteOutput := filepath.Join(t.TempDir(), "x")
for name, build := range map[string]*BuildConfig{
"empty command": {},
"absolute output": {Command: successfulBuildCommand, Output: absoluteOutput},
"escaping output": {Command: successfulBuildCommand, Output: "../x"},
"missing output": {Command: successfulBuildCommand, Output: "missing"},
} {
t.Run(name, func(t *testing.T) {
if err := runBuild(t.TempDir(), build); err == nil {
t.Fatal("expected build error")
}
})
}
if got := buildCommandFor("windows", "echo ok"); len(got.Args) < 2 || got.Args[0] != "cmd" {
t.Fatalf("windows build command = %#v", got.Args)
}
if got := buildCommandFor("linux", "true"); len(got.Args) < 2 || got.Args[0] != "sh" {
t.Fatalf("unix build command = %#v", got.Args)
}
bin := buildFakeGit(t)
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("DWS_PLUGIN_TEST_GIT_MODE", "")
gitPlugin, err := l.InstallFromGit("https://example.test/acme/repository.git")
if err != nil {
t.Fatalf("InstallFromGit: %v", err)
}
if gitPlugin.Root != filepath.Join(root, "user", "acme", "git-plugin") {
t.Fatalf("git plugin root = %q", gitPlugin.Root)
}
if _, err := os.Stat(filepath.Join(gitPlugin.Root, ".git")); !os.IsNotExist(err) {
t.Fatalf(".git was copied: %v", err)
}
if _, err := l.InstallFromGit("file:///tmp/plugin"); err == nil {
t.Fatal("expected local git URL rejection")
}
t.Setenv("DWS_PLUGIN_TEST_GIT_MODE", "clone-fail")
if _, err := l.InstallFromGit("https://example.test/acme/fail.git"); err == nil {
t.Fatal("expected git clone failure")
}
}
func TestCrossPlatformCoverageLoaderSystemCallEdges(t *testing.T) {
oldReadDir := pluginReadDir
oldMkdirTemp := pluginMkdirTemp
oldRemoveAll := pluginRemoveAll
oldMkdirAll := pluginMkdirAll
oldReadFile := pluginReadFile
oldWriteFile := pluginWriteFile
oldRemove := pluginRemove
oldWalk := pluginWalk
oldRel := pluginRel
oldCopyDir := pluginCopyDir
oldRunBuild := pluginRunBuild
t.Cleanup(func() {
pluginReadDir = oldReadDir
pluginMkdirTemp = oldMkdirTemp
pluginRemoveAll = oldRemoveAll
pluginMkdirAll = oldMkdirAll
pluginReadFile = oldReadFile
pluginWriteFile = oldWriteFile
pluginRemove = oldRemove
pluginWalk = oldWalk
pluginRel = oldRel
pluginCopyDir = oldCopyDir
pluginRunBuild = oldRunBuild
})
root := t.TempDir()
l := &Loader{PluginsDir: root, CLIVersion: "1.0.0"}
pluginReadDir = func(string) ([]os.DirEntry, error) { return nil, errors.New("read dir") }
if got := l.LoadUser(); got != nil {
t.Fatalf("LoadUser read error = %#v", got)
}
pluginReadDir = oldReadDir
workspace := filepath.Join(root, "user", "workspace")
if err := os.MkdirAll(workspace, 0o755); err != nil {
t.Fatal(err)
}
pluginReadDir = func(path string) ([]os.DirEntry, error) {
if path == workspace {
return nil, errors.New("workspace read")
}
return oldReadDir(path)
}
if got := l.LoadUser(); len(got) != 0 {
t.Fatalf("LoadUser workspace read error = %#v", got)
}
var infos []PluginInfo
l.collectUserPluginInfos(workspace, "workspace", &Settings{}, &infos)
pluginReadDir = oldReadDir
src := t.TempDir()
writePluginManifest(t, src, Manifest{Name: "copy-plugin", Version: "1.0.0"})
pluginCopyDir = func(string, string) error { return errors.New("copy") }
if _, err := l.InstallFromDir(src); err == nil {
t.Fatal("expected install copy error")
}
pluginCopyDir = oldCopyDir
writePluginManifest(t, filepath.Join(root, "user", "remove-plugin"), Manifest{Name: "remove-plugin", Version: "1.0.0"})
pluginRemoveAll = func(string) error { return errors.New("remove") }
if err := l.RemovePlugin("remove-plugin", false); err == nil {
t.Fatal("expected remove error")
}
pluginRemoveAll = oldRemoveAll
l.purgePluginFromSettings("not-present")
l.saveSettings(&Settings{PluginConfigs: map[string]map[string]any{"bad": {"value": func() {}}}})
l.saveSettings(&Settings{PluginConfigs: map[string]map[string]any{"plug": {"present": "x"}}})
if l.UnsetPluginConfig("missing", "present") {
t.Fatal("unset missing plugin succeeded")
}
if l.UnsetPluginConfig("plug", "missing") {
t.Fatal("unset missing key succeeded")
}
skillRoot := t.TempDir()
if err := os.Mkdir(filepath.Join(skillRoot, "skills"), 0o755); err != nil {
t.Fatal(err)
}
oldHome := pluginUserHomeDir
t.Cleanup(func() { pluginUserHomeDir = oldHome })
home := t.TempDir()
pluginUserHomeDir = func() (string, error) { return home, nil }
pluginReadDir = func(string) ([]os.DirEntry, error) { return nil, errors.New("skills read") }
SyncSkills([]*Plugin{{Manifest: Manifest{Name: "skill-plugin"}, Root: skillRoot}})
pluginReadDir = oldReadDir
pluginMkdirTemp = func(string, string) (string, error) { return "", errors.New("temp") }
if _, err := l.InstallFromGit("https://host/acme/plugin.git"); err == nil {
t.Fatal("expected temp dir error")
}
pluginMkdirTemp = oldMkdirTemp
}
func TestCrossPlatformCoverageInstallFromGitValidationAndBuildEdges(t *testing.T) {
oldCopyDir := pluginCopyDir
oldRunBuild := pluginRunBuild
t.Cleanup(func() {
pluginCopyDir = oldCopyDir
pluginRunBuild = oldRunBuild
})
bin := buildFakeGit(t)
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
root := t.TempDir()
l := &Loader{PluginsDir: root, CLIVersion: "1.0.0"}
for _, mode := range []string{"missing", "malformed", "invalid"} {
t.Setenv("DWS_PLUGIN_TEST_GIT_MODE", mode)
if _, err := l.InstallFromGit("https://host/acme/repo.git"); err == nil {
t.Errorf("mode %q unexpectedly succeeded", mode)
}
}
t.Setenv("DWS_PLUGIN_TEST_GIT_MODE", "valid")
pluginCopyDir = func(string, string) error { return errors.New("copy") }
if _, err := l.InstallFromGit("https://host/acme/repo.git"); err == nil {
t.Fatal("expected git install copy error")
}
pluginCopyDir = oldCopyDir
t.Setenv("DWS_PLUGIN_TEST_GIT_MODE", "build")
pluginRunBuild = func(string, *BuildConfig) error { return errors.New("build") }
if _, err := l.InstallFromGit("https://host/acme/repo.git"); err == nil {
t.Fatal("expected git install build error")
}
}
func TestCrossPlatformCoverageParseGitURLEdges(t *testing.T) {
for _, raw := range []string{"/tmp/x", "./x", "git@host", "git@host:single", "ftp://host/org/repo", "://bad"} {
if _, _, err := parseGitURL(raw); err == nil {
t.Errorf("parseGitURL(%q) unexpectedly succeeded", raw)
}
}
if ws, repo, err := parseGitURL(" http://host/org/repo/ "); err != nil || ws != "org" || repo != "repo" {
t.Fatalf("trimmed HTTP URL = %q/%q, %v", ws, repo, err)
}
}
func TestCrossPlatformCoverageSyncSkillsEdges(t *testing.T) {
oldHome := pluginUserHomeDir
t.Cleanup(func() { pluginUserHomeDir = oldHome })
SyncSkills(nil)
pluginUserHomeDir = func() (string, error) { return "", errors.New("no home") }
SyncSkills([]*Plugin{{Manifest: Manifest{Name: "plugin-one"}, Root: t.TempDir()}})
home := t.TempDir()
pluginUserHomeDir = func() (string, error) { return home, nil }
if err := os.Mkdir(filepath.Join(home, ".agents"), 0o755); err != nil {
t.Fatal(err)
}
root := t.TempDir()
skills := filepath.Join(root, "skills")
nested := filepath.Join(skills, "nested")
if err := os.MkdirAll(nested, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(skills, "SKILL.md"), []byte("root"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(nested, "child.md"), []byte("child"), 0o644); err != nil {
t.Fatal(err)
}
missing := &Plugin{Manifest: Manifest{Name: "missing-plugin"}, Root: t.TempDir()}
p := &Plugin{Manifest: Manifest{Name: "plugin-one"}, Root: root}
SyncSkills([]*Plugin{missing, p})
base := filepath.Join(home, ".agents", "skills", "dws", "plugins", "plugin-one")
for path, want := range map[string]string{
filepath.Join(base, "SKILL.md"): "root",
filepath.Join(base, "nested", "child.md"): "child",
} {
data, err := os.ReadFile(path)
if err != nil || string(data) != want {
t.Fatalf("synced %q = %q, %v", path, data, err)
}
}
}
func TestCrossPlatformCoverageCopyAndStaleEdges(t *testing.T) {
src := t.TempDir()
dst := t.TempDir()
if err := os.MkdirAll(filepath.Join(src, "nested"), 0o750); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "same"), []byte("same"), 0o640); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dst, "same"), []byte("same"), 0o640); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(src, "nested", "new"), []byte("new"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(filepath.Join(src, "nested", "new"), filepath.Join(src, "link")); err != nil {
t.Fatal(err)
}
if err := copyDir(src, dst); err != nil {
t.Fatal(err)
}
if _, err := os.Lstat(filepath.Join(dst, "link")); !os.IsNotExist(err) {
t.Fatalf("symlink copied: %v", err)
}
staleFile := filepath.Join(dst, "stale")
staleDir := filepath.Join(dst, "stale-dir")
if err := os.WriteFile(staleFile, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(staleDir, 0o755); err != nil {
t.Fatal(err)
}
removeStaleFiles(src, dst)
if _, err := os.Stat(staleFile); !os.IsNotExist(err) {
t.Fatalf("stale file remains: %v", err)
}
if _, err := os.Stat(staleDir); !os.IsNotExist(err) {
t.Fatalf("stale dir remains: %v", err)
}
if err := copyDir(filepath.Join(src, "missing"), filepath.Join(t.TempDir(), "dest")); err == nil {
t.Fatal("expected missing source error")
}
fileDst := filepath.Join(t.TempDir(), "file")
if err := os.WriteFile(fileDst, []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := copyDir(src, fileDst); err == nil {
t.Fatal("expected destination mkdir error")
}
removeStaleFiles(filepath.Join(src, "missing"), filepath.Join(dst, "missing"))
}
func TestCrossPlatformCoverageCopyAndStaleSystemCallEdges(t *testing.T) {
oldMkdirAll := pluginMkdirAll
oldReadFile := pluginReadFile
oldWriteFile := pluginWriteFile
oldRemove := pluginRemove
oldWalk := pluginWalk
oldRel := pluginRel
t.Cleanup(func() {
pluginMkdirAll = oldMkdirAll
pluginReadFile = oldReadFile
pluginWriteFile = oldWriteFile
pluginRemove = oldRemove
pluginWalk = oldWalk
pluginRel = oldRel
})
src := t.TempDir()
dst := filepath.Join(t.TempDir(), "dst")
file := filepath.Join(src, "file")
if err := os.WriteFile(file, []byte("data"), 0o644); err != nil {
t.Fatal(err)
}
info, err := os.Stat(file)
if err != nil {
t.Fatal(err)
}
pluginWalk = func(string, filepath.WalkFunc) error { return errors.New("walk") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected walk error")
}
pluginWalk = func(_ string, fn filepath.WalkFunc) error {
return fn(file, info, errors.New("visit"))
}
if err := copyDir(src, dst); err == nil {
t.Fatal("expected visit error")
}
pluginWalk = func(_ string, fn filepath.WalkFunc) error { return fn(file, info, nil) }
pluginRel = func(string, string) (string, error) { return "", errors.New("rel") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected rel error")
}
pluginRel = func(string, string) (string, error) { return "../escape", nil }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected traversal error")
}
pluginRel = oldRel
pluginReadFile = func(string) ([]byte, error) { return nil, errors.New("read") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected source read error")
}
pluginReadFile = oldReadFile
pluginWriteFile = func(string, []byte, os.FileMode) error { return errors.New("write") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected destination write error")
}
pluginWriteFile = oldWriteFile
pluginWalk = oldWalk
pluginMkdirAll = func(string, os.FileMode) error { return errors.New("mkdir") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected initial mkdir error")
}
pluginMkdirAll = oldMkdirAll
removeStaleFiles(src, t.TempDir())
pluginWalk = func(root string, fn filepath.WalkFunc) error {
return fn(root, nil, errors.New("walk item"))
}
removeStaleFiles(src, dst)
call := 0
pluginWalk = func(root string, fn filepath.WalkFunc) error {
call++
rootInfo, statErr := os.Stat(root)
if statErr != nil {
return statErr
}
return fn(root, rootInfo, nil)
}
pluginRel = func(string, string) (string, error) { return "", errors.New("rel") }
removeStaleFiles(src, dst)
if call != 2 {
t.Fatalf("walk calls = %d", call)
}
pluginWalk = oldWalk
pluginRel = oldRel
staleDst := t.TempDir()
stale := filepath.Join(staleDst, "stale")
if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil {
t.Fatal(err)
}
pluginRemove = func(string) error { return errors.New("remove") }
removeStaleFiles(src, staleDst)
}
func TestCrossPlatformCoverageCopyAndStaleSystemCallEdgesDuplicate(t *testing.T) {
oldMkdirAll := pluginMkdirAll
oldReadFile := pluginReadFile
oldWriteFile := pluginWriteFile
oldRemove := pluginRemove
oldWalk := pluginWalk
oldRel := pluginRel
t.Cleanup(func() {
pluginMkdirAll = oldMkdirAll
pluginReadFile = oldReadFile
pluginWriteFile = oldWriteFile
pluginRemove = oldRemove
pluginWalk = oldWalk
pluginRel = oldRel
})
src := t.TempDir()
dst := filepath.Join(t.TempDir(), "dst")
file := filepath.Join(src, "file")
if err := os.WriteFile(file, []byte("data"), 0o644); err != nil {
t.Fatal(err)
}
info, err := os.Stat(file)
if err != nil {
t.Fatal(err)
}
pluginWalk = func(string, filepath.WalkFunc) error { return errors.New("walk") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected walk error")
}
pluginWalk = func(_ string, fn filepath.WalkFunc) error {
return fn(file, info, errors.New("visit"))
}
if err := copyDir(src, dst); err == nil {
t.Fatal("expected visit error")
}
pluginWalk = func(_ string, fn filepath.WalkFunc) error { return fn(file, info, nil) }
pluginRel = func(string, string) (string, error) { return "", errors.New("rel") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected rel error")
}
pluginRel = func(string, string) (string, error) { return "../escape", nil }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected traversal error")
}
pluginRel = oldRel
pluginReadFile = func(string) ([]byte, error) { return nil, errors.New("read") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected source read error")
}
pluginReadFile = oldReadFile
pluginWriteFile = func(string, []byte, os.FileMode) error { return errors.New("write") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected destination write error")
}
pluginWriteFile = oldWriteFile
pluginWalk = oldWalk
pluginMkdirAll = func(string, os.FileMode) error { return errors.New("mkdir") }
if err := copyDir(src, dst); err == nil {
t.Fatal("expected initial mkdir error")
}
pluginMkdirAll = oldMkdirAll
removeStaleFiles(src, t.TempDir())
pluginWalk = func(root string, fn filepath.WalkFunc) error {
return fn(root, nil, errors.New("walk item"))
}
removeStaleFiles(src, dst)
call := 0
pluginWalk = func(root string, fn filepath.WalkFunc) error {
call++
rootInfo, statErr := os.Stat(root)
if statErr != nil {
return statErr
}
return fn(root, rootInfo, nil)
}
pluginRel = func(string, string) (string, error) { return "", errors.New("rel") }
removeStaleFiles(src, dst)
if call != 2 {
t.Fatalf("walk calls = %d", call)
}
pluginWalk = oldWalk
pluginRel = oldRel
staleDst := t.TempDir()
stale := filepath.Join(staleDst, "stale")
if err := os.WriteFile(stale, []byte("stale"), 0o644); err != nil {
t.Fatal(err)
}
pluginRemove = func(string) error { return errors.New("remove") }
removeStaleFiles(src, staleDst)
}