Files
homeagent-sdk/tools/hmapdev/yaegi/mocksdk/parity_test.go
JianFeeeee b6a6168e65 chore(module)!: module 路径随托管地迁移 gitcode → github
SDK 仓已迁到 GitHub 作为权威源,go module 路径同步:
`gitcode.com/JianFeeeee/homeagent-sdk` → `github.com/JianFeeeee/homeagentsdk`

## 范围

74 个文件:71 个 Go 源码 + go.mod/go.sum + hmapdev 模板 + CI 与打包脚本。

主仓(HomeAgent)侧对应改动是 6d38a164。本仓这些文件当时**不在工作树上**
(见下),所以那次的批量替换扫不到它们——这里是补偿。

## 为何这批文件当时不在工作树上(事故与恢复)

10-08 07:58 主仓做 checkout/cherry-pick 序列时,把本仓工作树的 20 个文件
误清理了(`example/*/plugin.go` + `plg.json`,10 个示例)。

根因是**结构误判**:本仓是「一目录两仓库」结构下主仓 `third_party/` 里的
**独立 git 仓库**,而主仓 .gitignore 忽略了 `example/`,于是它在主仓
`git status` 里不可见 —— 看着像「不受跟踪的构建产物」,实际是本仓受跟踪的源码。

已全部恢复(`git checkout -- <D 文件>`,只恢复删除、不动迁移改动),
20 个文件与 HEAD 逐字节一致。

**破坏的连锁后果**:恢复出来的 `plugin.go` 仍写着旧 module 路径,
因为迁移时它们已不在工作树上。所以这次补偿是必须的,不是可选优化。

## 保留 gitcode 引用

以下位置的 `gitcode` **有意保留**,不要"顺手清掉":
- README.md / docs 里「二进制实际分发地址目前是 gitcode」的说明
- mkdocs.yml 的 gitcode 镜像链接(国内直连更快)
- `tools/hmapdev/templates.go` 里「SDK 已从 gitcode 迁到 GitHub」的注释

gitcode 仍是**只读镜像**(源码同步;release 附件暂时仍在那边)。

## 验证

- 本仓根 module:`go build ./...` + `go test ./...` 全通过
- 全部 20 个 example:`go vet` 通过(补偿前 10 个因旧 module 路径失败)
- example/qq:`go test` 通过,且能从源码 build 出可用 plugin 产物
2026-10-09 07:25:25 +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 "github.com/JianFeeeee/homeagentsdk/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
})
}