Files
HomeAgent/internal/agent/io/channel_test.go
root a899d777c3 sdk: embed non-toolchain SDK in third_party, add NoMemory/Cleaner support
- 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
2026-07-29 14:48:23 +08:00

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