根因: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。
213 lines
6.8 KiB
Go
213 lines
6.8 KiB
Go
package sdk
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
// stubTool is a mock Tool used to verify the registration helpers forward to a
|
|
// Registrar.
|
|
type stubTool struct{ name string }
|
|
|
|
func (s stubTool) Name() string { return s.name }
|
|
func (s stubTool) Description() string { return "desc " + s.name }
|
|
func (s stubTool) Parameters() (map[string]any, error) { return map[string]any{"type": "object"}, nil }
|
|
func (s stubTool) Execute(_ context.Context, _ any) (any, error) { return "exec " + s.name, nil }
|
|
|
|
// stubSkill is a mock Skill used to verify the registration helpers forward.
|
|
type stubSkill struct{ name string }
|
|
|
|
func (s stubSkill) Name() string { return s.name }
|
|
func (s stubSkill) Description() string { return "desc " + s.name }
|
|
func (s stubSkill) Prompt() string { return "prompt " + s.name }
|
|
func (s stubSkill) Steps() []Step { return []Step{{Name: "s1", Prompt: "p1"}} }
|
|
func (s stubSkill) Refs() []Ref { return []Ref{{Name: "r1", Type: "doc"}} }
|
|
func (s stubSkill) Guardrails() []Guardrail {
|
|
return []Guardrail{{ID: "g1", Message: "no", Enforced: true}}
|
|
}
|
|
|
|
// stubChannel is a mock Channel used to verify the registration helpers forward.
|
|
type stubChannel struct{ name string }
|
|
|
|
func (s stubChannel) Name() string { return s.name }
|
|
func (s stubChannel) Connect(_ context.Context) error { return nil }
|
|
func (s stubChannel) Disconnect(_ context.Context) error { return nil }
|
|
func (s stubChannel) IsConnected() bool { return true }
|
|
func (s stubChannel) HealthCheck(_ context.Context) error { return nil }
|
|
|
|
// stubRegistrar records registrations and always succeeds.
|
|
type stubRegistrar struct {
|
|
tools []Tool
|
|
skills []Skill
|
|
channels []Channel
|
|
}
|
|
|
|
func (s *stubRegistrar) RegisterTool(t Tool) error {
|
|
s.tools = append(s.tools, t)
|
|
return nil
|
|
}
|
|
|
|
func (s *stubRegistrar) RegisterSkill(skl Skill) error {
|
|
s.skills = append(s.skills, skl)
|
|
return nil
|
|
}
|
|
|
|
func (s *stubRegistrar) RegisterChannel(c Channel) error {
|
|
s.channels = append(s.channels, c)
|
|
return nil
|
|
}
|
|
|
|
// errRegistrar fails every registration with the error it carries.
|
|
type errRegistrar struct{ err error }
|
|
|
|
func (e errRegistrar) RegisterTool(Tool) error { return e.err }
|
|
func (e errRegistrar) RegisterSkill(Skill) error { return e.err }
|
|
func (e errRegistrar) RegisterChannel(Channel) error { return e.err }
|
|
|
|
func TestRegisterTool_Forwards(t *testing.T) {
|
|
t.Parallel()
|
|
r := &stubRegistrar{}
|
|
tool := stubTool{name: "calc"}
|
|
if err := RegisterTool(r, tool); err != nil {
|
|
t.Fatalf("RegisterTool: %v", err)
|
|
}
|
|
if len(r.tools) != 1 || r.tools[0] != tool {
|
|
t.Fatalf("registered %d tools (%v), want exactly tool %v", len(r.tools), r.tools, tool)
|
|
}
|
|
}
|
|
|
|
func TestRegisterSkill_Forwards(t *testing.T) {
|
|
t.Parallel()
|
|
r := &stubRegistrar{}
|
|
skill := stubSkill{name: "review"}
|
|
if err := RegisterSkill(r, skill); err != nil {
|
|
t.Fatalf("RegisterSkill: %v", err)
|
|
}
|
|
if len(r.skills) != 1 || r.skills[0] != skill {
|
|
t.Fatalf("registered %d skills (%v), want exactly skill %v", len(r.skills), r.skills, skill)
|
|
}
|
|
}
|
|
|
|
func TestRegisterChannel_Forwards(t *testing.T) {
|
|
t.Parallel()
|
|
r := &stubRegistrar{}
|
|
channel := stubChannel{name: "telegram"}
|
|
if err := RegisterChannel(r, channel); err != nil {
|
|
t.Fatalf("RegisterChannel: %v", err)
|
|
}
|
|
if len(r.channels) != 1 || r.channels[0] != channel {
|
|
t.Fatalf("registered %d channels (%v), want exactly channel %v", len(r.channels), r.channels, channel)
|
|
}
|
|
}
|
|
|
|
func TestRegisterHelpers_NilRegistrar(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
call func() error
|
|
}{
|
|
{name: "tool", call: func() error { return RegisterTool(nil, stubTool{name: "t"}) }},
|
|
{name: "skill", call: func() error { return RegisterSkill(nil, stubSkill{name: "s"}) }},
|
|
{name: "channel", call: func() error { return RegisterChannel(nil, stubChannel{name: "c"}) }},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if err := tt.call(); !errors.Is(err, ErrNilRegistrar) {
|
|
t.Fatalf("err = %v, want ErrNilRegistrar", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegisterHelpers_PropagateError(t *testing.T) {
|
|
t.Parallel()
|
|
want := errors.New("sdk: propagate error")
|
|
tests := []struct {
|
|
name string
|
|
call func() error
|
|
}{
|
|
{name: "tool", call: func() error { return RegisterTool(errRegistrar{err: want}, stubTool{name: "t"}) }},
|
|
{name: "skill", call: func() error { return RegisterSkill(errRegistrar{err: want}, stubSkill{name: "s"}) }},
|
|
{name: "channel", call: func() error { return RegisterChannel(errRegistrar{err: want}, stubChannel{name: "c"}) }},
|
|
}
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if err := tt.call(); !errors.Is(err, want) {
|
|
t.Fatalf("err = %v, want %v", err, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunConfigHooks_RunsInOrder(t *testing.T) {
|
|
t.Parallel()
|
|
var order []string
|
|
hooks := []ConfigHook{
|
|
func(_ context.Context, _ map[string]any) error { order = append(order, "a"); return nil },
|
|
func(_ context.Context, _ map[string]any) error { order = append(order, "b"); return nil },
|
|
}
|
|
if err := RunConfigHooks(context.Background(), nil, hooks...); err != nil {
|
|
t.Fatalf("RunConfigHooks: %v", err)
|
|
}
|
|
if len(order) != 2 || order[0] != "a" || order[1] != "b" {
|
|
t.Fatalf("hook order = %v, want [a b]", order)
|
|
}
|
|
}
|
|
|
|
func TestRunConfigHooks_MutatesConfig(t *testing.T) {
|
|
t.Parallel()
|
|
cfg := map[string]any{"a": "1"}
|
|
hook := func(_ context.Context, c map[string]any) error { c["a"] = "2"; return nil }
|
|
if err := RunConfigHooks(context.Background(), cfg, hook); err != nil {
|
|
t.Fatalf("RunConfigHooks: %v", err)
|
|
}
|
|
if cfg["a"] != "2" {
|
|
t.Fatalf("config a = %v, want 2", cfg["a"])
|
|
}
|
|
}
|
|
|
|
func TestRunConfigHooks_StopsOnError(t *testing.T) {
|
|
t.Parallel()
|
|
hookErr := errors.New("sdk: hook error")
|
|
ranSecond := false
|
|
hooks := []ConfigHook{
|
|
func(_ context.Context, _ map[string]any) error { return hookErr },
|
|
func(_ context.Context, _ map[string]any) error { ranSecond = true; return nil },
|
|
}
|
|
if err := RunConfigHooks(context.Background(), nil, hooks...); !errors.Is(err, hookErr) {
|
|
t.Fatalf("err = %v, want hookErr", err)
|
|
}
|
|
if ranSecond {
|
|
t.Fatal("second hook ran after the first returned an error")
|
|
}
|
|
}
|
|
|
|
func TestRunConfigHooks_NilHookSkipped(t *testing.T) {
|
|
t.Parallel()
|
|
if err := RunConfigHooks(context.Background(), nil, nil, nil); err != nil {
|
|
t.Fatalf("RunConfigHooks(all nil): %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunConfigHooks_ContextCanceled(t *testing.T) {
|
|
t.Parallel()
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
ran := false
|
|
err := RunConfigHooks(ctx, nil, func(_ context.Context, _ map[string]any) error {
|
|
ran = true
|
|
return nil
|
|
})
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("err = %v, want context.Canceled", err)
|
|
}
|
|
if ran {
|
|
t.Fatal("hook ran on an already-canceled context")
|
|
}
|
|
}
|