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