Files
Cyrene_v2/internal/capability/registry/replacetool_test.go
T
AskaEth a30f4a49ff feat(search): provider 热重载 + primary/fallback(W3)
- 保存被包成事务:快照 → 落盘 → 按新配置重建工具 → 原子替换注册表条目(registry.ReplaceTool
  窄方法,保留运行期 Policy);下一轮即生效,不要求重启
- 若新配置会让后端下次起不来(主家未配齐 fail-fast)⇒ 文件按字节回滚 + 409 config_unstartable
- 声明面补 primary + fallback[](可配可生效);否决「另开 form」方案(会破坏既有 reauth 回填)
- 主 provider 解析口径核查结论:没被 PLG-4 改坏(顺手修了一处日志缺陷)
- 4 个变异自检(丢 Policy/不换条目/回滚 no-op/忽略 fallback)
2026-10-06 15:59:12 +08:00

102 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package registry
import (
"context"
"errors"
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
)
// 本文件钉住切片 W3 起的**窄方法** `ReplaceTool`(热替换,design/09 §5.5):
// 面板保存后重建出来的同名工具要能就地换掉旧条目,且换完不丢「用户改过的策略」。
//
// 单独成文件的理由:registry_test.go 在 HEAD 上就没过 gofmt(存量),
// 本片不动它(不顺手格式化会产生与主题无关的 diff)。
// stubTool 是一个名字可指定的最小工具(只用于注册/替换的判定)。
type stubTool struct {
name string
desc string
}
func (s stubTool) Name() string { return s.name }
func (s stubTool) Description() string { return s.desc }
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 nil, nil }
var _ sdk.Tool = (*stubTool)(nil)
// TestReplaceToolSwapsValueAndKeepsPolicy 是热替换的主判据:
// - Value 换成新工具(调用方随后 Invoke 拿到的就是新的那一把);
// - Description 跟着新工具走(它是工具实现的事实);
// - **Policy 原样保留** —— 分组/陌生人可达标记是运行期配置,不属于工具实现;
// 丢掉它 = 一次面板保存让某个群忽然不能用了。
func TestReplaceToolSwapsValueAndKeepsPolicy(t *testing.T) {
r := New()
if err := r.RegisterTool(&stubTool{name: "web-search", desc: "old"}); err != nil {
t.Fatalf("register: %v", err)
}
if err := r.SetAllowInGroup("web-search", true); err != nil {
t.Fatalf("SetAllowInGroup: %v", err)
}
next := &stubTool{name: "web-search", desc: "new"}
if err := r.ReplaceTool(next); err != nil {
t.Fatalf("ReplaceTool: %v", err)
}
cap, err := r.Get("web-search")
if err != nil {
t.Fatalf("Get: %v", err)
}
if cap.Value != sdk.Tool(next) {
t.Fatalf("替换后 Value = %#v, want 新的那一把", cap.Value)
}
if cap.Description != "new" {
t.Fatalf("替换后 Description = %q, want %q(描述属于工具实现,跟着换)", cap.Description, "new")
}
if !cap.Policy.AllowInGroup {
t.Fatal("替换丢掉了 AllowInGroup:运行期策略不属于工具构造产出,必须沿用")
}
if cap.Kind != KindTool {
t.Fatalf("替换后 Kind = %q, want %q", cap.Kind, KindTool)
}
}
// TestReplaceToolReportsMissingAndInvalid:两种**不该被静默吞掉**的调用错误。
func TestReplaceToolReportsMissingAndInvalid(t *testing.T) {
r := New()
err := r.ReplaceTool(&stubTool{name: "web-search"})
if !errors.Is(err, ErrNotFound) {
t.Fatalf("替换不存在的名字 err = %v, want ErrNotFound(不顺手新增:名字打错时静默插入会掩盖调用方的错)", err)
}
if err := r.RegisterTool(&stubTool{name: "nope"}); err != nil {
t.Fatalf("register: %v", err)
}
if err := r.ReplaceTool(nil); !errors.Is(err, ErrInvalidArgument) {
t.Fatalf("替换成 nil err = %v, want ErrInvalidArgument", err)
}
}
// TestReplaceToolRefusesNonToolEntry:同名条目不是工具时拒绝,绝不悄悄改掉它的 Kind
// (把一条渠道/MCP 能力换成工具会让「这条能力是什么」变成一件靠时间猜的事)。
func TestReplaceToolRefusesNonToolEntry(t *testing.T) {
r := New()
if err := r.Register(Capability{Name: "web-search", Kind: KindMCP, Value: struct{}{}}); err != nil {
t.Fatalf("register: %v", err)
}
err := r.ReplaceTool(&stubTool{name: "web-search"})
if !errors.Is(err, ErrInvalidArgument) {
t.Fatalf("err = %v, want ErrInvalidArgument", err)
}
cap, gerr := r.Get("web-search")
if gerr != nil {
t.Fatalf("Get: %v", gerr)
}
if cap.Kind != KindMCP {
t.Fatalf("被拒的替换却改了 Kind:%q", cap.Kind)
}
}