mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-02 15:14:37 +00:00
## 问题
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/
151 lines
4.2 KiB
Go
151 lines
4.2 KiB
Go
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
|
||
})
|
||
}
|