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
257 lines
5.4 KiB
Go
257 lines
5.4 KiB
Go
package plugin
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/orca/orca/pkg/bus"
|
|
)
|
|
|
|
// mockPlugin implements Plugin for testing.
|
|
type mockPlugin struct {
|
|
name string
|
|
version string
|
|
initFn func(host PluginHost) error
|
|
closeFn func() error
|
|
}
|
|
|
|
func (m *mockPlugin) Name() string { return m.name }
|
|
func (m *mockPlugin) Version() string { return m.version }
|
|
func (m *mockPlugin) Init(host PluginHost) error {
|
|
if m.initFn != nil {
|
|
return m.initFn(host)
|
|
}
|
|
return nil
|
|
}
|
|
func (m *mockPlugin) Shutdown() error {
|
|
if m.closeFn != nil {
|
|
return m.closeFn()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestRegistryNew(t *testing.T) {
|
|
r := NewRegistry()
|
|
if r == nil {
|
|
t.Fatal("NewRegistry() returned nil")
|
|
}
|
|
if n := r.Count(); n != 0 {
|
|
t.Errorf("expected empty registry, got %d plugins", n)
|
|
}
|
|
}
|
|
|
|
func TestRegistryRegister(t *testing.T) {
|
|
r := NewRegistry()
|
|
p := &mockPlugin{name: "test", version: "1.0.0"}
|
|
|
|
err := r.Register(p)
|
|
if err != nil {
|
|
t.Fatalf("Register failed: %v", err)
|
|
}
|
|
|
|
if n := r.Count(); n != 1 {
|
|
t.Errorf("expected 1 plugin, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestRegistryRegisterDuplicate(t *testing.T) {
|
|
r := NewRegistry()
|
|
p1 := &mockPlugin{name: "test", version: "1.0.0"}
|
|
p2 := &mockPlugin{name: "test", version: "2.0.0"}
|
|
|
|
r.Register(p1)
|
|
err := r.Register(p2)
|
|
if err == nil {
|
|
t.Error("expected error registering duplicate plugin")
|
|
}
|
|
}
|
|
|
|
func TestRegistryGet(t *testing.T) {
|
|
r := NewRegistry()
|
|
p := &mockPlugin{name: "test", version: "1.0.0"}
|
|
r.Register(p)
|
|
|
|
got, ok := r.Get("test")
|
|
if !ok {
|
|
t.Fatal("Get returned not found")
|
|
}
|
|
if got.Name() != "test" {
|
|
t.Errorf("expected name 'test', got %q", got.Name())
|
|
}
|
|
}
|
|
|
|
func TestRegistryGetNotFound(t *testing.T) {
|
|
r := NewRegistry()
|
|
_, ok := r.Get("nonexistent")
|
|
if ok {
|
|
t.Error("expected false for nonexistent plugin")
|
|
}
|
|
}
|
|
|
|
func TestRegistryUnregister(t *testing.T) {
|
|
r := NewRegistry()
|
|
p := &mockPlugin{name: "test", version: "1.0.0"}
|
|
r.Register(p)
|
|
|
|
err := r.Unregister("test")
|
|
if err != nil {
|
|
t.Fatalf("Unregister failed: %v", err)
|
|
}
|
|
|
|
if n := r.Count(); n != 0 {
|
|
t.Errorf("expected 0 plugins, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestRegistryUnregisterNotFound(t *testing.T) {
|
|
r := NewRegistry()
|
|
err := r.Unregister("nonexistent")
|
|
if err == nil {
|
|
t.Error("expected error unregistering nonexistent plugin")
|
|
}
|
|
}
|
|
|
|
func TestRegistryList(t *testing.T) {
|
|
r := NewRegistry()
|
|
r.Register(&mockPlugin{name: "a", version: "1.0.0"})
|
|
r.Register(&mockPlugin{name: "b", version: "1.0.0"})
|
|
r.Register(&mockPlugin{name: "c", version: "1.0.0"})
|
|
|
|
plugins := r.List()
|
|
if len(plugins) != 3 {
|
|
t.Errorf("expected 3 plugins, got %d", len(plugins))
|
|
}
|
|
|
|
names := make(map[string]bool)
|
|
for _, p := range plugins {
|
|
names[p.Name()] = true
|
|
}
|
|
|
|
for _, n := range []string{"a", "b", "c"} {
|
|
if !names[n] {
|
|
t.Errorf("missing plugin %q in list", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryState(t *testing.T) {
|
|
r := NewRegistry()
|
|
p := &mockPlugin{name: "test", version: "1.0.0"}
|
|
r.Register(p)
|
|
|
|
if s := r.State("test"); s != StateRegistered {
|
|
t.Errorf("expected StateRegistered, got %s", s)
|
|
}
|
|
|
|
r.SetState("test", StateRunning)
|
|
if s := r.State("test"); s != StateRunning {
|
|
t.Errorf("expected StateRunning, got %s", s)
|
|
}
|
|
}
|
|
|
|
func TestRegistryStateUnknown(t *testing.T) {
|
|
r := NewRegistry()
|
|
if s := r.State("nonexistent"); s != StateUnknown {
|
|
t.Errorf("expected StateUnknown for nonexistent, got %s", s)
|
|
}
|
|
}
|
|
|
|
func TestRegistrySetStateNoOp(t *testing.T) {
|
|
r := NewRegistry()
|
|
r.SetState("nonexistent", StateRunning)
|
|
if n := r.Count(); n != 0 {
|
|
t.Errorf("SetState should not add plugins")
|
|
}
|
|
}
|
|
|
|
func TestPluginStateString(t *testing.T) {
|
|
tests := []struct {
|
|
state PluginState
|
|
want string
|
|
}{
|
|
{StateUnknown, "unknown"},
|
|
{StateRegistered, "registered"},
|
|
{StateInitialized, "initialized"},
|
|
{StateRunning, "running"},
|
|
{StateStopped, "stopped"},
|
|
{StateError, "error"},
|
|
{PluginState(99), "unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
if got := tt.state.String(); got != tt.want {
|
|
t.Errorf("PluginState(%d).String() = %q, want %q", tt.state, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRegistryConcurrent(t *testing.T) {
|
|
r := NewRegistry()
|
|
done := make(chan struct{}, 2)
|
|
|
|
go func() {
|
|
for i := 0; i < 100; i++ {
|
|
r.Register(&mockPlugin{name: "a", version: "1.0.0"})
|
|
r.Get("a")
|
|
r.Unregister("a")
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
go func() {
|
|
for i := 0; i < 100; i++ {
|
|
r.Register(&mockPlugin{name: "b", version: "1.0.0"})
|
|
r.List()
|
|
r.State("b")
|
|
r.Unregister("b")
|
|
}
|
|
done <- struct{}{}
|
|
}()
|
|
|
|
<-done
|
|
<-done
|
|
}
|
|
|
|
// mockPluginHost implements PluginHost for testing kernel-level plugin init.
|
|
type mockPluginHost struct{}
|
|
|
|
func (m *mockPluginHost) Bus() bus.MessageBus { return nil }
|
|
func (m *mockPluginHost) GetPlugin(name string) (Plugin, bool) { return nil, false }
|
|
func (m *mockPluginHost) ListPlugins() []Plugin { return nil }
|
|
|
|
func TestPluginInitAndShutdown(t *testing.T) {
|
|
var initCalled, shutdownCalled bool
|
|
|
|
p := &mockPlugin{
|
|
name: "test",
|
|
version: "1.0.0",
|
|
initFn: func(host PluginHost) error {
|
|
initCalled = true
|
|
if host == nil {
|
|
return errors.New("host is nil")
|
|
}
|
|
return nil
|
|
},
|
|
closeFn: func() error {
|
|
shutdownCalled = true
|
|
return nil
|
|
},
|
|
}
|
|
|
|
host := &mockPluginHost{}
|
|
|
|
if err := p.Init(host); err != nil {
|
|
t.Fatalf("Init failed: %v", err)
|
|
}
|
|
if !initCalled {
|
|
t.Error("Init function was not called")
|
|
}
|
|
|
|
if err := p.Shutdown(); err != nil {
|
|
t.Fatalf("Shutdown failed: %v", err)
|
|
}
|
|
if !shutdownCalled {
|
|
t.Error("Shutdown function was not called")
|
|
}
|
|
}
|