From 4a6e410788b5eda2d9abb07cb07d1ead0334b3a8 Mon Sep 17 00:00:00 2001 From: crossoverJie Date: Wed, 29 Jul 2026 09:25:12 +0800 Subject: [PATCH 1/3] feat: handle symlinked plugin directories in Discover - os.ReadDir reports symlinks as ModeSymlink, causing entry.IsDir() to return false and symlinked plugin directories to be silently skipped - Resolve symlinks via os.Stat before the IsDir check; broken symlinks produce a warning (matching existing skip-warning convention) - Add TestDiscover_symlinkedPluginDirectory and TestDiscover_brokenSymlink tests Signed-off-by: crossoverJie --- cli/internal/plugin/discover.go | 15 +++++-- cli/internal/plugin/discover_test.go | 59 ++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/cli/internal/plugin/discover.go b/cli/internal/plugin/discover.go index 7263e323..4ee13018 100644 --- a/cli/internal/plugin/discover.go +++ b/cli/internal/plugin/discover.go @@ -33,10 +33,19 @@ func Discover(pluginsDir string, stderr io.Writer) ([]*Plugin, error) { var plugins []*Plugin for _, entry := range entries { - // TODO: handle symlinked plugin directories - // (entry.Type()&os.ModeSymlink != 0 requires os.Stat to resolve) if !entry.IsDir() { - continue + if entry.Type()&os.ModeSymlink == 0 { + continue + } + entryPath := filepath.Join(pluginsDir, entry.Name()) + resolved, err := os.Stat(entryPath) + if err != nil { + fmt.Fprintf(stderr, "warning: skipping plugin at %s: %s\n", entryPath, err) + continue + } + if !resolved.IsDir() { + continue + } } pluginPath := filepath.Join(pluginsDir, entry.Name()) p, err := loadPlugin(pluginPath) diff --git a/cli/internal/plugin/discover_test.go b/cli/internal/plugin/discover_test.go index f1c458ec..09f47c35 100644 --- a/cli/internal/plugin/discover_test.go +++ b/cli/internal/plugin/discover_test.go @@ -296,3 +296,62 @@ func TestDiscover_setupFieldAbsent(t *testing.T) { t.Errorf("Setup: got %q, want empty string", plugins[0].Setup) } } + +func TestDiscover_symlinkedPluginDirectory(t *testing.T) { + // Create the real plugin directory outside pluginsDir + realRoot := t.TempDir() + pluginDir := writePlugin(t, realRoot, "dbt", validPluginYAML) + + // Create pluginsDir with a symlink to the real plugin directory + pluginsDir := t.TempDir() + symlinkPath := filepath.Join(pluginsDir, "dbt-link") + if err := os.Symlink(pluginDir, symlinkPath); err != nil { + t.Fatalf("could not create symlink: %v", err) + } + + var stderr strings.Builder + plugins, err := plugin.Discover(pluginsDir, &stderr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(plugins) != 1 { + t.Fatalf("expected 1 plugin via symlink, got %d", len(plugins)) + } + p := plugins[0] + if p.Path != symlinkPath { + t.Errorf("Path: got %q, want %q", p.Path, symlinkPath) + } + if p.Name != "dbt" { + t.Errorf("Name: got %q, want %q", p.Name, "dbt") + } + if stderr.Len() != 0 { + t.Errorf("unexpected warning: %q", stderr.String()) + } +} + +func TestDiscover_brokenSymlink(t *testing.T) { + pluginsDir := t.TempDir() + + // Create a symlink pointing to a non-existent target + brokenTarget := filepath.Join(pluginsDir, "nonexistent-dir") + symlinkPath := filepath.Join(pluginsDir, "broken-link") + if err := os.Symlink(brokenTarget, symlinkPath); err != nil { + t.Fatalf("could not create symlink: %v", err) + } + + // Create a valid plugin to ensure Discover can still find non-symlink dirs + writePlugin(t, pluginsDir, "valid", validPluginYAML) + + var stderr strings.Builder + plugins, err := plugin.Discover(pluginsDir, &stderr) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(plugins) != 1 { + t.Errorf("expected 1 valid plugin, got %d", len(plugins)) + } + warnings := stderr.String() + if !strings.Contains(warnings, symlinkPath) { + t.Errorf("warning should contain broken symlink path, got: %q", warnings) + } +} From 5ecd0958a00775d1403ce2a3ee06f091258a9cca Mon Sep 17 00:00:00 2001 From: crossoverJie Date: Wed, 29 Jul 2026 09:32:12 +0800 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cli/internal/plugin/discover_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/internal/plugin/discover_test.go b/cli/internal/plugin/discover_test.go index 09f47c35..5045663c 100644 --- a/cli/internal/plugin/discover_test.go +++ b/cli/internal/plugin/discover_test.go @@ -304,10 +304,10 @@ func TestDiscover_symlinkedPluginDirectory(t *testing.T) { // Create pluginsDir with a symlink to the real plugin directory pluginsDir := t.TempDir() - symlinkPath := filepath.Join(pluginsDir, "dbt-link") - if err := os.Symlink(pluginDir, symlinkPath); err != nil { - t.Fatalf("could not create symlink: %v", err) - } +symlinkPath := filepath.Join(pluginsDir, "dbt-link") +if err := os.Symlink(pluginDir, symlinkPath); err != nil { + t.Skipf("symlinks not supported or not permitted on this platform: %v", err) +} var stderr strings.Builder plugins, err := plugin.Discover(pluginsDir, &stderr) From 54f8b6efa3bfb9e16fcbdc9c134790cbde59f520 Mon Sep 17 00:00:00 2001 From: crossoverJie Date: Wed, 29 Jul 2026 09:33:27 +0800 Subject: [PATCH 3/3] test: improve symlink handling in discover tests --- cli/internal/plugin/discover_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cli/internal/plugin/discover_test.go b/cli/internal/plugin/discover_test.go index 5045663c..93016a95 100644 --- a/cli/internal/plugin/discover_test.go +++ b/cli/internal/plugin/discover_test.go @@ -304,10 +304,10 @@ func TestDiscover_symlinkedPluginDirectory(t *testing.T) { // Create pluginsDir with a symlink to the real plugin directory pluginsDir := t.TempDir() -symlinkPath := filepath.Join(pluginsDir, "dbt-link") -if err := os.Symlink(pluginDir, symlinkPath); err != nil { - t.Skipf("symlinks not supported or not permitted on this platform: %v", err) -} + symlinkPath := filepath.Join(pluginsDir, "dbt-link") + if err := os.Symlink(pluginDir, symlinkPath); err != nil { + t.Skipf("symlinks not supported or not permitted on this platform: %v", err) + } var stderr strings.Builder plugins, err := plugin.Discover(pluginsDir, &stderr)