Files
synapbus/internal/jsruntime/pool_test.go
T
Algis DumbrisandClaude Opus 4.6 e8165511de feat: add sandboxed JS/TS runtime engine (internal/jsruntime)
Pure Go JavaScript/TypeScript execution engine using goja + esbuild.
Provides sandboxed code execution with a call() bridge for agent
actions, automatic TypeScript detection and transpilation, timeout
enforcement, max-calls limits, and a concurrent execution pool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 07:53:25 +02:00

190 lines
4.2 KiB
Go

package jsruntime
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
)
func TestNewPool(t *testing.T) {
p := NewPool(5)
if p.Size() != 5 {
t.Errorf("expected size 5, got %d", p.Size())
}
if p.Available() != 5 {
t.Errorf("expected 5 available, got %d", p.Available())
}
}
func TestNewPool_MinSize(t *testing.T) {
p := NewPool(0)
if p.Size() != 1 {
t.Errorf("expected size clamped to 1, got %d", p.Size())
}
p2 := NewPool(-5)
if p2.Size() != 1 {
t.Errorf("expected size clamped to 1, got %d", p2.Size())
}
}
func TestPool_Execute(t *testing.T) {
p := NewPool(3)
defer p.Close()
caller := newMockCaller()
result, err := p.Execute(context.Background(), `42`, caller, ExecuteOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var num int64
switch v := result.Value.(type) {
case int64:
num = v
case float64:
num = int64(v)
}
if num != 42 {
t.Errorf("expected 42, got %v", result.Value)
}
}
func TestPool_ConcurrentExecution(t *testing.T) {
poolSize := 3
p := NewPool(poolSize)
defer p.Close()
numGoroutines := 20
var wg sync.WaitGroup
var successCount int32
var errCount int32
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
caller := newMockCaller()
result, err := p.Execute(context.Background(), `({ value: 1 })`, caller, ExecuteOptions{})
if err != nil {
atomic.AddInt32(&errCount, 1)
return
}
if result != nil {
atomic.AddInt32(&successCount, 1)
}
}(i)
}
wg.Wait()
if int(errCount) > 0 {
t.Errorf("expected 0 errors, got %d", errCount)
}
if int(successCount) != numGoroutines {
t.Errorf("expected %d successes, got %d", numGoroutines, successCount)
}
// All slots should be available again
if p.Available() != poolSize {
t.Errorf("expected %d available after all complete, got %d", poolSize, p.Available())
}
}
func TestPool_BlocksWhenExhausted(t *testing.T) {
p := NewPool(1)
defer p.Close()
// Occupy the only slot with a long-running script
started := make(chan struct{})
done := make(chan struct{})
go func() {
caller := newMockCaller()
close(started)
p.Execute(context.Background(), `
var i = 0;
while(i < 1000000) { i++; }
i
`, caller, ExecuteOptions{})
close(done)
}()
<-started
// Give the goroutine a moment to acquire the slot
time.Sleep(10 * time.Millisecond)
// Try to execute with a short timeout — should fail because the slot is occupied
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
caller := newMockCaller()
_, err := p.Execute(ctx, `42`, caller, ExecuteOptions{})
if err == nil {
t.Error("expected context deadline error when pool is exhausted, got nil")
}
// Wait for first execution to finish
<-done
}
func TestPool_ClosedPoolRejectsExecute(t *testing.T) {
p := NewPool(3)
p.Close()
caller := newMockCaller()
_, err := p.Execute(context.Background(), `42`, caller, ExecuteOptions{})
if err == nil {
t.Error("expected error from closed pool, got nil")
}
}
func TestPool_WithCallBridge(t *testing.T) {
p := NewPool(2)
defer p.Close()
caller := newMockCaller()
caller.results["greet"] = map[string]any{"greeting": "hello"}
code := `
var res = call("greet", { name: "world" });
res.ok ? res.result.greeting : "error"
`
result, err := p.Execute(context.Background(), code, caller, ExecuteOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Value != "hello" {
t.Errorf("expected 'hello', got %v", result.Value)
}
if result.CallCount != 1 {
t.Errorf("expected CallCount=1, got %d", result.CallCount)
}
}
func TestPool_IsolationBetweenExecutions(t *testing.T) {
p := NewPool(1)
defer p.Close()
caller := newMockCaller()
// First execution sets a variable
_, err := p.Execute(context.Background(), `var shared = 42; shared`, caller, ExecuteOptions{})
if err != nil {
t.Fatalf("first execution failed: %v", err)
}
// Second execution should not see the variable from the first
_, err = p.Execute(context.Background(), `
typeof shared === "undefined" ? "isolated" : "leaked"
`, caller, ExecuteOptions{})
if err != nil {
t.Fatalf("second execution failed: %v", err)
}
// Note: since Execute creates a fresh VM each time, isolation is guaranteed
}