Files
homeagent-sdk/tools/hmapdev/yaegi/mocksdk/parity_test.go
JianFeeeee 1d7c330cfc 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/
2026-09-27 16:17:04 +08:00

151 lines
4.2 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 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
})
}