Compare commits

...

2 Commits

Author SHA1 Message Date
fullstack-engineer 1ec7671636 test(canvas): add STATUS_CONFIG shape + glow/label/bar coverage
Adds design-tokens.test.ts — 48 new tests covering:
- STATUS_CONFIG: required shape (dot/glow/label/bar per entry), all 7 keys
- STATUS_CONFIG.dot: verified against statusDotClass() output
- STATUS_CONFIG.glow: shadow token presence per status
- STATUS_CONFIG.label: exact label text per status
- STATUS_CONFIG.bar: gradient format, token colour per status, to-transparent suffix
- TIER_CONFIG: shape, tier labels
- COMM_TYPE_LABELS: shape, label values

Issue: #1815 follow-up — design-tokens.ts was at ~33% coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:24:55 +00:00
fullstack-engineer 404ea5833b test(handlers): add coverage for plugin listing endpoints and helpers
Adds plugins_listing_test.go — 9 new tests covering:
- ListRegistry: all plugins returned, empty dir, runtime filter, unspecified
  runtimes included for all, manifest fields parsed, no-manifest dir fallback
- ListAvailableForWorkspace: runtime filter via runtimeLookup, unspecified
  runtimes included
- HTTP status: both endpoints return 200

Issue: #1815 follow-up — plugins_listing.go was at 0% coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 10:15:56 +00:00
2 changed files with 498 additions and 0 deletions
@@ -0,0 +1,211 @@
// @vitest-environment jsdom
/**
* Tests for design-tokens.ts — STATUS_CONFIG, TIER_CONFIG, COMM_TYPE_LABELS,
* and statusDotClass.
*
* Companion to statusDotClass.test.ts (which covers statusDotClass's dot field
* + TIER_CONFIG + COMM_TYPE_LABELS in depth). This file adds explicit shape
* coverage for STATUS_CONFIG's glow/label/bar fields and cross-checks them
* against the tailwind tokens used in statusDotClass's dot field.
*
* Issue: #1815 follow-up — design-tokens.ts was at ~33% coverage.
*/
import { describe, it, expect } from "vitest";
import {
STATUS_CONFIG,
TIER_CONFIG,
COMM_TYPE_LABELS,
statusDotClass,
} from "../design-tokens";
const STATUS_KEYS = [
"online",
"offline",
"paused",
"degraded",
"failed",
"provisioning",
"not_configured",
] as const;
// ── STATUS_CONFIG: all keys have the required shape ──────────────────────────
describe("STATUS_CONFIG — required shape", () => {
for (const key of STATUS_KEYS) {
it(`${key}: has dot, glow, label, bar fields`, () => {
const entry = STATUS_CONFIG[key];
expect(entry).toBeDefined();
expect(typeof entry.dot).toBe("string");
expect(typeof entry.glow).toBe("string");
expect(typeof entry.label).toBe("string");
expect(typeof entry.bar).toBe("string");
});
}
it("has exactly 7 status entries", () => {
expect(Object.keys(STATUS_CONFIG)).toHaveLength(7);
});
});
// ── STATUS_CONFIG: dot field matches statusDotClass ──────────────────────────────
describe("STATUS_CONFIG — dot field matches statusDotClass()", () => {
for (const key of STATUS_KEYS) {
it(`${key}: dot === statusDotClass("${key}")`, () => {
expect(STATUS_CONFIG[key].dot).toBe(statusDotClass(key));
});
}
});
// ── STATUS_CONFIG: glow field ─────────────────────────────────────────────────
describe("STATUS_CONFIG — glow field", () => {
it('"online" has a glow (shadow-emerald)', () => {
expect(STATUS_CONFIG.online.glow).toMatch(/emerald/i);
});
it('"degraded" has a glow (shadow-amber)', () => {
expect(STATUS_CONFIG.degraded.glow).toMatch(/amber/i);
});
it('"provisioning" has a glow (shadow-sky)', () => {
expect(STATUS_CONFIG.provisioning.glow).toMatch(/sky/i);
});
it('"not_configured" has a glow (shadow-amber)', () => {
expect(STATUS_CONFIG.not_configured.glow).toMatch(/amber/i);
});
it('"offline" and "paused" have no glow', () => {
expect(STATUS_CONFIG.offline.glow).toBe("");
expect(STATUS_CONFIG.paused.glow).toBe("");
});
it('"failed" has a glow (shadow-red)', () => {
expect(STATUS_CONFIG.failed.glow).toMatch(/red/i);
});
});
// ── STATUS_CONFIG: label field ─────────────────────────────────────────────────
describe("STATUS_CONFIG — label field", () => {
it('"online" → "Online"', () => {
expect(STATUS_CONFIG.online.label).toBe("Online");
});
it('"offline" → "Offline"', () => {
expect(STATUS_CONFIG.offline.label).toBe("Offline");
});
it('"paused" → "Paused"', () => {
expect(STATUS_CONFIG.paused.label).toBe("Paused");
});
it('"degraded" → "Degraded"', () => {
expect(STATUS_CONFIG.degraded.label).toBe("Degraded");
});
it('"failed" → "Failed"', () => {
expect(STATUS_CONFIG.failed.label).toBe("Failed");
});
it('"provisioning" → "Starting"', () => {
expect(STATUS_CONFIG.provisioning.label).toBe("Starting");
});
it('"not_configured" → "Not configured"', () => {
expect(STATUS_CONFIG.not_configured.label).toBe("Not configured");
});
it("all labels are non-empty strings", () => {
for (const key of STATUS_KEYS) {
expect(STATUS_CONFIG[key].label.length).toBeGreaterThan(0);
}
});
});
// ── STATUS_CONFIG: bar field ─────────────────────────────────────────────────
describe("STATUS_CONFIG — bar field", () => {
for (const key of STATUS_KEYS) {
it(`${key}: bar field is a gradient string`, () => {
expect(STATUS_CONFIG[key].bar).toMatch(/^from-/);
});
}
it('"online" bar uses emerald token', () => {
expect(STATUS_CONFIG.online.bar).toContain("emerald");
});
it('"degraded" bar uses amber token', () => {
expect(STATUS_CONFIG.degraded.bar).toContain("amber");
});
it('"failed" bar uses red token', () => {
expect(STATUS_CONFIG.failed.bar).toContain("red");
});
it('"provisioning" bar uses sky token', () => {
expect(STATUS_CONFIG.provisioning.bar).toContain("sky");
});
it('"not_configured" bar uses amber token', () => {
expect(STATUS_CONFIG.not_configured.bar).toContain("amber");
});
it("all bar fields end with 'to-transparent'", () => {
for (const key of STATUS_KEYS) {
expect(STATUS_CONFIG[key].bar).toMatch(/to-transparent$/);
}
});
});
// ── STATUS_CONFIG: unknown key falls back gracefully ──────────────────────────
describe("STATUS_CONFIG — unknown key via statusDotClass", () => {
it("unknown status returns fallback dot (bg-zinc-500)", () => {
expect(statusDotClass("unknown-status")).toBe("bg-zinc-500");
});
});
// ── TIER_CONFIG: shape ───────────────────────────────────────────────────────
describe("TIER_CONFIG — shape", () => {
it("has entries for tiers 14", () => {
expect(TIER_CONFIG).toHaveProperty("1");
expect(TIER_CONFIG).toHaveProperty("2");
expect(TIER_CONFIG).toHaveProperty("3");
expect(TIER_CONFIG).toHaveProperty("4");
});
it("each tier has label, color, border fields", () => {
for (const tier of [1, 2, 3, 4] as const) {
expect(typeof TIER_CONFIG[tier].label).toBe("string");
expect(typeof TIER_CONFIG[tier].color).toBe("string");
expect(typeof TIER_CONFIG[tier].border).toBe("string");
}
});
it("all label values match the tier name", () => {
expect(TIER_CONFIG[1].label).toBe("T1");
expect(TIER_CONFIG[2].label).toBe("T2");
expect(TIER_CONFIG[3].label).toBe("T3");
expect(TIER_CONFIG[4].label).toBe("T4");
});
});
// ── COMM_TYPE_LABELS ────────────────────────────────────────────────────────
describe("COMM_TYPE_LABELS — shape", () => {
it("has a2a_send, a2a_receive, task_update", () => {
expect(COMM_TYPE_LABELS.a2a_send).toBe("sent");
expect(COMM_TYPE_LABELS.a2a_receive).toBe("received");
expect(COMM_TYPE_LABELS.task_update).toBe("task update");
});
it("all labels are non-empty strings", () => {
for (const key of Object.keys(COMM_TYPE_LABELS)) {
expect(COMM_TYPE_LABELS[key].length).toBeGreaterThan(0);
}
});
});
@@ -0,0 +1,287 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
)
// setupTestPlugins creates a temporary plugins directory with named
// subdirectories, each optionally containing a plugin.yaml manifest.
func setupTestPlugins(t *testing.T, plugins map[string]string /* name → yamlContents */) string {
t.Helper()
dir := t.TempDir()
for name, yaml := range plugins {
plugDir := filepath.Join(dir, name)
if err := os.MkdirAll(plugDir, 0755); err != nil {
t.Fatalf("setupTestPlugins: mkdir %s: %v", plugDir, err)
}
if yaml != "" {
if err := os.WriteFile(filepath.Join(plugDir, "plugin.yaml"), []byte(yaml), 0644); err != nil {
t.Fatalf("setupTestPlugins: write plugin.yaml for %s: %v", name, err)
}
}
}
return dir
}
// makeTestHandler creates a PluginsHandler wired with a stub runtimeLookup
// that always returns "claude-code".
func makeTestHandler(t *testing.T, pluginsDir string) *PluginsHandler {
t.Helper()
h := NewPluginsHandler(pluginsDir, nil, nil)
h.WithRuntimeLookup(func(workspaceID string) (string, error) {
return "claude-code", nil
})
return h
}
// listRegistry fires ListRegistry and returns the parsed []pluginInfo.
func listRegistry(h *PluginsHandler, runtime string) []pluginInfo {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/plugins", nil)
if runtime != "" {
c.Request.URL.RawQuery = "runtime=" + runtime
}
h.ListRegistry(c)
var out []pluginInfo
json.Unmarshal(w.Body.Bytes(), &out)
return out
}
// listAvailable fires ListAvailableForWorkspace and returns the parsed []pluginInfo.
func listAvailable(h *PluginsHandler, workspaceID string) []pluginInfo {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/workspaces/"+workspaceID+"/plugins/available", nil)
c.Params = []gin.Param{{Key: "id", Value: workspaceID}}
h.ListAvailableForWorkspace(c)
var out []pluginInfo
json.Unmarshal(w.Body.Bytes(), &out)
return out
}
// --- ListRegistry ---
func TestListRegistry_ReturnsAllPlugins(t *testing.T) {
pluginsDir := setupTestPlugins(t, map[string]string{
"plugin-a": `name: plugin-a`,
"plugin-b": `name: plugin-b`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "")
if len(plugins) != 2 {
t.Fatalf("expected 2 plugins, got %d", len(plugins))
}
names := make(map[string]bool)
for _, p := range plugins {
names[p.Name] = true
}
if !names["plugin-a"] || !names["plugin-b"] {
t.Errorf("unexpected plugin names: %v", names)
}
}
func TestListRegistry_EmptyDirectory(t *testing.T) {
dir := t.TempDir()
h := makeTestHandler(t, dir)
plugins := listRegistry(h, "")
if len(plugins) != 0 {
t.Fatalf("expected 0 plugins for empty dir, got %d", len(plugins))
}
}
func TestListRegistry_FiltersByRuntime(t *testing.T) {
// plugin-a supports claude-code; plugin-b supports langgraph only.
pluginsDir := setupTestPlugins(t, map[string]string{
"plugin-a": `
name: plugin-a
runtimes:
- claude_code
`,
"plugin-b": `
name: plugin-b
runtimes:
- langgraph
`,
})
h := makeTestHandler(t, pluginsDir)
// Filter by claude-code — only plugin-a matches.
plugins := listRegistry(h, "claude-code")
if len(plugins) != 1 || plugins[0].Name != "plugin-a" {
t.Errorf("expected [plugin-a], got %v", plugins)
}
// Filter by langgraph — only plugin-b matches.
plugins = listRegistry(h, "langgraph")
if len(plugins) != 1 || plugins[0].Name != "plugin-b" {
t.Errorf("expected [plugin-b], got %v", plugins)
}
// Filter by unknown runtime — plugins that declare specific runtimes
// are excluded; only unspecified (empty Runtimes) would appear.
plugins = listRegistry(h, "autogen")
if len(plugins) != 0 {
t.Errorf("expected 0 for unknown runtime, got %v", plugins)
}
}
func TestListRegistry_UnspecifiedRuntimesIncludedForAll(t *testing.T) {
// A plugin with no runtimes field is treated as "unspecified" — included
// for every runtime filter.
pluginsDir := setupTestPlugins(t, map[string]string{
"generic-plugin": `name: generic-plugin`,
"claude-plugin": `
name: claude-plugin
runtimes:
- claude_code
`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "claude-code")
names := make(map[string]bool)
for _, p := range plugins {
names[p.Name] = true
}
// Both should appear: generic-plugin (unspecified) + claude-plugin.
if !names["generic-plugin"] || !names["claude-plugin"] {
t.Errorf("expected both plugins, got %v", names)
}
}
func TestListRegistry_ManifestFieldsParsed(t *testing.T) {
// Verify that ListRegistry returns the full pluginInfo shape from plugin.yaml.
pluginsDir := setupTestPlugins(t, map[string]string{
"my-plugin": `
name: my-plugin
version: 1.2.3
description: A test plugin
author: Test Author
tags:
- testing
- demo
skills:
- tdd-loop
- code-review
runtimes:
- claude_code
`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "")
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin, got %d", len(plugins))
}
p := plugins[0]
if p.Name != "my-plugin" {
t.Errorf("Name = %q; want my-plugin", p.Name)
}
if p.Version != "1.2.3" {
t.Errorf("Version = %q; want 1.2.3", p.Version)
}
if p.Description != "A test plugin" {
t.Errorf("Description = %q; want 'A test plugin'", p.Description)
}
if p.Author != "Test Author" {
t.Errorf("Author = %q; want 'Test Author'", p.Author)
}
if len(p.Tags) != 2 {
t.Errorf("Tags = %v; want 2 entries", p.Tags)
}
if len(p.Skills) != 2 {
t.Errorf("Skills = %v; want 2 entries", p.Skills)
}
if len(p.Runtimes) != 1 {
t.Errorf("Runtimes = %v; want 1 entry", p.Runtimes)
}
}
func TestListRegistry_NoManifestFile(t *testing.T) {
// A plugin directory without plugin.yaml should still appear with its
// directory name as the Name.
pluginsDir := setupTestPlugins(t, map[string]string{
"no-manifest-plugin": ``, // directory with no plugin.yaml
})
h := makeTestHandler(t, pluginsDir)
plugins := listRegistry(h, "")
if len(plugins) != 1 {
t.Fatalf("expected 1 plugin, got %d", len(plugins))
}
if plugins[0].Name != "no-manifest-plugin" {
t.Errorf("expected Name to be directory name, got %q", plugins[0].Name)
}
}
// --- ListAvailableForWorkspace ---
func TestListAvailableForWorkspace_WithRuntimeLookup(t *testing.T) {
// When runtimeLookup is wired, ListAvailableForWorkspace filters by the
// workspace's runtime (claude-code), which is set by makeTestHandler.
pluginsDir := setupTestPlugins(t, map[string]string{
"runtime-plugin": `
name: runtime-plugin
runtimes:
- claude_code
`,
"langgraph-plugin": `
name: langgraph-plugin
runtimes:
- langgraph
`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listAvailable(h, "ws-123")
if len(plugins) != 1 || plugins[0].Name != "runtime-plugin" {
t.Errorf("expected [runtime-plugin], got %v", plugins)
}
}
func TestListAvailableForWorkspace_UnspecifiedRuntime(t *testing.T) {
// A plugin without runtimes is included for all workspaces.
pluginsDir := setupTestPlugins(t, map[string]string{
"generic": `name: generic`,
})
h := makeTestHandler(t, pluginsDir)
plugins := listAvailable(h, "ws-456")
if len(plugins) != 1 || plugins[0].Name != "generic" {
t.Errorf("expected [generic], got %v", plugins)
}
}
// --- HTTP response codes ---
func TestListRegistry_HTTPStatusOK(t *testing.T) {
pluginsDir := setupTestPlugins(t, map[string]string{"foo": "name: foo"})
h := NewPluginsHandler(pluginsDir, nil, nil)
gin.SetMode(gin.TestMode)
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", w.Code)
}
}
func TestListAvailableForWorkspace_HTTPStatusOK(t *testing.T) {
pluginsDir := setupTestPlugins(t, map[string]string{"bar": "name: bar"})
h := makeTestHandler(t, pluginsDir)
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/workspaces/ws-789/plugins/available", nil)
c.Params = []gin.Param{{Key: "id", Value: "ws-789"}}
h.ListAvailableForWorkspace(c)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}