Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ec7671636 | |||
| 404ea5833b |
@@ -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 1–4", () => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user