Merge pull request #158 from bnema/fix/ignore-manifestless-extension-dirs
fix(extensions): ignore directories without manifests
2 files · +37 -1
M packages/agent/extensions/manager.go +5 -1
| @@ -289,11 +289,15 @@ func (m *Manager) Discover(ctx context.Context) []error { | |||
| 289 | 289 | if !e.IsDir() || strings.HasPrefix(e.Name(), ".") { | |
| 290 | 290 | continue | |
| 291 | 291 | } | |
| 292 | + | extDir := filepath.Join(dir, e.Name()) | |
| 293 | + | if _, err := os.Stat(filepath.Join(extDir, "extension.json")); errors.Is(err, os.ErrNotExist) { | |
| 294 | + | continue | |
| 295 | + | } | |
| 292 | 296 | if seenDirs[e.Name()] { | |
| 293 | 297 | continue // higher-priority location already queued | |
| 294 | 298 | } | |
| 295 | 299 | seenDirs[e.Name()] = true | |
| 296 | - | jobs = append(jobs, loadJob{dir: filepath.Join(dir, e.Name())}) | |
| 300 | + | jobs = append(jobs, loadJob{dir: extDir}) | |
| 297 | 301 | } | |
| 298 | 302 | } | |
| 299 | 303 | ||
| @@ -289,11 +289,15 @@ func (m *Manager) Discover(ctx context.Context) []error { | |||||
| 289 | if !e.IsDir() || strings.HasPrefix(e.Name(), ".") { | 289 | if !e.IsDir() || strings.HasPrefix(e.Name(), ".") { | ||
| 290 | continue | 290 | continue | ||
| 291 | } | 291 | } | ||
| 292 | + | extDir := filepath.Join(dir, e.Name()) | |||
| 293 | + | if _, err := os.Stat(filepath.Join(extDir, "extension.json")); errors.Is(err, os.ErrNotExist) { | |||
| 294 | + | continue | |||
| 295 | + | } | |||
| 292 | if seenDirs[e.Name()] { | 296 | if seenDirs[e.Name()] { | ||
| 293 | continue // higher-priority location already queued | 297 | continue // higher-priority location already queued | ||
| 294 | } | 298 | } | ||
| 295 | seenDirs[e.Name()] = true | 299 | seenDirs[e.Name()] = true | ||
| 296 | - | jobs = append(jobs, loadJob{dir: filepath.Join(dir, e.Name())}) | 300 | + | jobs = append(jobs, loadJob{dir: extDir}) |
| 297 | } | 301 | } | ||
| 298 | } | 302 | } | ||
| 299 | 303 | ||||
M packages/agent/extensions/manager_test.go +32 -0
| @@ -103,6 +103,38 @@ func (s *stubHooks) ClearExtensionChrome(name string) { | |||
| 103 | 103 | s.chromeClears = append(s.chromeClears, name) | |
| 104 | 104 | } | |
| 105 | 105 | ||
| 106 | + | func TestDiscoverIgnoresDirectoriesWithoutManifest(t *testing.T) { | |
| 107 | + | home := t.TempDir() | |
| 108 | + | cwd := t.TempDir() | |
| 109 | + | ||
| 110 | + | orphanedDataDir := filepath.Join(cwd, ".zut", "extensions", "example", "projects") | |
| 111 | + | if err := os.MkdirAll(orphanedDataDir, 0o700); err != nil { | |
| 112 | + | t.Fatal(err) | |
| 113 | + | } | |
| 114 | + | if err := os.WriteFile(filepath.Join(orphanedDataDir, "state.json"), []byte(`{}`), 0o600); err != nil { | |
| 115 | + | t.Fatal(err) | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | globalExtDir := filepath.Join(home, "extensions", "example") | |
| 119 | + | if err := os.MkdirAll(globalExtDir, 0o755); err != nil { | |
| 120 | + | t.Fatal(err) | |
| 121 | + | } | |
| 122 | + | if err := os.WriteFile(filepath.Join(globalExtDir, "extension.json"), []byte(`{"name":"example"}`), 0o644); err != nil { | |
| 123 | + | t.Fatal(err) | |
| 124 | + | } | |
| 125 | + | if err := os.WriteFile(filepath.Join(globalExtDir, "theme.json"), []byte(`{"name":"Example"}`), 0o644); err != nil { | |
| 126 | + | t.Fatal(err) | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | mgr := New(home, cwd, "0.0.0-test", "", "", nil) | |
| 130 | + | if errs := mgr.Discover(context.Background()); len(errs) > 0 { | |
| 131 | + | t.Fatalf("discover errors = %v, want none", errs) | |
| 132 | + | } | |
| 133 | + | if _, ok := mgr.ext["example"]; !ok { | |
| 134 | + | t.Fatal("global extension was shadowed by a project directory without a manifest") | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 106 | 138 | // writeMockExtension creates a minimal extension on disk that uses a | |
| 107 | 139 | // shell script (or batch file on windows) to drive the protocol. The | |
| 108 | 140 | // script reads commands from stdin and emits hard-coded responses, | |
| @@ -103,6 +103,38 @@ func (s *stubHooks) ClearExtensionChrome(name string) { | |||||
| 103 | s.chromeClears = append(s.chromeClears, name) | 103 | s.chromeClears = append(s.chromeClears, name) | ||
| 104 | } | 104 | } | ||
| 105 | 105 | ||||
| 106 | + | func TestDiscoverIgnoresDirectoriesWithoutManifest(t *testing.T) { | |||
| 107 | + | home := t.TempDir() | |||
| 108 | + | cwd := t.TempDir() | |||
| 109 | + | ||||
| 110 | + | orphanedDataDir := filepath.Join(cwd, ".zut", "extensions", "example", "projects") | |||
| 111 | + | if err := os.MkdirAll(orphanedDataDir, 0o700); err != nil { | |||
| 112 | + | t.Fatal(err) | |||
| 113 | + | } | |||
| 114 | + | if err := os.WriteFile(filepath.Join(orphanedDataDir, "state.json"), []byte(`{}`), 0o600); err != nil { | |||
| 115 | + | t.Fatal(err) | |||
| 116 | + | } | |||
| 117 | + | ||||
| 118 | + | globalExtDir := filepath.Join(home, "extensions", "example") | |||
| 119 | + | if err := os.MkdirAll(globalExtDir, 0o755); err != nil { | |||
| 120 | + | t.Fatal(err) | |||
| 121 | + | } | |||
| 122 | + | if err := os.WriteFile(filepath.Join(globalExtDir, "extension.json"), []byte(`{"name":"example"}`), 0o644); err != nil { | |||
| 123 | + | t.Fatal(err) | |||
| 124 | + | } | |||
| 125 | + | if err := os.WriteFile(filepath.Join(globalExtDir, "theme.json"), []byte(`{"name":"Example"}`), 0o644); err != nil { | |||
| 126 | + | t.Fatal(err) | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | mgr := New(home, cwd, "0.0.0-test", "", "", nil) | |||
| 130 | + | if errs := mgr.Discover(context.Background()); len(errs) > 0 { | |||
| 131 | + | t.Fatalf("discover errors = %v, want none", errs) | |||
| 132 | + | } | |||
| 133 | + | if _, ok := mgr.ext["example"]; !ok { | |||
| 134 | + | t.Fatal("global extension was shadowed by a project directory without a manifest") | |||
| 135 | + | } | |||
| 136 | + | } | |||
| 137 | + | ||||
| 106 | // writeMockExtension creates a minimal extension on disk that uses a | 138 | // writeMockExtension creates a minimal extension on disk that uses a | ||
| 107 | // shell script (or batch file on windows) to drive the protocol. The | 139 | // shell script (or batch file on windows) to drive the protocol. The | ||
| 108 | // script reads commands from stdin and emits hard-coded responses, | 140 | // script reads commands from stdin and emits hard-coded responses, | ||