All checks were successful
CI/CD / detect-changes (pull_request) Successful in 18s
CI/CD / test-frontend (pull_request) Successful in 5s
CI/CD / test-go-pg (pull_request) Successful in 2m42s
CI/CD / test-sqlite (pull_request) Successful in 2m59s
CI/CD / build-and-deploy (pull_request) Successful in 1m24s
Add realtime pub/sub: Starlark realtime.publish() module gated by new realtime.publish permission, WS room protocol (room.subscribe/ room.unsubscribe intercepted in readPump with 100-room cap), and SDK sw.realtime.subscribe() with auto room join/leave and reconnect recovery. Migrate 5 bare confirm() calls to sw.confirm() with destructive styling in tasks, schedules, notes (×2), and editor packages. Add admin permissions UI: per-permission grant/revoke drawer, Grant All bulk action, pending_review/suspended status badges, disabled enable toggle when pending. Closes v0.4.7 permissions UI gap. 8 new Go tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
159 lines
4.2 KiB
Go
159 lines
4.2 KiB
Go
package sandbox
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"go.starlark.net/starlark"
|
|
|
|
"switchboard-core/events"
|
|
)
|
|
|
|
func TestRealtimePublish_HappyPath(t *testing.T) {
|
|
bus := events.NewBus()
|
|
|
|
var mu sync.Mutex
|
|
var received events.Event
|
|
|
|
bus.Subscribe("realtime.*", func(e events.Event) {
|
|
mu.Lock()
|
|
received = e
|
|
mu.Unlock()
|
|
})
|
|
|
|
mod := BuildRealtimeModule(context.Background(), bus, "test-pkg")
|
|
sb := New(DefaultConfig())
|
|
|
|
_, err := sb.Exec(context.Background(), "test.star", `
|
|
result = realtime.publish("room:123", "chat.message", {"text": "hello", "from": "alice"})
|
|
`, map[string]starlark.Value{"realtime": mod})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if received.Label != "realtime.chat.message" {
|
|
t.Errorf("expected label 'realtime.chat.message', got %q", received.Label)
|
|
}
|
|
if received.Room != "room:123" {
|
|
t.Errorf("expected room 'room:123', got %q", received.Room)
|
|
}
|
|
|
|
var payload map[string]any
|
|
if err := json.Unmarshal(received.Payload, &payload); err != nil {
|
|
t.Fatalf("payload unmarshal error: %v", err)
|
|
}
|
|
if payload["text"] != "hello" {
|
|
t.Errorf("expected text 'hello', got %v", payload["text"])
|
|
}
|
|
if payload["from"] != "alice" {
|
|
t.Errorf("expected from 'alice', got %v", payload["from"])
|
|
}
|
|
if payload["_pkg"] != "test-pkg" {
|
|
t.Errorf("expected _pkg 'test-pkg', got %v", payload["_pkg"])
|
|
}
|
|
}
|
|
|
|
func TestRealtimePublish_EmptyData(t *testing.T) {
|
|
bus := events.NewBus()
|
|
var received events.Event
|
|
bus.Subscribe("realtime.*", func(e events.Event) { received = e })
|
|
|
|
mod := BuildRealtimeModule(context.Background(), bus, "my-pkg")
|
|
sb := New(DefaultConfig())
|
|
|
|
_, err := sb.Exec(context.Background(), "test.star", `
|
|
realtime.publish("ch:1", "heartbeat")
|
|
`, map[string]starlark.Value{"realtime": mod})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
var payload map[string]any
|
|
json.Unmarshal(received.Payload, &payload)
|
|
if payload["_pkg"] != "my-pkg" {
|
|
t.Errorf("expected _pkg 'my-pkg', got %v", payload["_pkg"])
|
|
}
|
|
// Should only have _pkg
|
|
if len(payload) != 1 {
|
|
t.Errorf("expected 1 key (_pkg only), got %d", len(payload))
|
|
}
|
|
}
|
|
|
|
func TestRealtimePublish_MissingArgs(t *testing.T) {
|
|
bus := events.NewBus()
|
|
mod := BuildRealtimeModule(context.Background(), bus, "pkg")
|
|
sb := New(DefaultConfig())
|
|
|
|
// Missing event
|
|
_, err := sb.Exec(context.Background(), "test.star", `
|
|
realtime.publish("ch:1")
|
|
`, map[string]starlark.Value{"realtime": mod})
|
|
if err == nil {
|
|
t.Fatal("expected error for missing event arg")
|
|
}
|
|
}
|
|
|
|
func TestRealtimePublish_ReservedPrefix(t *testing.T) {
|
|
bus := events.NewBus()
|
|
mod := BuildRealtimeModule(context.Background(), bus, "pkg")
|
|
sb := New(DefaultConfig())
|
|
|
|
_, err := sb.Exec(context.Background(), "test.star", `
|
|
realtime.publish("ch:1", "system.notify", {})
|
|
`, map[string]starlark.Value{"realtime": mod})
|
|
if err == nil {
|
|
t.Fatal("expected error for reserved prefix")
|
|
}
|
|
if !strings.Contains(err.Error(), "reserved prefix") {
|
|
t.Errorf("error should mention reserved prefix, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRealtimePublish_PayloadTooLarge(t *testing.T) {
|
|
bus := events.NewBus()
|
|
mod := BuildRealtimeModule(context.Background(), bus, "pkg")
|
|
sb := New(DefaultConfig())
|
|
|
|
// Build a large string via repeated concatenation in Starlark
|
|
_, err := sb.Exec(context.Background(), "test.star", `
|
|
big = "x" * 8000
|
|
realtime.publish("ch:1", "big.event", {"data": big})
|
|
`, map[string]starlark.Value{"realtime": mod})
|
|
if err == nil {
|
|
t.Fatal("expected error for payload too large")
|
|
}
|
|
if !strings.Contains(err.Error(), "too large") {
|
|
t.Errorf("error should mention 'too large', got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStarlarkToGoVal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
val starlark.Value
|
|
want any
|
|
}{
|
|
{"none", starlark.None, nil},
|
|
{"true", starlark.True, true},
|
|
{"false", starlark.False, false},
|
|
{"int", starlark.MakeInt(42), int64(42)},
|
|
{"float", starlark.Float(3.14), float64(3.14)},
|
|
{"string", starlark.String("hello"), "hello"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := starlarkToGoVal(tt.val)
|
|
if got != tt.want {
|
|
t.Errorf("starlarkToGoVal(%s) = %v (%T), want %v (%T)", tt.val, got, got, tt.want, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|