mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-03 15:44:11 +00:00
fix(hmapdev): mocksdk 补齐 ToolDef 声明项,并加字段/类型一致性判据
## 问题
mocksdk(yaegi 解释执行时的替身 SDK)的 ToolDef 只有 6 个字段,
公共 SDK 已有 10 个 —— 缺 RecallPolicy / ParallelSafe / Serial。
★ 危害不在编译期,而在**调试期**:插件作者用 hmapdev 在本地解释执行时,
写了 ParallelSafe:true 照常跑、不报错;直到编译安装后才发现声明根本没
被内核读到。这类不一致不会让任何现有测试失败。
这正是"黑名单不会自动跟上新执行能力"的又一次复现:主 SDK 每加一个声明项,
替身不会自动跟上。
## 修复
① mocksdk/plugin.go 补齐 RecallPolicy / ParallelSafe / Serial 三个字段,
注释写明"主 SDK 先加、mocksdk 没跟上"这段历史,避免后人再漂移。
② 新增 yaegi/mocksdk/parity_test.go:
- TestMockSDKToolDefMatchesSDK 比对**字段名**集合,双向都查
(少字段 = 声明静默失效;多字段 = 替身比本体还多,必有一方理解错了)
- TestMockSDKToolDefTypesMatch 比对**字段类型**,逐个断言
★ 归一化必须抹掉所有空白:gofmt 打印 "interface{}" 而反射给
"interface {}",这是打印格式差异。我第一版没抹空白,
结果每个复合类型都被误报成"类型不符" —— 判据自己制造假警报。
判据放在 tools/hmapdev 模块内(它是独立 module,主模块不包含它),
跑法:cd tools/hmapdev && go test ./yaegi/mocksdk/
This commit is contained in:
150
tools/hmapdev/yaegi/mocksdk/parity_test.go
Normal file
150
tools/hmapdev/yaegi/mocksdk/parity_test.go
Normal file
@ -0,0 +1,150 @@
|
||||
package mocksdk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/printer"
|
||||
"go/token"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
// structFields 解析 file 中的指定 struct,返回其字段名集合。
|
||||
func structFields(t *testing.T, file, structName string) map[string]bool {
|
||||
t.Helper()
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, file, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("解析 %s 失败: %v", file, err)
|
||||
}
|
||||
return fieldsOf(t, fset, f, structName)
|
||||
}
|
||||
|
||||
func fieldsOf(t *testing.T, fset *token.FileSet, f *ast.File, structName string) map[string]bool {
|
||||
t.Helper()
|
||||
out := map[string]bool{}
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
ts, ok := n.(*ast.TypeSpec)
|
||||
if !ok || ts.Name.Name != structName {
|
||||
return true
|
||||
}
|
||||
st, ok := ts.Type.(*ast.StructType)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, fld := range st.Fields.List {
|
||||
for _, nm := range fld.Names {
|
||||
out[nm.Name] = true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// sdkFieldsOf 用反射取公共 SDK ToolDef 的字段名。
|
||||
func sdkFieldsOf(t *testing.T) map[string]bool {
|
||||
t.Helper()
|
||||
out := map[string]bool{}
|
||||
rt := reflect.TypeOf(sdk.ToolDef{})
|
||||
for i := 0; i < rt.NumField(); i++ {
|
||||
out[rt.Field(i).Name] = true
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func os_ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
|
||||
|
||||
func exprString(fset *token.FileSet, e ast.Expr) string {
|
||||
var sb strings.Builder
|
||||
if err := printer.Fprint(&sb, fset, e); err != nil {
|
||||
return ""
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// normalize 归一化类型串:抹掉包路径与指针/切片等修饰差异,只比基础类型。
|
||||
func normalize(s string) string {
|
||||
// 抹掉所有空白:gofmt 打印 "interface{}" 而反射给 "interface {}",
|
||||
// 这是**打印格式差异**,不是类型差异 —— 我第一版没抹空白,
|
||||
// 结果判据把每个复合类型都误报成"类型不符"。
|
||||
s = strings.Join(strings.Fields(s), "")
|
||||
if i := strings.LastIndex(s, "."); i >= 0 && !strings.Contains(s, "]") {
|
||||
s = s[i+1:]
|
||||
}
|
||||
return strings.TrimPrefix(s, "*")
|
||||
}
|
||||
|
||||
var _ = fmt.Sprintf
|
||||
|
||||
// TestMockSDKToolDefMatchesSDK 保证 mocksdk 的 ToolDef 与公共 SDK **逐字段对齐**。
|
||||
//
|
||||
// 为什么必须有:mocksdk 是 yaegi 解释执行时的替身 SDK。插件作者用它本地调试,
|
||||
// 少一个字段就会"写了声明却不报错",直到编译安装后才发现声明没被内核读到。
|
||||
// 这种不一致极难察觉,且**不会让任何现有测试失败** —— 所以要显式钉住。
|
||||
//
|
||||
// 这不是理论风险:RecallPolicy、ParallelSafe、Serial 三个字段就是先后漂移的
|
||||
// (主 SDK 先加,mocksdk 没跟上)。
|
||||
func TestMockSDKToolDefMatchesSDK(t *testing.T) {
|
||||
mockFields := structFields(t, "plugin.go", "ToolDef")
|
||||
sdkFields := sdkFieldsOf(t)
|
||||
|
||||
var missing, extra []string
|
||||
for name := range sdkFields {
|
||||
if !mockFields[name] {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
for name := range mockFields {
|
||||
if !sdkFields[name] {
|
||||
extra = append(extra, name)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
t.Errorf("mocksdk.ToolDef 缺少字段 %v —— 插件本地调试时写了声明不报错,装到内核却不生效", missing)
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
t.Errorf("mocksdk.ToolDef 有公共 SDK 没有的字段 %v —— 替身比本体还多,必有一方理解错了", extra)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMockSDKToolDefTypesMatch 字段类型也要一致(不只是名字)。
|
||||
func TestMockSDKToolDefTypesMatch(t *testing.T) {
|
||||
rt := reflect.TypeOf(sdk.ToolDef{})
|
||||
src, err := os_ReadFile("plugin.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, "plugin.go", src, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ast.Inspect(f, func(n ast.Node) bool {
|
||||
ts, ok := n.(*ast.TypeSpec)
|
||||
if !ok || ts.Name.Name != "ToolDef" {
|
||||
return true
|
||||
}
|
||||
st := ts.Type.(*ast.StructType)
|
||||
for _, fld := range st.Fields.List {
|
||||
name := fld.Names[0].Name
|
||||
sf, exists := rt.FieldByName(name)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
want := sf.Type.String()
|
||||
got := exprString(fset, fld.Type)
|
||||
// 归一化包路径前缀
|
||||
got = normalize(got)
|
||||
if got != normalize(want) {
|
||||
t.Errorf("mocksdk.ToolDef.%s 类型是 %s,公共 SDK 是 %s", name, got, want)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
@ -90,6 +90,15 @@ type ToolDef struct {
|
||||
NoMemory bool `json:"no_memory,omitempty"`
|
||||
Cleaner func(string) string `json:"-"`
|
||||
ContextPolicy string `json:"context_policy,omitempty"`
|
||||
RecallPolicy string `json:"recall_policy,omitempty"`
|
||||
// ParallelSafe / Serial 与公共 SDK 的同名声明项**逐字对齐**。
|
||||
//
|
||||
// ⚠️ 不要只补主 SDK 就以为完事:mocksdk 是 yaegi 解释执行用的**替身**,
|
||||
// 少一个字段,插件作者在本地调试时"写了声明却不报错",编译安装后才发现
|
||||
// 声明没生效 —— 这种不一致极难察觉(黑名单不会自动跟上新执行能力)。
|
||||
// TestMockSDKToolDefMatchesSDK 钉住这一致。
|
||||
ParallelSafe bool `json:"parallel_safe,omitempty"`
|
||||
Serial bool `json:"serial,omitempty"`
|
||||
}
|
||||
|
||||
// 上下文策略取值,与公共 SDK 一致。
|
||||
|
||||
Reference in New Issue
Block a user