Core features: - Microkernel architecture with Actor model - Session management with JSONL persistence - Tool system (5 built-in tools) - Skill system with SKILL.md parsing - Sandbox security execution - Ollama integration with gemma4:e4b - Prompt-based tool calling (compatible with native function calling) - REPL interface 11 packages, all tests passing
344 lines
6.5 KiB
Go
344 lines
6.5 KiB
Go
package kernel
|
|
|
|
import (
|
|
"errors"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/orca/orca/pkg/bus"
|
|
"github.com/orca/orca/pkg/plugin"
|
|
)
|
|
|
|
// testPlugin implements Plugin for kernel testing.
|
|
type testPlugin struct {
|
|
name string
|
|
version string
|
|
initFn func(host plugin.PluginHost) error
|
|
closeFn func() error
|
|
}
|
|
|
|
func (p *testPlugin) Name() string { return p.name }
|
|
func (p *testPlugin) Version() string { return p.version }
|
|
func (p *testPlugin) Init(host plugin.PluginHost) error {
|
|
if p.initFn != nil {
|
|
return p.initFn(host)
|
|
}
|
|
return nil
|
|
}
|
|
func (p *testPlugin) Shutdown() error {
|
|
if p.closeFn != nil {
|
|
return p.closeFn()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestNewKernel(t *testing.T) {
|
|
k := New()
|
|
if k == nil {
|
|
t.Fatal("New() returned nil")
|
|
}
|
|
if k.Bus() == nil {
|
|
t.Error("Bus() returned nil")
|
|
}
|
|
if k.Registry() == nil {
|
|
t.Error("Registry() returned nil")
|
|
}
|
|
}
|
|
|
|
func TestKernelStartStop(t *testing.T) {
|
|
k := New()
|
|
|
|
if err := k.Start(); err != nil {
|
|
t.Fatalf("Start failed: %v", err)
|
|
}
|
|
if !k.IsRunning() {
|
|
t.Error("expected kernel running after Start")
|
|
}
|
|
|
|
if err := k.Stop(); err != nil {
|
|
t.Fatalf("Stop failed: %v", err)
|
|
}
|
|
if k.IsRunning() {
|
|
t.Error("expected kernel stopped after Stop")
|
|
}
|
|
}
|
|
|
|
func TestKernelDoubleStart(t *testing.T) {
|
|
k := New()
|
|
k.Start()
|
|
err := k.Start()
|
|
if err == nil {
|
|
t.Error("expected error on double start")
|
|
}
|
|
k.Stop()
|
|
}
|
|
|
|
func TestKernelRegisterPlugin(t *testing.T) {
|
|
k := New()
|
|
p := &testPlugin{name: "test", version: "1.0.0"}
|
|
|
|
err := k.RegisterPlugin(p)
|
|
if err != nil {
|
|
t.Fatalf("RegisterPlugin failed: %v", err)
|
|
}
|
|
|
|
got, ok := k.GetPlugin("test")
|
|
if !ok {
|
|
t.Fatal("GetPlugin returned not found")
|
|
}
|
|
if got.Name() != "test" {
|
|
t.Errorf("expected name 'test', got %q", got.Name())
|
|
}
|
|
}
|
|
|
|
func TestKernelRegisterPluginAfterStart(t *testing.T) {
|
|
k := New()
|
|
k.Start()
|
|
defer k.Stop()
|
|
|
|
err := k.RegisterPlugin(&testPlugin{name: "test", version: "1.0.0"})
|
|
if err == nil {
|
|
t.Error("expected error registering plugin after start")
|
|
}
|
|
}
|
|
|
|
func TestKernelPluginLifecycle(t *testing.T) {
|
|
k := New()
|
|
|
|
var initCount int32
|
|
var shutdownCount int32
|
|
|
|
p := &testPlugin{
|
|
name: "lifecycle",
|
|
version: "1.0.0",
|
|
initFn: func(host plugin.PluginHost) error {
|
|
atomic.AddInt32(&initCount, 1)
|
|
return nil
|
|
},
|
|
closeFn: func() error {
|
|
atomic.AddInt32(&shutdownCount, 1)
|
|
return nil
|
|
},
|
|
}
|
|
|
|
k.RegisterPlugin(p)
|
|
k.Start()
|
|
|
|
if n := atomic.LoadInt32(&initCount); n != 1 {
|
|
t.Errorf("expected init called once, got %d", n)
|
|
}
|
|
|
|
k.Stop()
|
|
|
|
if n := atomic.LoadInt32(&shutdownCount); n != 1 {
|
|
t.Errorf("expected shutdown called once, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestKernelPluginInitFailure(t *testing.T) {
|
|
k := New()
|
|
|
|
p := &testPlugin{
|
|
name: "failing",
|
|
version: "1.0.0",
|
|
initFn: func(host plugin.PluginHost) error {
|
|
return errors.New("init failed")
|
|
},
|
|
}
|
|
|
|
k.RegisterPlugin(p)
|
|
|
|
// Init failure should not prevent Start from succeeding (graceful degradation)
|
|
err := k.Start()
|
|
if err != nil {
|
|
t.Fatalf("Start should succeed even with failing plugin: %v", err)
|
|
}
|
|
k.Stop()
|
|
}
|
|
|
|
func TestKernelPluginShutdownFailure(t *testing.T) {
|
|
k := New()
|
|
|
|
p := &testPlugin{
|
|
name: "failing-shutdown",
|
|
version: "1.0.0",
|
|
initFn: func(host plugin.PluginHost) error {
|
|
return nil
|
|
},
|
|
closeFn: func() error {
|
|
return errors.New("shutdown failed")
|
|
},
|
|
}
|
|
|
|
k.RegisterPlugin(p)
|
|
k.Start()
|
|
|
|
// Shutdown failure should not prevent Stop from succeeding
|
|
err := k.Stop()
|
|
if err != nil {
|
|
t.Fatalf("Stop should succeed even with failing plugin shutdown: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestKernelMultiplePlugins(t *testing.T) {
|
|
k := New()
|
|
|
|
names := []string{"alpha", "beta", "gamma"}
|
|
for _, name := range names {
|
|
k.RegisterPlugin(&testPlugin{name: name, version: "1.0.0"})
|
|
}
|
|
|
|
k.Start()
|
|
|
|
plugins := k.ListPlugins()
|
|
if len(plugins) != len(names) {
|
|
t.Errorf("expected %d plugins, got %d", len(names), len(plugins))
|
|
}
|
|
|
|
k.Stop()
|
|
}
|
|
|
|
func TestKernelUnregisterPlugin(t *testing.T) {
|
|
k := New()
|
|
|
|
k.RegisterPlugin(&testPlugin{name: "remove-me", version: "1.0.0"})
|
|
|
|
err := k.UnregisterPlugin("remove-me")
|
|
if err != nil {
|
|
t.Fatalf("UnregisterPlugin failed: %v", err)
|
|
}
|
|
|
|
_, ok := k.GetPlugin("remove-me")
|
|
if ok {
|
|
t.Error("plugin should not exist after unregister")
|
|
}
|
|
}
|
|
|
|
func TestKernelStopWithoutStart(t *testing.T) {
|
|
k := New()
|
|
err := k.Stop()
|
|
if err != nil {
|
|
t.Fatalf("Stop without Start should be a no-op: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestKernelPluginReceivesHost(t *testing.T) {
|
|
k := New()
|
|
|
|
var gotHost plugin.PluginHost
|
|
p := &testPlugin{
|
|
name: "host-check",
|
|
version: "1.0.0",
|
|
initFn: func(host plugin.PluginHost) error {
|
|
gotHost = host
|
|
return nil
|
|
},
|
|
}
|
|
|
|
k.RegisterPlugin(p)
|
|
k.Start()
|
|
|
|
if gotHost == nil {
|
|
t.Fatal("plugin did not receive PluginHost")
|
|
}
|
|
|
|
// Verify the host can access bus
|
|
if gotHost.Bus() == nil {
|
|
t.Error("PluginHost.Bus() returned nil")
|
|
}
|
|
|
|
// Verify plugin discovery through host
|
|
p2, ok := gotHost.GetPlugin("host-check")
|
|
if !ok {
|
|
t.Error("PluginHost.GetPlugin should find itself")
|
|
}
|
|
if p2.Name() != "host-check" {
|
|
t.Errorf("expected name 'host-check', got %q", p2.Name())
|
|
}
|
|
|
|
k.Stop()
|
|
}
|
|
|
|
func TestKernelAllPluginsInitialized(t *testing.T) {
|
|
k := New()
|
|
|
|
names := []string{"a", "b", "c"}
|
|
initialized := make(map[string]bool)
|
|
|
|
for _, name := range names {
|
|
n := name
|
|
k.RegisterPlugin(&testPlugin{
|
|
name: n,
|
|
initFn: func(host plugin.PluginHost) error {
|
|
initialized[n] = true
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
k.Start()
|
|
|
|
for _, name := range names {
|
|
if !initialized[name] {
|
|
t.Errorf("plugin %q was not initialized", name)
|
|
}
|
|
}
|
|
|
|
k.Stop()
|
|
}
|
|
|
|
func TestKernelShutdownAllPlugins(t *testing.T) {
|
|
k := New()
|
|
|
|
names := []string{"x", "y", "z"}
|
|
shutdown := make(map[string]bool)
|
|
|
|
for _, name := range names {
|
|
n := name
|
|
k.RegisterPlugin(&testPlugin{
|
|
name: n,
|
|
initFn: func(host plugin.PluginHost) error {
|
|
return nil
|
|
},
|
|
closeFn: func() error {
|
|
shutdown[n] = true
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
k.Start()
|
|
k.Stop()
|
|
|
|
for _, name := range names {
|
|
if !shutdown[name] {
|
|
t.Errorf("plugin %q was not shut down", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestKernelMessageBusIntegration(t *testing.T) {
|
|
k := New()
|
|
k.Start()
|
|
defer k.Stop()
|
|
|
|
mb := k.Bus()
|
|
|
|
var received int32
|
|
sub, err := mb.Subscribe("kernel-test", func(msg bus.Message) {
|
|
atomic.AddInt32(&received, 1)
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Subscribe failed: %v", err)
|
|
}
|
|
defer sub.Unsubscribe()
|
|
|
|
mb.Publish("kernel-test", bus.Message{ID: "test-msg"})
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
if n := atomic.LoadInt32(&received); n != 1 {
|
|
t.Errorf("expected 1 message via kernel bus, got %d", n)
|
|
}
|
|
}
|