From b8583ef019487768ce4fc900c215e61856d9ca78 Mon Sep 17 00:00:00 2001 From: Molecule AI Fullstack Engineer Date: Mon, 18 May 2026 00:44:48 +0000 Subject: [PATCH] test(handlers): add filesystem suite for ListRegistry (plugins_listing.go) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests listRegistryFiltered and ListRegistry endpoint: - empty dir → empty list - files (non-dirs) → ignored - single plugin with plugin.yaml → appears in list - runtime filter: claude-code plugin matches, hermes plugin excluded - universal plugin (no runtimes field) → always included - nonexistent dir → empty list (ReadDir error is non-fatal) - HTTP endpoint: GET /plugins → 200 with JSON array Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/plugins_listing_test.go | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 workspace-server/internal/handlers/plugins_listing_test.go diff --git a/workspace-server/internal/handlers/plugins_listing_test.go b/workspace-server/internal/handlers/plugins_listing_test.go new file mode 100644 index 000000000..8f9db9be1 --- /dev/null +++ b/workspace-server/internal/handlers/plugins_listing_test.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/gin-gonic/gin" +) + +func TestListRegistry_EmptyDir(t *testing.T) { + dir := t.TempDir() + h := NewPluginsHandler(dir, nil, nil) + + got := h.listRegistryFiltered("") + if len(got) != 0 { + t.Errorf("expected empty list, got %d plugins", len(got)) + } +} + +func TestListRegistry_IgnoresFiles(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "not-a-plugin.txt"), []byte("x"), 0600); err != nil { + t.Fatal(err) + } + h := NewPluginsHandler(dir, nil, nil) + + got := h.listRegistryFiltered("") + if len(got) != 0 { + t.Errorf("expected empty list (files ignored), got %d", len(got)) + } +} + +func TestListRegistry_SinglePlugin(t *testing.T) { + dir := t.TempDir() + pluginDir := filepath.Join(dir, "my-plugin") + if err := os.Mkdir(pluginDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte("name: my-plugin\nversion: 1.0.0\n"), 0600); err != nil { + t.Fatal(err) + } + h := NewPluginsHandler(dir, nil, nil) + + got := h.listRegistryFiltered("") + if len(got) != 1 { + t.Fatalf("expected 1 plugin, got %d", len(got)) + } + if got[0].Name != "my-plugin" { + t.Errorf("expected name 'my-plugin', got %q", got[0].Name) + } +} + +func TestListRegistry_FiltersByRuntime(t *testing.T) { + dir := t.TempDir() + for _, spec := range []struct{ name, yaml string }{ + {"runtime-a", "name: runtime-a\nruntimes:\n - claude-code\n"}, + {"runtime-b", "name: runtime-b\nruntimes:\n - hermes\n"}, + {"universal", "name: universal\nversion: 1.0.0\n"}, + } { + pd := filepath.Join(dir, spec.name) + if err := os.Mkdir(pd, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(pd, "plugin.yaml"), []byte(spec.yaml), 0600); err != nil { + t.Fatal(err) + } + } + h := NewPluginsHandler(dir, nil, nil) + + // Filter to claude-code: runtime-a matches, universal (no runtimes field) + // is always included per supportsRuntime semantics. + got := h.listRegistryFiltered("claude-code") + if len(got) != 2 { + t.Fatalf("expected 2 (runtime-a + universal), got %d: %v", len(got), func() []string { + ns := make([]string, len(got)) + for i, p := range got { ns[i] = p.Name } + return ns + }()) + } +} + +func TestListRegistry_PluginWithNoRuntimeDeclarations_AlwaysIncluded(t *testing.T) { + dir := t.TempDir() + pd := filepath.Join(dir, "universal-plugin") + if err := os.Mkdir(pd, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(pd, "plugin.yaml"), []byte("name: universal-plugin\nversion: 1.0.0\n"), 0600); err != nil { + t.Fatal(err) + } + h := NewPluginsHandler(dir, nil, nil) + + // When plugin declares no runtimes, it should always be included (try-it). + got := h.listRegistryFiltered("any-runtime") + if len(got) != 1 { + t.Errorf("expected 1 plugin (unspecified runtime), got %d", len(got)) + } +} + +func TestListRegistry_ReadDirError_ReturnsEmpty(t *testing.T) { + h := NewPluginsHandler("/nonexistent/path/for/plugins", nil, nil) + got := h.listRegistryFiltered("") + if len(got) != 0 { + t.Errorf("expected empty list on ReadDir error, got %d", len(got)) + } +} + +func TestListRegistry_HTTPEndpoint(t *testing.T) { + dir := t.TempDir() + pd := filepath.Join(dir, "test-plugin") + if err := os.Mkdir(pd, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(pd, "plugin.yaml"), []byte("name: test-plugin\nversion: 2.0.0\n"), 0600); err != nil { + t.Fatal(err) + } + h := NewPluginsHandler(dir, nil, nil) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("GET", "/plugins", nil) + h.ListRegistry(c) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + var plugins []pluginInfo + if err := json.Unmarshal(w.Body.Bytes(), &plugins); err != nil { + t.Fatalf("failed to parse JSON: %v", err) + } + if len(plugins) != 1 { + t.Errorf("expected 1 plugin, got %d", len(plugins)) + } + if plugins[0].Name != "test-plugin" { + t.Errorf("expected name 'test-plugin', got %q", plugins[0].Name) + } +} -- 2.52.0