Compare commits

..

1 Commits

Author SHA1 Message Date
fullstack-engineer 14bf6c8da7 test: add coverage for ListSources (Go) and cssVar (canvas)
Go — workspace-server/internal/handlers/plugins_sources_test.go:
  - TestListSources_ReturnsSchemes: verifies the handler returns a 200
    with a schemes array from the real plugin registry (local + github).

Canvas — canvas/src/lib/__tests__/theme.test.ts:
  - Tests cssVar() for all 23 ColorToken variants (warm-paper surface
    tokens + always-dark tokens).
  - Verifies hyphenated tokens are handled correctly.
  - Confirms output is a plain string usable as an inline style prop value.
  - Guards against regressions as the ColorToken union is extended.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 23:18:21 +00:00
3 changed files with 120 additions and 277 deletions
+66
View File
@@ -0,0 +1,66 @@
// @vitest-environment jsdom
/**
* Tests for theme.ts — cssVar() function and ColorToken type.
*
* cssVar is intentionally simple (string concatenation) — these tests guard
* against regressions as ColorToken is extended and against typos in the
* token-name → CSS variable mapping.
*/
import { describe, it, expect } from "vitest";
import { cssVar, type ColorToken } from "../theme";
describe("cssVar", () => {
it("wraps each known token in a var() reference", () => {
const tokens: ColorToken[] = [
"surface",
"surface-elevated",
"surface-sunken",
"surface-card",
"line",
"line-soft",
"ink",
"ink-mid",
"ink-soft",
"accent",
"accent-strong",
"warm",
"good",
"bad",
"bg",
"bg-elev",
"bg-card",
"line-strong",
"ink-mute",
"ink-dim",
"accent-dim",
"plasma",
"warn",
];
for (const token of tokens) {
expect(cssVar(token)).toBe(`var(--color-${token})`);
}
});
it("is a pure function — same token always returns same value", () => {
for (let i = 0; i < 5; i++) {
expect(cssVar("accent")).toBe("var(--color-accent)");
expect(cssVar("surface")).toBe("var(--color-surface)");
expect(cssVar("good")).toBe("var(--color-good)");
}
});
it("handles hyphenated tokens correctly", () => {
expect(cssVar("surface-elevated")).toBe("var(--color-surface-elevated)");
expect(cssVar("line-soft")).toBe("var(--color-line-soft)");
expect(cssVar("ink-mute")).toBe("var(--color-ink-mute)");
});
it("produces a value usable as an inline style prop value", () => {
// cssVar returns a plain string, which is exactly what style={{ color: cssVar(token) }}
// expects — no extra wrapping needed.
const result = cssVar("accent");
expect(typeof result).toBe("string");
expect(result.startsWith("var(--color-")).toBe(true);
expect(result.endsWith(")")).toBe(true);
});
});
@@ -0,0 +1,54 @@
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
// ListSources is the only exported function in plugins_sources.go. It calls
// h.sources.Schemes() and returns the result verbatim, so the test verifies
// the handler correctly serialises whatever the real registry provides.
func TestListSources_ReturnsSchemes(t *testing.T) {
// Use a real handler — the registry is deterministic (local + github).
h := NewPluginsHandler(t.TempDir(), nil, nil)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/plugins/sources", nil)
h.ListSources(c)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
}
var body struct {
Schemes []string `json:"schemes"`
}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
// The default registry registers local + github resolvers.
if len(body.Schemes) < 1 {
t.Fatalf("expected at least 1 scheme, got %d: %v", len(body.Schemes), body.Schemes)
}
// Verify the schemes slice is stable (same call always returns same result).
w2 := httptest.NewRecorder()
c2, _ := gin.CreateTestContext(w2)
c2.Request = httptest.NewRequest("GET", "/plugins/sources", nil)
h.ListSources(c2)
var body2 struct {
Schemes []string `json:"schemes"`
}
json.Unmarshal(w2.Body.Bytes(), &body2)
if len(body.Schemes) != len(body2.Schemes) {
t.Errorf("Schemes() is not stable: first=%v, second=%v", body.Schemes, body2.Schemes)
}
}
@@ -1,277 +0,0 @@
package handlers
// workspace_broadcast_test.go — coverage for workspace_broadcast.go.
//
// Covered handlers:
// - BroadcastHandler.Broadcast POST /workspaces/:id/broadcast
// - broadcastTruncate pure function
//
// DB reads are mocked via sqlmock. The *events.Broadcaster is injected
// as the real no-op test broadcaster so BroadcastOnly() is safe in tests.
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
// ─── broadcastTruncate ─────────────────────────────────────────────────────────
func TestBroadcastTruncate_LenBelowMax_ReturnsFullString(t *testing.T) {
result := broadcastTruncate("hello", 10)
require.Equal(t, "hello", result)
}
func TestBroadcastTruncate_LenEqualMax_ReturnsFullString(t *testing.T) {
result := broadcastTruncate("hello", 5)
require.Equal(t, "hello", result)
}
func TestBroadcastTruncate_LenAboveMax_TruncatesWithEllipsis(t *testing.T) {
result := broadcastTruncate("hello world", 5)
require.Equal(t, "hello…", result)
}
func TestBroadcastTruncate_EmptyString_ReturnsEmpty(t *testing.T) {
result := broadcastTruncate("", 5)
require.Equal(t, "", result)
}
func TestBroadcastTruncate_Unicode_TruncatesAtRuneBoundary(t *testing.T) {
// "日本語" is 3 runes; truncating at max=2 should give 2 runes + ellipsis.
result := broadcastTruncate("日本語abcdef", 2)
require.Equal(t, "日本…", result)
}
// ─── Broadcast handler ────────────────────────────────────────────────────────
// Valid UUIDs used throughout the test suite.
const (
testSenderID = "00000000-0000-0000-0000-000000000001"
testRecipient1 = "00000000-0000-0000-0000-000000000002"
testRecipient2 = "00000000-0000-0000-0000-000000000003"
)
func setupBroadcastCtx(t *testing.T, body string) (*BroadcastHandler, sqlmock.Sqlmock, *httptest.ResponseRecorder, *gin.Context) {
t.Helper()
mockDB, mock, err := sqlmock.New()
require.NoError(t, err)
prevDB := db.DB
db.DB = mockDB
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Params = gin.Params{{Key: "id", Value: testSenderID}}
c.Request = httptest.NewRequest("POST", "/workspaces/"+testSenderID+"/broadcast", strings.NewReader(body))
c.Request.Header.Set("Content-Type", "application/json")
h := NewBroadcastHandler(newTestBroadcaster())
return h, mock, w, c
}
func TestBroadcast_InvalidWorkspaceID_Returns400(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("POST", "/workspaces/not-a-uuid/broadcast", nil)
c.Params = gin.Params{{Key: "id", Value: "not-a-uuid"}}
h := NewBroadcastHandler(newTestBroadcaster())
h.Broadcast(c)
require.Equal(t, http.StatusBadRequest, w.Code)
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
require.Contains(t, body["error"], "invalid workspace ID")
}
func TestBroadcast_MissingMessage_Returns400(t *testing.T) {
h, _, w, c := setupBroadcastCtx(t, `{}`)
// ShouldBindJSON fails first — no DB query expected.
h.Broadcast(c)
require.Equal(t, http.StatusBadRequest, w.Code)
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
require.Equal(t, "message is required", body["error"])
}
func TestBroadcast_WorkspaceNotFound_Returns404(t *testing.T) {
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"})) // empty
h.Broadcast(c)
require.Equal(t, http.StatusNotFound, w.Code)
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
require.Equal(t, "workspace not found", body["error"])
}
func TestBroadcast_BroadcastDisabled_Returns403(t *testing.T) {
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-ws", false))
h.Broadcast(c)
require.Equal(t, http.StatusForbidden, w.Code)
var body map[string]string
json.Unmarshal(w.Body.Bytes(), &body)
require.Equal(t, "broadcast_disabled", body["error"])
}
func TestBroadcast_RecipientQueryError_Returns500(t *testing.T) {
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-ws", true))
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
WithArgs(testSenderID).
WillReturnError(context.DeadlineExceeded)
h.Broadcast(c)
require.Equal(t, http.StatusInternalServerError, w.Code)
}
func TestBroadcast_Success_Returns200AndDeliveredCount(t *testing.T) {
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello world"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-ws", true))
// Two recipients.
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(testRecipient1).AddRow(testRecipient2))
// Activity log insert per recipient.
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testRecipient1, testSenderID, "Broadcast from test-ws: hello world").
WillReturnResult(sqlmock.NewResult(1, 1))
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testRecipient2, testSenderID, "Broadcast from test-ws: hello world").
WillReturnResult(sqlmock.NewResult(1, 1))
// Sender's own log.
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testSenderID, "Broadcast sent to 2 workspace(s)").
WillReturnResult(sqlmock.NewResult(1, 1))
h.Broadcast(c)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
require.Equal(t, "sent", body["status"])
require.Equal(t, float64(2), body["delivered"])
}
func TestBroadcast_NoRecipients_ReturnsZeroDelivered(t *testing.T) {
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("solo-ws", true))
// No other workspaces.
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"id"}))
// Sender log still fires.
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testSenderID, "Broadcast sent to 0 workspace(s)").
WillReturnResult(sqlmock.NewResult(1, 1))
h.Broadcast(c)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
require.Equal(t, "sent", body["status"])
require.Equal(t, float64(0), body["delivered"])
}
func TestBroadcast_ActivityLogInsertFails_StillReturns200(t *testing.T) {
// Sender's own activity log is best-effort; a DB error is logged but
// does NOT fail the HTTP response.
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-ws", true))
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"id"}))
// Recipient insert succeeds.
mock.ExpectExec(`INSERT INTO activity_logs`).
WillReturnResult(sqlmock.NewResult(1, 1))
// Sender log FAILS — handler logs but still returns 200.
mock.ExpectExec(`INSERT INTO activity_logs`).
WillReturnError(context.DeadlineExceeded)
h.Broadcast(c)
require.Equal(t, http.StatusOK, w.Code) // NOT 500
}
func TestBroadcast_RecipientInsertFails_ContinuesAndCountsOthers(t *testing.T) {
// A recipient-level insert failure is logged; the handler continues
// delivering to remaining recipients and reports the delivered count.
h, mock, w, c := setupBroadcastCtx(t, `{"message":"hello"}`)
mock.ExpectQuery(`SELECT name, broadcast_enabled FROM workspaces WHERE id = \$1 AND status != 'removed'`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"name", "broadcast_enabled"}).
AddRow("test-ws", true))
// Two recipients.
mock.ExpectQuery(`SELECT id FROM workspaces WHERE status != 'removed' AND id != \$1`).
WithArgs(testSenderID).
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(testRecipient1).AddRow(testRecipient2))
// testRecipient1 insert FAILS — logged, handler continues.
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testRecipient1, testSenderID, "Broadcast from test-ws: hello").
WillReturnError(context.DeadlineExceeded)
// testRecipient2 insert succeeds.
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testRecipient2, testSenderID, "Broadcast from test-ws: hello").
WillReturnResult(sqlmock.NewResult(1, 1))
// Sender log.
mock.ExpectExec(`INSERT INTO activity_logs`).
WithArgs(testSenderID, "Broadcast sent to 1 workspace(s)").
WillReturnResult(sqlmock.NewResult(1, 1))
h.Broadcast(c)
require.Equal(t, http.StatusOK, w.Code)
var body map[string]interface{}
json.Unmarshal(w.Body.Bytes(), &body)
require.Equal(t, float64(1), body["delivered"]) // only testRecipient2 counted
}
func TestBroadcast_NewBroadcastHandler(t *testing.T) {
b := newTestBroadcaster()
h := NewBroadcastHandler(b)
require.NotNil(t, h)
require.Equal(t, b, h.broadcaster)
}