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>
200 lines
5.5 KiB
Go
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)
|
|
}
|