package sandbox import ( "context" "encoding/json" "strings" "sync" "testing" "go.starlark.net/starlark" "armature/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) } }) } }