- 保存被包成事务:快照 → 落盘 → 按新配置重建工具 → 原子替换注册表条目(registry.ReplaceTool 窄方法,保留运行期 Policy);下一轮即生效,不要求重启 - 若新配置会让后端下次起不来(主家未配齐 fail-fast)⇒ 文件按字节回滚 + 409 config_unstartable - 声明面补 primary + fallback[](可配可生效);否决「另开 form」方案(会破坏既有 reauth 回填) - 主 provider 解析口径核查结论:没被 PLG-4 改坏(顺手修了一处日志缺陷) - 4 个变异自检(丢 Policy/不换条目/回滚 no-op/忽略 fallback)
102 lines
3.7 KiB
Go
102 lines
3.7 KiB
Go
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)
|
||
}
|
||
}
|