go/pkg/coredeno/server_test.go
Claude af98accc03
feat(coredeno): Tier 2 bidirectional bridge — Go↔Deno module lifecycle
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>
2026-02-17 22:43:12 +00:00

200 lines
5.5 KiB
Go

package coredeno
import (
"context"
"fmt"
"testing"
pb "forge.lthn.ai/core/go/pkg/coredeno/proto"
"forge.lthn.ai/core/go/pkg/io"
"forge.lthn.ai/core/go/pkg/manifest"
"forge.lthn.ai/core/go/pkg/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// mockProcessRunner implements ProcessRunner for testing.
type mockProcessRunner struct {
started map[string]bool
nextID int
}
func newMockProcessRunner() *mockProcessRunner {
return &mockProcessRunner{started: make(map[string]bool)}
}
func (m *mockProcessRunner) Start(_ context.Context, command string, args ...string) (ProcessHandle, error) {
m.nextID++
id := fmt.Sprintf("proc-%d", m.nextID)
m.started[id] = true
return &mockProcessHandle{id: id}, nil
}
func (m *mockProcessRunner) Kill(id string) error {
if !m.started[id] {
return fmt.Errorf("process not found: %s", id)
}
delete(m.started, id)
return nil
}
type mockProcessHandle struct{ id string }
func (h *mockProcessHandle) Info() ProcessInfo { return ProcessInfo{ID: h.id} }
func newTestServer(t *testing.T) *Server {
t.Helper()
medium := io.NewMockMedium()
medium.Files["./data/test.txt"] = "hello"
st, err := store.New(":memory:")
require.NoError(t, err)
t.Cleanup(func() { st.Close() })
srv := NewServer(medium, st)
srv.RegisterModule(&manifest.Manifest{
Code: "test-mod",
Permissions: manifest.Permissions{
Read: []string{"./data/"},
Write: []string{"./data/"},
},
})
return srv
}
func TestFileRead_Good(t *testing.T) {
srv := newTestServer(t)
resp, err := srv.FileRead(context.Background(), &pb.FileReadRequest{
Path: "./data/test.txt", ModuleCode: "test-mod",
})
require.NoError(t, err)
assert.Equal(t, "hello", resp.Content)
}
func TestFileRead_Bad_PermissionDenied(t *testing.T) {
srv := newTestServer(t)
_, err := srv.FileRead(context.Background(), &pb.FileReadRequest{
Path: "./secrets/key.pem", ModuleCode: "test-mod",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "permission denied")
}
func TestFileRead_Bad_UnknownModule(t *testing.T) {
srv := newTestServer(t)
_, err := srv.FileRead(context.Background(), &pb.FileReadRequest{
Path: "./data/test.txt", ModuleCode: "unknown",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "unknown module")
}
func TestFileWrite_Good(t *testing.T) {
srv := newTestServer(t)
resp, err := srv.FileWrite(context.Background(), &pb.FileWriteRequest{
Path: "./data/new.txt", Content: "world", ModuleCode: "test-mod",
})
require.NoError(t, err)
assert.True(t, resp.Ok)
}
func TestFileWrite_Bad_PermissionDenied(t *testing.T) {
srv := newTestServer(t)
_, err := srv.FileWrite(context.Background(), &pb.FileWriteRequest{
Path: "./secrets/bad.txt", Content: "nope", ModuleCode: "test-mod",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "permission denied")
}
func TestStoreGetSet_Good(t *testing.T) {
srv := newTestServer(t)
ctx := context.Background()
_, err := srv.StoreSet(ctx, &pb.StoreSetRequest{Group: "cfg", Key: "theme", Value: "dark"})
require.NoError(t, err)
resp, err := srv.StoreGet(ctx, &pb.StoreGetRequest{Group: "cfg", Key: "theme"})
require.NoError(t, err)
assert.True(t, resp.Found)
assert.Equal(t, "dark", resp.Value)
}
func TestStoreGet_Good_NotFound(t *testing.T) {
srv := newTestServer(t)
resp, err := srv.StoreGet(context.Background(), &pb.StoreGetRequest{Group: "cfg", Key: "missing"})
require.NoError(t, err)
assert.False(t, resp.Found)
}
func newTestServerWithProcess(t *testing.T) (*Server, *mockProcessRunner) {
t.Helper()
srv := newTestServer(t)
srv.RegisterModule(&manifest.Manifest{
Code: "runner-mod",
Permissions: manifest.Permissions{
Run: []string{"echo", "ls"},
},
})
pr := newMockProcessRunner()
srv.SetProcessRunner(pr)
return srv, pr
}
func TestProcessStart_Good(t *testing.T) {
srv, _ := newTestServerWithProcess(t)
resp, err := srv.ProcessStart(context.Background(), &pb.ProcessStartRequest{
Command: "echo", Args: []string{"hello"}, ModuleCode: "runner-mod",
})
require.NoError(t, err)
assert.NotEmpty(t, resp.ProcessId)
}
func TestProcessStart_Bad_PermissionDenied(t *testing.T) {
srv, _ := newTestServerWithProcess(t)
_, err := srv.ProcessStart(context.Background(), &pb.ProcessStartRequest{
Command: "rm", Args: []string{"-rf", "/"}, ModuleCode: "runner-mod",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "permission denied")
}
func TestProcessStart_Bad_NoProcessService(t *testing.T) {
srv := newTestServer(t)
srv.RegisterModule(&manifest.Manifest{
Code: "no-proc-mod",
Permissions: manifest.Permissions{Run: []string{"echo"}},
})
_, err := srv.ProcessStart(context.Background(), &pb.ProcessStartRequest{
Command: "echo", ModuleCode: "no-proc-mod",
})
assert.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.Unimplemented, st.Code())
}
func TestProcessStop_Good(t *testing.T) {
srv, _ := newTestServerWithProcess(t)
// Start a process first
startResp, err := srv.ProcessStart(context.Background(), &pb.ProcessStartRequest{
Command: "echo", ModuleCode: "runner-mod",
})
require.NoError(t, err)
// Stop it
resp, err := srv.ProcessStop(context.Background(), &pb.ProcessStopRequest{
ProcessId: startResp.ProcessId,
})
require.NoError(t, err)
assert.True(t, resp.Ok)
}
func TestProcessStop_Bad_NotFound(t *testing.T) {
srv, _ := newTestServerWithProcess(t)
_, err := srv.ProcessStop(context.Background(), &pb.ProcessStopRequest{
ProcessId: "nonexistent",
})
assert.Error(t, err)
}