根因:sdk.Channel.Connect 为 2 参 (ctx, config any),adapter.PlatformAdapter.Connect 为 1 参 (ctx),Go 无重载 → 4 个渠道(qq-obv11/weixin-ilink/telegram/webhook)只实现 1 参 Connect,类型断言 cap.Value.(sdk.Channel) 恒失败 → /api/v1/admin/channels 恒返回 []。 收敛方向:sdk.Channel.Connect 改 1 参 (ctx),与 PlatformAdapter 生命周期子集一致; 4 个真实适配器自动满足新接口,adminListChannels 枚举成功。最小改动(不改渠道实现、 不引依赖)。同步修正 sdk_test/registry_test/extra_test 的 mock。
316 lines
9.3 KiB
Go
316 lines
9.3 KiB
Go
package registry
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
)
|
|
|
|
// regTool is a mock Tool for registry tests.
|
|
type regTool struct{ name string }
|
|
|
|
func (t regTool) Name() string { return t.name }
|
|
func (t regTool) Description() string { return "desc " + t.name }
|
|
func (t regTool) Parameters() (map[string]any, error) { return map[string]any{}, nil }
|
|
func (t regTool) Execute(_ context.Context, _ any) (any, error) { return nil, nil }
|
|
|
|
// regSkill is a mock Skill for registry tests.
|
|
type regSkill struct{ name string }
|
|
|
|
func (s regSkill) Name() string { return s.name }
|
|
func (s regSkill) Description() string { return "desc " + s.name }
|
|
func (s regSkill) Prompt() string { return "prompt" }
|
|
func (s regSkill) Steps() []sdk.Step { return []sdk.Step{{Name: "s1", Prompt: "p1"}} }
|
|
func (s regSkill) Refs() []sdk.Ref { return nil }
|
|
func (s regSkill) Guardrails() []sdk.Guardrail { return nil }
|
|
|
|
// regChannel is a mock Channel for registry tests.
|
|
type regChannel struct{ name string }
|
|
|
|
func (c regChannel) Name() string { return c.name }
|
|
func (c regChannel) Connect(_ context.Context) error { return nil }
|
|
func (c regChannel) Disconnect(_ context.Context) error { return nil }
|
|
func (c regChannel) IsConnected() bool { return true }
|
|
func (c regChannel) HealthCheck(_ context.Context) error { return nil }
|
|
|
|
func TestNew_Empty(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
if got := r.List(KindTool); len(got) != 0 {
|
|
t.Fatalf("List(KindTool) = %v, want empty", got)
|
|
}
|
|
if _, err := r.Get("missing"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("Get(missing) error = %v, want ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestRegister_Get(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
if err := r.Register(Capability{Name: "t1", Kind: KindTool, Description: "d", Value: regTool{name: "t1"}}); err != nil {
|
|
t.Fatalf("Register: %v", err)
|
|
}
|
|
got, err := r.Get("t1")
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got.Name != "t1" || got.Kind != KindTool || got.Description != "d" {
|
|
t.Fatalf("Get = %+v, want name=t1 kind=tool desc=d", got)
|
|
}
|
|
}
|
|
|
|
func TestRegister_Duplicate(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
cap := Capability{Name: "dup", Kind: KindTool, Value: regTool{name: "dup"}}
|
|
if err := r.Register(cap); err != nil {
|
|
t.Fatalf("first Register: %v", err)
|
|
}
|
|
err := r.Register(Capability{Name: "dup", Kind: KindSkill, Value: regSkill{name: "dup"}})
|
|
if !errors.Is(err, ErrDuplicate) {
|
|
t.Fatalf("second Register error = %v, want ErrDuplicate", err)
|
|
}
|
|
}
|
|
|
|
func TestRegister_InvalidArgument(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
tests := []struct {
|
|
name string
|
|
cap Capability
|
|
}{
|
|
{name: "empty name", cap: Capability{Kind: KindTool, Value: regTool{name: "x"}}},
|
|
{name: "unknown kind", cap: Capability{Name: "x", Kind: "bogus", Value: regTool{name: "x"}}},
|
|
{name: "nil value", cap: Capability{Name: "x", Kind: KindTool}},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if err := r.Register(tt.cap); !errors.Is(err, ErrInvalidArgument) {
|
|
t.Fatalf("Register(%+v) error = %v, want ErrInvalidArgument", tt.cap, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGet_NotFound(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
got, err := r.Get("nope")
|
|
if !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("Get error = %v, want ErrNotFound", err)
|
|
}
|
|
if got != (Capability{}) {
|
|
t.Fatalf("Get returned %+v on error, want zero Capability", got)
|
|
}
|
|
}
|
|
|
|
func TestList_ByKind(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
caps := []Capability{
|
|
{Name: "t1", Kind: KindTool, Value: regTool{name: "t1"}},
|
|
{Name: "s1", Kind: KindSkill, Value: regSkill{name: "s1"}},
|
|
{Name: "c1", Kind: KindChannel, Value: regChannel{name: "c1"}},
|
|
{Name: "m1", Kind: KindMCP, Value: "mcp"},
|
|
{Name: "i1", Kind: KindIoT, Value: "iot"},
|
|
}
|
|
for _, c := range caps {
|
|
if err := r.Register(c); err != nil {
|
|
t.Fatalf("Register(%s): %v", c.Name, err)
|
|
}
|
|
}
|
|
if got := r.List(KindTool); len(got) != 1 || got[0].Name != "t1" {
|
|
t.Fatalf("List(KindTool) = %+v, want [t1]", got)
|
|
}
|
|
if got := r.List(KindMCP); len(got) != 1 || got[0].Name != "m1" {
|
|
t.Fatalf("List(KindMCP) = %+v, want [m1]", got)
|
|
}
|
|
if got := r.List(""); len(got) != len(caps) {
|
|
t.Fatalf("List(\"\") length = %d, want %d", len(got), len(caps))
|
|
}
|
|
}
|
|
|
|
func TestRegister_SDKWrappers(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
|
|
if err := r.RegisterTool(regTool{name: "calc"}); err != nil {
|
|
t.Fatalf("RegisterTool: %v", err)
|
|
}
|
|
tool, err := r.Get("calc")
|
|
if err != nil {
|
|
t.Fatalf("Get tool: %v", err)
|
|
}
|
|
if tool.Kind != KindTool {
|
|
t.Fatalf("tool kind = %q, want %q", tool.Kind, KindTool)
|
|
}
|
|
if got := tool.Value.(sdk.Tool).Name(); got != "calc" {
|
|
t.Fatalf("tool Value.Name() = %q, want calc", got)
|
|
}
|
|
|
|
if err := r.RegisterSkill(regSkill{name: "review"}); err != nil {
|
|
t.Fatalf("RegisterSkill: %v", err)
|
|
}
|
|
skill, err := r.Get("review")
|
|
if err != nil {
|
|
t.Fatalf("Get skill: %v", err)
|
|
}
|
|
if skill.Kind != KindSkill {
|
|
t.Fatalf("skill kind = %q, want %q", skill.Kind, KindSkill)
|
|
}
|
|
if got := skill.Value.(sdk.Skill).Name(); got != "review" {
|
|
t.Fatalf("skill Value.Name() = %q, want review", got)
|
|
}
|
|
|
|
if err := r.RegisterChannel(regChannel{name: "telegram"}); err != nil {
|
|
t.Fatalf("RegisterChannel: %v", err)
|
|
}
|
|
channel, err := r.Get("telegram")
|
|
if err != nil {
|
|
t.Fatalf("Get channel: %v", err)
|
|
}
|
|
if channel.Kind != KindChannel {
|
|
t.Fatalf("channel kind = %q, want %q", channel.Kind, KindChannel)
|
|
}
|
|
if got := channel.Value.(sdk.Channel).Name(); got != "telegram" {
|
|
t.Fatalf("channel Value.Name() = %q, want telegram", got)
|
|
}
|
|
}
|
|
|
|
func TestRegister_NilSDKValue(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
tests := []struct {
|
|
name string
|
|
call func() error
|
|
}{
|
|
{name: "tool", call: func() error { return r.RegisterTool(nil) }},
|
|
{name: "skill", call: func() error { return r.RegisterSkill(nil) }},
|
|
{name: "channel", call: func() error { return r.RegisterChannel(nil) }},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
err := tt.call()
|
|
if !errors.Is(err, ErrInvalidArgument) {
|
|
t.Fatalf("err = %v, want ErrInvalidArgument", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistry_ConcurrentSafety(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
|
|
const workers = 8
|
|
const perWorker = 100
|
|
var wg sync.WaitGroup
|
|
for w := 0; w < workers; w++ {
|
|
wg.Add(1)
|
|
go func(w int) {
|
|
defer wg.Done()
|
|
for i := 0; i < perWorker; i++ {
|
|
name := kn(w, i)
|
|
if err := r.RegisterTool(regTool{name: name}); err != nil {
|
|
t.Errorf("RegisterTool(%s): %v", name, err)
|
|
return
|
|
}
|
|
if _, err := r.Get(name); err != nil {
|
|
t.Errorf("Get(%s): %v", name, err)
|
|
}
|
|
// Concurrent reads and a miss do not race with writes.
|
|
_ = r.List("")
|
|
_, _ = r.Get("missing-" + name)
|
|
}
|
|
}(w)
|
|
}
|
|
wg.Wait()
|
|
|
|
if got := len(r.List("")); got != workers*perWorker {
|
|
t.Fatalf("registered %d capabilities, want %d", got, workers*perWorker)
|
|
}
|
|
if _, err := r.Get(kn(workers-1, perWorker-1)); err != nil {
|
|
t.Fatalf("Get last capability: %v", err)
|
|
}
|
|
}
|
|
|
|
// kn builds a capability name unique to a worker/iteration pair.
|
|
func kn(w, i int) string {
|
|
return "cap" + strconv.Itoa(w) + "-" + strconv.Itoa(i)
|
|
}
|
|
|
|
func TestRegistry_ToolPolicyDefaultsFalse(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
if err := r.RegisterTool(regTool{name: "calc"}); err != nil {
|
|
t.Fatalf("RegisterTool: %v", err)
|
|
}
|
|
pol, err := r.Policy("calc")
|
|
if err != nil {
|
|
t.Fatalf("Policy: %v", err)
|
|
}
|
|
if pol.AllowInGroup || pol.AllowInStranger {
|
|
t.Fatalf("default policy = %+v, want both markers false", pol)
|
|
}
|
|
}
|
|
|
|
func TestRegistry_SetPolicyAndSetters(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
if err := r.RegisterTool(regTool{name: "calc"}); err != nil {
|
|
t.Fatalf("RegisterTool: %v", err)
|
|
}
|
|
|
|
if err := r.SetPolicy("calc", ToolPolicy{AllowInGroup: true, AllowInStranger: true}); err != nil {
|
|
t.Fatalf("SetPolicy: %v", err)
|
|
}
|
|
pol, _ := r.Policy("calc")
|
|
if !pol.AllowInGroup || !pol.AllowInStranger {
|
|
t.Fatalf("after SetPolicy = %+v, want both true", pol)
|
|
}
|
|
|
|
if err := r.SetAllowInGroup("calc", false); err != nil {
|
|
t.Fatalf("SetAllowInGroup: %v", err)
|
|
}
|
|
pol, _ = r.Policy("calc")
|
|
if pol.AllowInGroup {
|
|
t.Fatalf("SetAllowInGroup(false) did not clear group marker: %+v", pol)
|
|
}
|
|
if !pol.AllowInStranger {
|
|
t.Fatalf("SetAllowInGroup unexpectedly cleared stranger marker: %+v", pol)
|
|
}
|
|
|
|
if err := r.SetAllowInStranger("calc", false); err != nil {
|
|
t.Fatalf("SetAllowInStranger: %v", err)
|
|
}
|
|
pol, _ = r.Policy("calc")
|
|
if pol.AllowInGroup || pol.AllowInStranger {
|
|
t.Fatalf("after clearing both = %+v, want both false", pol)
|
|
}
|
|
}
|
|
|
|
func TestRegistry_Policy_NotFound(t *testing.T) {
|
|
t.Parallel()
|
|
r := New()
|
|
if _, err := r.Policy("missing"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("Policy(missing) error = %v, want ErrNotFound", err)
|
|
}
|
|
if err := r.SetPolicy("missing", ToolPolicy{}); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("SetPolicy(missing) error = %v, want ErrNotFound", err)
|
|
}
|
|
if err := r.SetAllowInGroup("missing", true); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("SetAllowInGroup(missing) error = %v, want ErrNotFound", err)
|
|
}
|
|
if err := r.SetAllowInStranger("missing", true); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("SetAllowInStranger(missing) error = %v, want ErrNotFound", err)
|
|
}
|
|
}
|