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") } }