Service.OnStartup now creates sandboxed I/O medium, opens SQLite store, starts gRPC listener on Unix socket, loads .core/view.yml manifest, and launches Deno sidecar with CORE_SOCKET env var. Full shutdown in reverse. New files: listener.go (Unix socket gRPC server), runtime/main.ts (Deno entry point), integration_test.go (full boot with real Deno). 34 tests pass (33 unit + 1 integration). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
183 lines
4.3 KiB
Go
183 lines
4.3 KiB
Go
package coredeno
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
pb "forge.lthn.ai/core/go/pkg/coredeno/proto"
|
|
core "forge.lthn.ai/core/go/pkg/framework/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
func TestNewServiceFactory_Good(t *testing.T) {
|
|
opts := Options{
|
|
DenoPath: "echo",
|
|
SocketPath: "/tmp/test-service.sock",
|
|
}
|
|
c, err := core.New()
|
|
require.NoError(t, err)
|
|
|
|
factory := NewServiceFactory(opts)
|
|
result, err := factory(c)
|
|
require.NoError(t, err)
|
|
|
|
svc, ok := result.(*Service)
|
|
require.True(t, ok)
|
|
assert.NotNil(t, svc.sidecar)
|
|
assert.Equal(t, "echo", svc.sidecar.opts.DenoPath)
|
|
assert.NotNil(t, svc.Core(), "ServiceRuntime should provide Core access")
|
|
assert.Equal(t, opts, svc.Opts(), "ServiceRuntime should provide Options access")
|
|
}
|
|
|
|
func TestService_WithService_Good(t *testing.T) {
|
|
opts := Options{DenoPath: "echo"}
|
|
c, err := core.New(core.WithService(NewServiceFactory(opts)))
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, c)
|
|
}
|
|
|
|
func TestService_Lifecycle_Good(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
sockPath := filepath.Join(tmpDir, "lifecycle.sock")
|
|
|
|
c, err := core.New()
|
|
require.NoError(t, err)
|
|
|
|
factory := NewServiceFactory(Options{
|
|
DenoPath: "echo",
|
|
SocketPath: sockPath,
|
|
})
|
|
result, _ := factory(c)
|
|
svc := result.(*Service)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Verify Startable
|
|
err = svc.OnStartup(ctx)
|
|
assert.NoError(t, err)
|
|
|
|
// Verify Stoppable
|
|
err = svc.OnShutdown(context.Background())
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestService_Sidecar_Good(t *testing.T) {
|
|
c, err := core.New()
|
|
require.NoError(t, err)
|
|
|
|
factory := NewServiceFactory(Options{DenoPath: "echo"})
|
|
result, _ := factory(c)
|
|
svc := result.(*Service)
|
|
|
|
assert.NotNil(t, svc.Sidecar())
|
|
}
|
|
|
|
func TestService_OnStartup_Good(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
sockPath := filepath.Join(tmpDir, "core.sock")
|
|
|
|
// Write a minimal manifest
|
|
coreDir := filepath.Join(tmpDir, ".core")
|
|
require.NoError(t, os.MkdirAll(coreDir, 0755))
|
|
require.NoError(t, os.WriteFile(filepath.Join(coreDir, "view.yml"), []byte(`
|
|
code: test-app
|
|
name: Test App
|
|
version: "1.0"
|
|
permissions:
|
|
read: ["./data/"]
|
|
write: ["./data/"]
|
|
`), 0644))
|
|
|
|
opts := Options{
|
|
DenoPath: "sleep",
|
|
SocketPath: sockPath,
|
|
AppRoot: tmpDir,
|
|
StoreDBPath: ":memory:",
|
|
SidecarArgs: []string{"60"},
|
|
}
|
|
|
|
c, err := core.New()
|
|
require.NoError(t, err)
|
|
|
|
factory := NewServiceFactory(opts)
|
|
result, err := factory(c)
|
|
require.NoError(t, err)
|
|
svc := result.(*Service)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
err = svc.OnStartup(ctx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify socket appeared
|
|
require.Eventually(t, func() bool {
|
|
_, err := os.Stat(sockPath)
|
|
return err == nil
|
|
}, 2*time.Second, 10*time.Millisecond, "gRPC socket should appear after startup")
|
|
|
|
// Verify gRPC responds
|
|
conn, err := grpc.NewClient(
|
|
"unix://"+sockPath,
|
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
|
)
|
|
require.NoError(t, err)
|
|
defer conn.Close()
|
|
|
|
client := pb.NewCoreServiceClient(conn)
|
|
_, err = client.StoreSet(ctx, &pb.StoreSetRequest{
|
|
Group: "boot", Key: "ok", Value: "true",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
resp, err := client.StoreGet(ctx, &pb.StoreGetRequest{
|
|
Group: "boot", Key: "ok",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, resp.Found)
|
|
assert.Equal(t, "true", resp.Value)
|
|
|
|
// Verify sidecar is running
|
|
assert.True(t, svc.sidecar.IsRunning(), "sidecar should be running")
|
|
|
|
// Shutdown
|
|
err = svc.OnShutdown(context.Background())
|
|
assert.NoError(t, err)
|
|
assert.False(t, svc.sidecar.IsRunning(), "sidecar should be stopped")
|
|
}
|
|
|
|
func TestService_OnStartup_Good_NoManifest(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
sockPath := filepath.Join(tmpDir, "core.sock")
|
|
|
|
opts := Options{
|
|
DenoPath: "sleep",
|
|
SocketPath: sockPath,
|
|
AppRoot: tmpDir,
|
|
StoreDBPath: ":memory:",
|
|
}
|
|
|
|
c, err := core.New()
|
|
require.NoError(t, err)
|
|
|
|
factory := NewServiceFactory(opts)
|
|
result, _ := factory(c)
|
|
svc := result.(*Service)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
// Should succeed even without .core/view.yml
|
|
err = svc.OnStartup(ctx)
|
|
require.NoError(t, err)
|
|
|
|
err = svc.OnShutdown(context.Background())
|
|
assert.NoError(t, err)
|
|
}
|