- internal/plugin: Plugin/IoTProvider/ToolProvider/RAGProvider interfaces - internal/plugin: Registry with type-safe auto-detection, 11 tests - plugins/iot-mock: 8 simulated smart home devices, 11 tests - 63 total tests, build + vet clean
302 lines
7.6 KiB
Go
302 lines
7.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
|
)
|
|
|
|
// --- Test helpers: mock plugins ---
|
|
|
|
type mockBasePlugin struct {
|
|
name string
|
|
version string
|
|
desc string
|
|
enabled bool
|
|
initErr error
|
|
closeCalled bool
|
|
}
|
|
|
|
func (m *mockBasePlugin) Name() string { return m.name }
|
|
func (m *mockBasePlugin) Version() string { return m.version }
|
|
func (m *mockBasePlugin) Description() string { return m.desc }
|
|
func (m *mockBasePlugin) Init(_ context.Context, _ PluginConfig) error {
|
|
return m.initErr
|
|
}
|
|
func (m *mockBasePlugin) Close() error {
|
|
m.closeCalled = true
|
|
return nil
|
|
}
|
|
func (m *mockBasePlugin) Enabled() bool { return m.enabled }
|
|
|
|
type mockIoTPlugin struct {
|
|
mockBasePlugin
|
|
devices []Device
|
|
mu sync.Mutex
|
|
eventCh chan DeviceEvent
|
|
}
|
|
|
|
func (m *mockIoTPlugin) ListDevices(_ context.Context) ([]Device, error) {
|
|
return m.devices, nil
|
|
}
|
|
func (m *mockIoTPlugin) GetDevice(_ context.Context, id string) (*Device, error) {
|
|
for _, d := range m.devices {
|
|
if d.ID == id {
|
|
return &d, nil
|
|
}
|
|
}
|
|
return nil, errors.NewNotFound("device " + id)
|
|
}
|
|
func (m *mockIoTPlugin) SetDevice(_ context.Context, id, field string, value any) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
for i, d := range m.devices {
|
|
if d.ID == id {
|
|
if m.devices[i].State == nil {
|
|
m.devices[i].State = make(map[string]any)
|
|
}
|
|
m.devices[i].State[field] = value
|
|
if m.eventCh != nil {
|
|
m.eventCh <- DeviceEvent{DeviceID: id, Field: field, NewValue: value}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
func (m *mockIoTPlugin) ToggleDevice(_ context.Context, id string) error {
|
|
return m.SetDevice(context.Background(), id, "power", true)
|
|
}
|
|
func (m *mockIoTPlugin) SubscribeChanges(_ context.Context) (<-chan DeviceEvent, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.eventCh = make(chan DeviceEvent, 10)
|
|
return m.eventCh, nil
|
|
}
|
|
|
|
type mockToolPlugin struct {
|
|
mockBasePlugin
|
|
tools []ToolDefinition
|
|
execFunc func(ctx context.Context, toolName string, params map[string]any) (string, error)
|
|
}
|
|
|
|
func (m *mockToolPlugin) Tools() []ToolDefinition { return m.tools }
|
|
func (m *mockToolPlugin) Execute(ctx context.Context, toolName string, params map[string]any) (string, error) {
|
|
if m.execFunc != nil {
|
|
return m.execFunc(ctx, toolName, params)
|
|
}
|
|
return "ok", nil
|
|
}
|
|
|
|
// --- Tests ---
|
|
|
|
func TestRegistryRegister(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockBasePlugin{name: "test", version: "1.0", enabled: true}
|
|
|
|
err := reg.Register(p, PluginConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Register failed: %v", err)
|
|
}
|
|
|
|
if reg.Count() != 1 {
|
|
t.Errorf("expected 1 plugin, got %d", reg.Count())
|
|
}
|
|
|
|
got := reg.Get("test")
|
|
if got == nil {
|
|
t.Error("expected to find plugin 'test'")
|
|
}
|
|
}
|
|
|
|
func TestRegistryDuplicateRegister(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p1 := &mockBasePlugin{name: "test", version: "1.0", enabled: true}
|
|
p2 := &mockBasePlugin{name: "test", version: "2.0", enabled: true}
|
|
|
|
_ = reg.Register(p1, PluginConfig{})
|
|
err := reg.Register(p2, PluginConfig{})
|
|
|
|
if err == nil {
|
|
t.Error("expected error for duplicate plugin name")
|
|
}
|
|
}
|
|
|
|
func TestRegistryInitError(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockBasePlugin{name: "bad", version: "1.0", enabled: true, initErr: fmt.Errorf("init error")}
|
|
|
|
err := reg.Register(p, PluginConfig{})
|
|
if err == nil {
|
|
t.Error("expected error for init failure")
|
|
}
|
|
if reg.Count() != 0 {
|
|
t.Errorf("plugin should not be registered on init error, got %d", reg.Count())
|
|
}
|
|
}
|
|
|
|
func TestRegistryDisabledPlugin(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockBasePlugin{name: "disabled", version: "1.0", enabled: false}
|
|
|
|
err := reg.Register(p, PluginConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Register should not error for disabled plugin: %v", err)
|
|
}
|
|
if reg.Count() != 0 {
|
|
t.Errorf("disabled plugin should not be counted, got %d", reg.Count())
|
|
}
|
|
}
|
|
|
|
func TestRegistryListNames(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p1 := &mockBasePlugin{name: "plugin-a", version: "1.0", enabled: true}
|
|
p2 := &mockBasePlugin{name: "plugin-b", version: "1.0", enabled: true}
|
|
|
|
reg.Register(p1, PluginConfig{})
|
|
reg.Register(p2, PluginConfig{})
|
|
|
|
names := reg.ListNames()
|
|
if len(names) != 2 {
|
|
t.Errorf("expected 2 names, got %d", len(names))
|
|
}
|
|
}
|
|
|
|
func TestRegistryIoTProvider(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockIoTPlugin{
|
|
mockBasePlugin: mockBasePlugin{name: "iot-mock", version: "1.0", enabled: true},
|
|
devices: []Device{
|
|
{ID: "light-1", Name: "客厅灯", Type: DeviceLight, State: map[string]any{"power": false}},
|
|
},
|
|
}
|
|
|
|
err := reg.Register(p, PluginConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Register failed: %v", err)
|
|
}
|
|
|
|
iot := reg.GetIoTProvider()
|
|
if iot == nil {
|
|
t.Fatal("expected IoT provider to be available")
|
|
}
|
|
|
|
devices, err := iot.ListDevices(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("ListDevices failed: %v", err)
|
|
}
|
|
if len(devices) != 1 {
|
|
t.Errorf("expected 1 device, got %d", len(devices))
|
|
}
|
|
if devices[0].Name != "客厅灯" {
|
|
t.Errorf("expected device name 客厅灯, got %s", devices[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestRegistrySetDevice(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockIoTPlugin{
|
|
mockBasePlugin: mockBasePlugin{name: "iot-mock", version: "1.0", enabled: true},
|
|
devices: []Device{
|
|
{ID: "light-1", Name: "客厅灯", Type: DeviceLight, State: map[string]any{"power": false}},
|
|
},
|
|
}
|
|
reg.Register(p, PluginConfig{})
|
|
|
|
iot := reg.GetIoTProvider()
|
|
err := iot.SetDevice(context.Background(), "light-1", "power", true)
|
|
if err != nil {
|
|
t.Fatalf("SetDevice failed: %v", err)
|
|
}
|
|
|
|
d, err := iot.GetDevice(context.Background(), "light-1")
|
|
if err != nil {
|
|
t.Fatalf("GetDevice failed: %v", err)
|
|
}
|
|
if d.State["power"] != true {
|
|
t.Errorf("expected power=true, got %v", d.State["power"])
|
|
}
|
|
}
|
|
|
|
func TestRegistryToolProvider(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockToolPlugin{
|
|
mockBasePlugin: mockBasePlugin{name: "calculator", version: "1.0", enabled: true},
|
|
tools: []ToolDefinition{
|
|
{Name: "calc", Description: "Calculate math expressions"},
|
|
},
|
|
}
|
|
|
|
err := reg.Register(p, PluginConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Register failed: %v", err)
|
|
}
|
|
|
|
providers := reg.GetAllToolProviders()
|
|
if len(providers) != 1 {
|
|
t.Errorf("expected 1 tool provider, got %d", len(providers))
|
|
}
|
|
|
|
defs := reg.GetAllToolDefinitions()
|
|
if len(defs) != 1 {
|
|
t.Errorf("expected 1 tool definition, got %d", len(defs))
|
|
}
|
|
if defs[0].Name != "calc" {
|
|
t.Errorf("expected tool name 'calc', got %s", defs[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestRegistryExecuteTool(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p := &mockToolPlugin{
|
|
mockBasePlugin: mockBasePlugin{name: "calc", version: "1.0", enabled: true},
|
|
tools: []ToolDefinition{
|
|
{Name: "add", Description: "Add two numbers"},
|
|
},
|
|
execFunc: func(_ context.Context, name string, params map[string]any) (string, error) {
|
|
return "42", nil
|
|
},
|
|
}
|
|
reg.Register(p, PluginConfig{})
|
|
|
|
result, err := reg.ExecuteTool(context.Background(), "add", nil)
|
|
if err != nil {
|
|
t.Fatalf("ExecuteTool failed: %v", err)
|
|
}
|
|
if result != "42" {
|
|
t.Errorf("expected '42', got %s", result)
|
|
}
|
|
}
|
|
|
|
func TestRegistryExecuteToolNotFound(t *testing.T) {
|
|
reg := NewRegistry()
|
|
|
|
_, err := reg.ExecuteTool(context.Background(), "nonexistent", nil)
|
|
if err == nil {
|
|
t.Error("expected error for unknown tool")
|
|
}
|
|
}
|
|
|
|
func TestRegistryShutdown(t *testing.T) {
|
|
reg := NewRegistry()
|
|
p1 := &mockBasePlugin{name: "p1", version: "1.0", enabled: true}
|
|
p2 := &mockToolPlugin{
|
|
mockBasePlugin: mockBasePlugin{name: "p2", version: "1.0", enabled: true},
|
|
}
|
|
|
|
reg.Register(p1, PluginConfig{})
|
|
reg.Register(p2, PluginConfig{})
|
|
|
|
reg.Shutdown()
|
|
|
|
if !p1.closeCalled {
|
|
t.Error("p1.Close should have been called")
|
|
}
|
|
if !p2.closeCalled {
|
|
t.Error("p2.Close should have been called")
|
|
}
|
|
}
|