Wire the CoreDeno sidecar into a fully bidirectional bridge: - Deno→Go (gRPC): Deno connects as CoreService client via polyfilled @grpc/grpc-js over Unix socket. Polyfill patches Deno 2.x http2 gaps (getDefaultSettings, pre-connected socket handling, remoteSettings). - Go→Deno (JSON-RPC): Go connects to Deno's newline-delimited JSON-RPC server for module lifecycle (LoadModule, UnloadModule, ModuleStatus). gRPC server direction avoided due to Deno http2.createServer limitations. - ProcessStart/ProcessStop: gRPC handlers delegate to process.Service with manifest permission gating (run permissions). - Deno runtime: main.ts boots DenoService server, connects CoreService client with retry + health-check round-trip, handles SIGTERM shutdown. 40 unit tests + 2 integration tests (Tier 1 boot + Tier 2 bidirectional). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
124 lines
2.7 KiB
Go
124 lines
2.7 KiB
Go
package coredeno
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestStart_Good(t *testing.T) {
|
|
sockDir := t.TempDir()
|
|
sc := NewSidecar(Options{
|
|
DenoPath: "sleep",
|
|
SocketPath: filepath.Join(sockDir, "test.sock"),
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
err := sc.Start(ctx, "10") // sleep 10 — will be killed by Stop
|
|
require.NoError(t, err)
|
|
assert.True(t, sc.IsRunning())
|
|
|
|
err = sc.Stop()
|
|
require.NoError(t, err)
|
|
assert.False(t, sc.IsRunning())
|
|
}
|
|
|
|
func TestStop_Good_NotStarted(t *testing.T) {
|
|
sc := NewSidecar(Options{DenoPath: "sleep"})
|
|
err := sc.Stop()
|
|
assert.NoError(t, err, "stopping a not-started sidecar should be a no-op")
|
|
}
|
|
|
|
func TestStart_Good_EnvPassedToChild(t *testing.T) {
|
|
sockDir := t.TempDir()
|
|
sockPath := filepath.Join(sockDir, "test.sock")
|
|
|
|
sc := NewSidecar(Options{
|
|
DenoPath: "sleep",
|
|
SocketPath: sockPath,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
err := sc.Start(ctx, "10")
|
|
require.NoError(t, err)
|
|
defer sc.Stop()
|
|
|
|
// Verify the child process has CORE_SOCKET in its environment
|
|
sc.mu.RLock()
|
|
env := sc.cmd.Env
|
|
sc.mu.RUnlock()
|
|
|
|
found := false
|
|
expected := "CORE_SOCKET=" + sockPath
|
|
for _, e := range env {
|
|
if e == expected {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "child process should receive CORE_SOCKET=%s", sockPath)
|
|
}
|
|
|
|
func TestStart_Good_DenoSocketEnv(t *testing.T) {
|
|
sockDir := t.TempDir()
|
|
sockPath := filepath.Join(sockDir, "core.sock")
|
|
denoSockPath := filepath.Join(sockDir, "deno.sock")
|
|
|
|
sc := NewSidecar(Options{
|
|
DenoPath: "sleep",
|
|
SocketPath: sockPath,
|
|
DenoSocketPath: denoSockPath,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
err := sc.Start(ctx, "10")
|
|
require.NoError(t, err)
|
|
defer sc.Stop()
|
|
|
|
sc.mu.RLock()
|
|
env := sc.cmd.Env
|
|
sc.mu.RUnlock()
|
|
|
|
foundCore := false
|
|
foundDeno := false
|
|
for _, e := range env {
|
|
if e == "CORE_SOCKET="+sockPath {
|
|
foundCore = true
|
|
}
|
|
if e == "DENO_SOCKET="+denoSockPath {
|
|
foundDeno = true
|
|
}
|
|
}
|
|
assert.True(t, foundCore, "child should receive CORE_SOCKET")
|
|
assert.True(t, foundDeno, "child should receive DENO_SOCKET")
|
|
}
|
|
|
|
func TestSocketDirCreated_Good(t *testing.T) {
|
|
dir := t.TempDir()
|
|
sockPath := filepath.Join(dir, "sub", "deno.sock")
|
|
sc := NewSidecar(Options{
|
|
DenoPath: "sleep",
|
|
SocketPath: sockPath,
|
|
})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
err := sc.Start(ctx, "10")
|
|
require.NoError(t, err)
|
|
defer sc.Stop()
|
|
|
|
_, err = os.Stat(filepath.Join(dir, "sub"))
|
|
assert.NoError(t, err, "socket directory should be created")
|
|
}
|