mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 09:28:14 +00:00
- Embed sdk/, example/, meta/, go.mod from homeagent-sdk (no .git)
- Core .gitignore excludes SDK toolchain: bin/, tools/, package/
- RegisterInputChannel + ChannelDef(NoMemory, Cleaner) in SDK
- IOManager input channel registry with GetInputChannelDef
- eventloop: apply channel Cleaner/NoMemory to interrupt text
- context engine: channelDefLookup applied in textForVector
- document store: ChannelCleaner param for archive functions
- All callers/adapters updated with ChannelDef{} default
512 lines
12 KiB
Go
512 lines
12 KiB
Go
package io
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// mockDevice implements Device for testing
|
|
type mockDevice struct {
|
|
name string
|
|
devType DeviceType
|
|
desc string
|
|
tools []ToolDef
|
|
caps OutputCapability
|
|
startFn func() error
|
|
stopFn func() error
|
|
executeFn func(string, map[string]interface{}) (interface{}, error)
|
|
startCallCount int
|
|
stopCallCount int
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (d *mockDevice) Name() string { return d.name }
|
|
func (d *mockDevice) Type() DeviceType { return d.devType }
|
|
func (d *mockDevice) Description() string { return d.desc }
|
|
func (d *mockDevice) Tools() []ToolDef { return d.tools }
|
|
func (d *mockDevice) Start() error {
|
|
d.mu.Lock()
|
|
d.startCallCount++
|
|
d.mu.Unlock()
|
|
if d.startFn != nil {
|
|
return d.startFn()
|
|
}
|
|
return nil
|
|
}
|
|
func (d *mockDevice) Stop() error {
|
|
d.mu.Lock()
|
|
d.stopCallCount++
|
|
d.mu.Unlock()
|
|
if d.stopFn != nil {
|
|
return d.stopFn()
|
|
}
|
|
return nil
|
|
}
|
|
func (d *mockDevice) OutputCapabilities() OutputCapability { return d.caps }
|
|
func (d *mockDevice) ChannelDef() ChannelDef { return ChannelDef{} }
|
|
func (d *mockDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
|
if d.executeFn != nil {
|
|
return d.executeFn(tool, args)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func TestNewIOManager(t *testing.T) {
|
|
m := NewIOManager()
|
|
if m == nil {
|
|
t.Fatal("IOManager should not be nil")
|
|
}
|
|
if m.InputChan() == nil {
|
|
t.Error("InputChan should not be nil")
|
|
}
|
|
if m.OutputChan() == nil {
|
|
t.Error("OutputChan should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestRegisterDevice(t *testing.T) {
|
|
m := NewIOManager()
|
|
dev := &mockDevice{name: "test_dev"}
|
|
if err := m.RegisterDevice(dev); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Duplicate registration
|
|
if err := m.RegisterDevice(dev); err == nil {
|
|
t.Error("expected error for duplicate registration")
|
|
}
|
|
}
|
|
|
|
func TestUnregisterDevice(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{name: "dev1"})
|
|
m.RegisterDevice(&mockDevice{name: "dev2"})
|
|
|
|
m.UnregisterDevice("dev1")
|
|
|
|
devices := m.ListDevices()
|
|
if len(devices) != 1 {
|
|
t.Errorf("expected 1 device, got %d", len(devices))
|
|
}
|
|
if devices[0].Name() != "dev2" {
|
|
t.Errorf("expected 'dev2', got %q", devices[0].Name())
|
|
}
|
|
}
|
|
|
|
func TestUnregisterDeviceTwice(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{name: "dev1"})
|
|
m.UnregisterDevice("dev1")
|
|
m.UnregisterDevice("dev1") // should not panic
|
|
}
|
|
|
|
func TestInjectTextTo(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
if evt.OutputChannel != "speaker" {
|
|
t.Errorf("expected OutputChannel 'speaker', got %q", evt.OutputChannel)
|
|
}
|
|
content, _ := evt.Payload["content"].(string)
|
|
if content != "say hello" {
|
|
t.Errorf("expected 'say hello', got %q", content)
|
|
}
|
|
}()
|
|
m.InjectTextTo("mic", "speaker", "say hello")
|
|
}
|
|
|
|
func TestInjectTextSyncTo(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
if evt.OutputChannel != "qq" {
|
|
t.Errorf("expected OutputChannel 'qq', got %q", evt.OutputChannel)
|
|
}
|
|
evt.ResponseCh <- &OutputEvent{Done: true}
|
|
}()
|
|
|
|
resp := m.InjectTextSyncTo("onebot", "qq", "hi")
|
|
if resp == nil || !resp.Done {
|
|
t.Error("expected Done response")
|
|
}
|
|
}
|
|
|
|
func TestInjectInput(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
if evt.Source != "test_source" {
|
|
t.Errorf("expected source 'test_source', got %q", evt.Source)
|
|
}
|
|
if evt.Type != "text" {
|
|
t.Errorf("expected type 'text', got %q", evt.Type)
|
|
}
|
|
if evt.OutputChannel != "test_source" {
|
|
t.Errorf("expected OutputChannel 'test_source', got %q", evt.OutputChannel)
|
|
}
|
|
}()
|
|
|
|
m.InjectInput("test_source", "text", map[string]interface{}{"content": "hello"})
|
|
}
|
|
|
|
func TestInjectInputSync(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
if evt.OutputChannel != "test" {
|
|
t.Errorf("expected OutputChannel 'test', got %q", evt.OutputChannel)
|
|
}
|
|
evt.ResponseCh <- &OutputEvent{RequestID: evt.RequestID, Done: true}
|
|
}()
|
|
|
|
resp := m.InjectInputSync("test", "text", map[string]interface{}{"content": "sync"})
|
|
if resp == nil {
|
|
t.Fatal("expected response")
|
|
}
|
|
if !resp.Done {
|
|
t.Error("expected Done=true")
|
|
}
|
|
}
|
|
|
|
func TestInjectInputTo(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
if evt.OutputChannel != "speaker" {
|
|
t.Errorf("expected OutputChannel 'speaker', got %q", evt.OutputChannel)
|
|
}
|
|
}()
|
|
|
|
m.InjectInputTo("mic", "speaker", "text", map[string]interface{}{"content": "hello"})
|
|
}
|
|
|
|
func TestInjectInputSyncTo(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
if evt.OutputChannel != "email" {
|
|
t.Errorf("expected OutputChannel 'email', got %q", evt.OutputChannel)
|
|
}
|
|
evt.ResponseCh <- &OutputEvent{Done: true}
|
|
}()
|
|
|
|
resp := m.InjectInputSyncTo("plugin", "email", "text", map[string]interface{}{"content": "hi"})
|
|
if resp == nil || !resp.Done {
|
|
t.Error("expected Done response")
|
|
}
|
|
}
|
|
|
|
func TestEmitOutput(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.OutputChan()
|
|
if evt.Target != "memory" {
|
|
t.Errorf("expected target 'memory', got %q", evt.Target)
|
|
}
|
|
}()
|
|
|
|
m.EmitOutput("memory", "text", map[string]interface{}{"content": "data"})
|
|
}
|
|
|
|
func TestEmitOutputTo(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.OutputChan()
|
|
if evt.OutputChannel != "speaker" {
|
|
t.Errorf("expected channel 'speaker', got %q", evt.OutputChannel)
|
|
}
|
|
}()
|
|
|
|
m.EmitOutputTo("agent", "speaker", "text", map[string]interface{}{"content": "hello"})
|
|
}
|
|
|
|
func TestGetAllTools(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "dev1",
|
|
tools: []ToolDef{
|
|
{Name: "tool1", Description: "first tool"},
|
|
},
|
|
})
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "dev2",
|
|
tools: []ToolDef{
|
|
{Name: "tool2", Description: "second tool"},
|
|
{Name: "tool3", Description: "third tool"},
|
|
},
|
|
})
|
|
|
|
tools := m.GetAllTools()
|
|
if len(tools) != 3 {
|
|
t.Errorf("expected 3 tools, got %d", len(tools))
|
|
}
|
|
}
|
|
|
|
func TestExecuteTool(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "calc",
|
|
tools: []ToolDef{
|
|
{Name: "add", Description: "addition"},
|
|
},
|
|
executeFn: func(tool string, args map[string]interface{}) (interface{}, error) {
|
|
a, _ := args["a"].(float64)
|
|
b, _ := args["b"].(float64)
|
|
return a + b, nil
|
|
},
|
|
})
|
|
|
|
result, err := m.ExecuteTool("add", map[string]interface{}{"a": 1.0, "b": 2.0})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.(float64) != 3.0 {
|
|
t.Errorf("expected 3.0, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestExecuteToolNotFound(t *testing.T) {
|
|
m := NewIOManager()
|
|
_, err := m.ExecuteTool("nonexistent", nil)
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent tool")
|
|
}
|
|
}
|
|
|
|
func TestListChannels(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "screen",
|
|
caps: CapText | CapImage,
|
|
devType: DeviceOutput,
|
|
})
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "mic",
|
|
caps: 0,
|
|
devType: DeviceInput,
|
|
})
|
|
|
|
channels := m.ListChannels()
|
|
if len(channels) != 2 {
|
|
t.Errorf("expected 2 channels, got %d", len(channels))
|
|
}
|
|
}
|
|
|
|
func TestGetChannelCapabilities(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "speaker",
|
|
caps: CapText | CapAudio,
|
|
})
|
|
|
|
caps := m.GetChannelCapabilities("speaker")
|
|
if !caps.Supports(CapText) {
|
|
t.Error("should support text")
|
|
}
|
|
if !caps.Supports(CapAudio) {
|
|
t.Error("should support audio")
|
|
}
|
|
if caps.Supports(CapImage) {
|
|
t.Error("should not support image")
|
|
}
|
|
|
|
caps = m.GetChannelCapabilities("nonexistent")
|
|
if caps != 0 {
|
|
t.Errorf("expected 0 capabilities, got %v", caps)
|
|
}
|
|
}
|
|
|
|
func TestStartAll(t *testing.T) {
|
|
m := NewIOManager()
|
|
started := false
|
|
dev := &mockDevice{
|
|
name: "test",
|
|
startFn: func() error {
|
|
started = true
|
|
return nil
|
|
},
|
|
}
|
|
m.RegisterDevice(dev)
|
|
|
|
if err := m.StartAll(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !started {
|
|
t.Error("device should have been started")
|
|
}
|
|
}
|
|
|
|
func TestStartAllError(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "fail",
|
|
startFn: func() error {
|
|
return nil
|
|
},
|
|
})
|
|
m.RegisterDevice(&mockDevice{
|
|
name: "ok",
|
|
startFn: func() error {
|
|
return nil
|
|
},
|
|
})
|
|
|
|
if err := m.StartAll(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestStopAll(t *testing.T) {
|
|
m := NewIOManager()
|
|
stopped := false
|
|
dev := &mockDevice{
|
|
name: "test",
|
|
stopFn: func() error {
|
|
stopped = true
|
|
return nil
|
|
},
|
|
}
|
|
m.RegisterDevice(dev)
|
|
m.StartAll()
|
|
m.StopAll()
|
|
|
|
if !stopped {
|
|
t.Error("device should have been stopped")
|
|
}
|
|
}
|
|
|
|
func TestAtomicSwapDevices(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{name: "old1"})
|
|
m.RegisterDevice(&mockDevice{name: "old2"})
|
|
|
|
newDevices := map[string]Device{
|
|
"new1": &mockDevice{name: "new1"},
|
|
"new2": &mockDevice{name: "new2"},
|
|
}
|
|
|
|
old := m.AtomicSwapDevices(newDevices)
|
|
if len(old) != 2 {
|
|
t.Errorf("expected 2 old devices, got %d", len(old))
|
|
}
|
|
devices := m.ListDevices()
|
|
if len(devices) != 2 {
|
|
t.Errorf("expected 2 devices after swap, got %d", len(devices))
|
|
}
|
|
}
|
|
|
|
func TestInjectText(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
content, _ := evt.Payload["content"].(string)
|
|
if content != "hello world" {
|
|
t.Errorf("expected 'hello world', got %q", content)
|
|
}
|
|
if evt.OutputChannel != "user" {
|
|
t.Errorf("expected OutputChannel 'user', got %q", evt.OutputChannel)
|
|
}
|
|
}()
|
|
m.InjectText("user", "hello world")
|
|
}
|
|
|
|
func TestInjectTextSync(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.InputChan()
|
|
evt.ResponseCh <- &OutputEvent{Done: true}
|
|
}()
|
|
|
|
resp := m.InjectTextSync("http", "ping")
|
|
if resp == nil || !resp.Done {
|
|
t.Error("expected Done response")
|
|
}
|
|
}
|
|
|
|
func TestEmitText(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.OutputChan()
|
|
content, _ := evt.Payload["content"].(string)
|
|
if content != "notification" {
|
|
t.Errorf("expected 'notification', got %q", content)
|
|
}
|
|
}()
|
|
m.EmitText("user", "notification")
|
|
}
|
|
|
|
func TestEmitTextTo(t *testing.T) {
|
|
m := NewIOManager()
|
|
go func() {
|
|
evt := <-m.OutputChan()
|
|
if evt.OutputChannel != "email" {
|
|
t.Errorf("expected 'email', got %q", evt.OutputChannel)
|
|
}
|
|
content, _ := evt.Payload["content"].(string)
|
|
if content != "alert" {
|
|
t.Errorf("expected 'alert', got %q", content)
|
|
}
|
|
}()
|
|
m.EmitTextTo("agent", "email", "alert")
|
|
}
|
|
|
|
func TestOutputCapability(t *testing.T) {
|
|
caps := CapText | CapImage
|
|
if !caps.Supports(CapText) {
|
|
t.Error("CapText should be supported")
|
|
}
|
|
if !caps.Supports(CapImage) {
|
|
t.Error("CapImage should be supported")
|
|
}
|
|
if caps.Supports(CapAudio) {
|
|
t.Error("CapAudio should not be supported")
|
|
}
|
|
if caps.Supports(CapFile) {
|
|
t.Error("CapFile should not be supported")
|
|
}
|
|
if caps.Supports(CapStructured) {
|
|
t.Error("CapStructured should not be supported")
|
|
}
|
|
}
|
|
|
|
func TestOutputCapabilityString(t *testing.T) {
|
|
caps := CapText | CapAudio
|
|
s := caps.String()
|
|
if s != "[text audio]" && s != "[audio text]" {
|
|
t.Errorf("unexpected string: %s", s)
|
|
}
|
|
|
|
if OutputCapability(0).String() != "[]" {
|
|
t.Errorf("expected empty, got %s", OutputCapability(0).String())
|
|
}
|
|
}
|
|
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
m := NewIOManager()
|
|
m.RegisterDevice(&mockDevice{name: "dev1"})
|
|
m.RegisterDevice(&mockDevice{name: "dev2"})
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
m.GetAllTools()
|
|
m.ListChannels()
|
|
m.ListDevices()
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestNextRequestID(t *testing.T) {
|
|
m := NewIOManager()
|
|
ids := make(map[string]bool)
|
|
for i := 0; i < 100; i++ {
|
|
id := m.nextRequestID()
|
|
if ids[id] {
|
|
t.Errorf("duplicate request ID: %s", id)
|
|
}
|
|
ids[id] = true
|
|
}
|
|
}
|