chore(vendor): 主仓不再跟踪 SDK 示例代码(决策 A)

## 背景

`.gitignore` 第 26-45 行早已写明「外部插件与工具链维护在独立 SDK 仓
(决策 sdk_repo_only),本仓经 go.mod 的 replace 引用」,并忽略了
example/、tools/、docs/、site_build/、package/、scripts/。

但有 29 个文件**早于该规则**被跟踪,靠「已跟踪文件不受 .gitignore
影响」留着,注释还特意写了「这是有意的,不要『修』」。

本轮修 example/bili 时踩到了:那 87 行改动先落在主仓、再手动同步到
SDK 仓 —— 同一份代码两个仓各改一遍,正是这个遗留结构的成本。

## 核实:主仓到底需要什么

- 主仓 Go 代码只 import 两个包:`homeagent-sdk/sdk`(插件入口)、
  `homeagent-sdk/meta`(版本号)
- `remotedevice/` 是 C 库,主仓 C 侧明确「不链接任何外部库」,
  只有注释里提到它
- `tools/hmapdev/yaegi` 的 import 出现在 **SDK 仓自己的文件之间**
  (yaegi/interp.go → yaegi/mocksdk),不是主仓依赖;
  且 tools/ 本就在 .gitignore 里,从未被跟踪

所以 29 个文件全部可以删。实际仓库负担也只有 42 个文件 / 0.6MB
(工作树里那 971MB 绝大部分是未跟踪的构建产物,不是仓库体积)。

## 改动

- 删 21 个 example/ 文件(10 个示例的 plg.json + plugin.go + qq/plugin_test.go)
- 删 8 个 remotedevice/ C 文件
- 工作树里一并清掉(不受跟踪的构建产物顺带回收)

构建与全量测试均通过。

## 判据:3 条 + 变异(internal/meta/vendored_sdk_test.go)

- TestVendoredSDKHasNoExampleOrRemotedevice:防止示例代码被重新提交进来
- TestVendoredSDKKeepsRequiredPackages:**反向**保护,防止为省事把
  sdk/ 与 meta/ 也删掉(上一条只防"多了",删过头要靠这条)
- TestGoModStillReplacesSDKToVendoredPath:决策 A 依赖 replace 指向 vendored 路径

★ 判据自己踩了两个坑,都靠实跑抓出来:
1. `git ls-files` 的路径参数**相对当前目录**解析,而测试跑在
   internal/meta/ 下 ⇒ 就地执行返回空,表现为「必需包全都不在」的假红。
   改用 `git -C <仓库根>`。
2. 把 tools/hmapdev/yaegi 当成主仓依赖写进必需清单 ⇒ 又一次假红。
   根因是把 SDK 内部的引用误当成主仓依赖(它本就在 .gitignore 里)。

变异验证:塞一个 example 文件进版本控制 → 判红。
This commit is contained in:
JianFeeeee
2026-09-26 17:18:41 +08:00
parent eb14b3a1d2
commit df872b9e86
30 changed files with 126 additions and 14144 deletions

View File

@ -0,0 +1,126 @@
package meta_test
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// ===== 主仓不再 vendoring SDK 的示例代码 =====
//
// 决策(对应 .gitignore 里的 sdk_repo_only):外部插件与工具链只在 SDK 仓
// 维护,主仓经 go.mod 的 replace 引用,**只保留编译所需的那几个包**。
//
// 曾经的状况:主仓跟踪了 29 个 SDK 示例文件(example/*/plg.json、plugin.go、
// qq/plugin_test.go,加 remotedevice/ 的 8 个 C 文件)。它们不参与主仓构建
// (唯一 import 的 SDK 包是 sdk/ 与 meta/),却要在两个仓里同步同一份代码。
// 本轮修 example/bili 时的重复劳动就是这么来的。
//
// 本测试钉住这个决策:有人再把示例代码提交进主仓时立刻变红。
// ★ 必须用 git ls-files 判断「**版本控制里有没有**」而不是看目录是否存在 ——
// 工作树里可能因为 vendoring 或本地试验而临时存在,那不算回归。
// vendoredSDKRoot 是 vendored SDK 在主仓里的位置。
const vendoredSDKRoot = "third_party/homeagent-sdk"
// repoRoot 返回主仓根目录(本测试包在 internal/meta/,往上是两层)。
func repoRoot(t *testing.T) string {
t.Helper()
abs, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("解析仓库根: %v", err)
}
return abs
}
// gitTracked 列出 git 版本控制里该路径下的文件(相对仓库根)。
//
// ★ 必须用 `git -C <仓库根>` 而不是就地执行:git ls-files 的路径参数
//
// 是**相对当前目录**解析的,而这个测试运行在 internal/meta/ 下,
// 就地执行会返回空 —— 表现为"必需包全都不在"的假红(我第一版就这样)。
func gitTracked(t *testing.T, pathPrefix string) []string {
t.Helper()
out, err := exec.Command("git", "-C", repoRoot(t), "ls-files", pathPrefix).Output()
if err != nil {
// 不是 git 仓库 / git 不可用:跳过而不是误报。
if _, statErr := os.Stat(filepath.Join(repoRoot(t), ".git")); statErr != nil {
t.Skip("非 git 仓库,跳过结构守卫")
}
t.Fatalf("git ls-files %s: %v", pathPrefix, err)
}
var files []string
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
if line = strings.TrimSpace(line); line != "" {
files = append(files, line)
}
}
return files
}
// example/ 与 remotedevice/ 不该再被主仓跟踪。
func TestVendoredSDKHasNoExampleOrRemotedevice(t *testing.T) {
for _, sub := range []string{"example", "remotedevice"} {
files := gitTracked(t, vendoredSDKRoot+"/"+sub)
if len(files) > 0 {
t.Errorf("主仓仍在跟踪 %s/ 下的 %d 个文件(决策:示例代码只在 SDK 仓维护):\n %s\n"+
" 这些文件不参与主仓构建(唯一 import 的 SDK 包是 sdk/ 与 meta/),\n"+
" 却要在两个仓同步同一份代码 —— 本轮修 example/bili 时已因此重复劳动。\n"+
" 修复:git rm -r --cached %s/%s",
sub, len(files), strings.Join(files, "\n "), vendoredSDKRoot, sub)
}
}
}
// 反向保护:编译真正需要的包必须在,否则删过头会编译失败。
//
// 这条不能省:上一条只防"多了",若有人为了省事把 sdk/ 或 meta/ 也删了,
// 上一条照样绿,而 go build ./... 会在 CI 里才炸。
func TestVendoredSDKKeepsRequiredPackages(t *testing.T) {
required := []string{
"sdk", // 公开契约:所有插件的入口
"sdk/plugin.go", // 具体文件:确保不是空目录
"meta", // 版本号:主仓与 SDK 版本一致性检查
"meta/meta.go",
// ★ 不列 tools/hmapdev/yaegi:.gitignore 第 33 行忽略了
// third_party/homeagent-sdk/tools/,它从来没被跟踪过。
// 而 grep 到的 yaegi 引用是 **SDK 仓自己的文件**在互相 import
// (tools/hmapdev/yaegi/interp.go → .../mocksdk),不是主仓依赖。
// 我第一版把它当主仓依赖写进来,判据直接假红。
}
tracked := map[string]bool{}
for _, f := range gitTracked(t, vendoredSDKRoot) {
tracked[f] = true
}
for _, req := range required {
// 支持两种形态:目录(下面有文件)或直接是文件
found := tracked[vendoredSDKRoot+"/"+req]
if !found {
// 目录形态:检查它下面是否有被跟踪的文件
for f := range tracked {
if strings.HasPrefix(f, vendoredSDKRoot+"/"+req+"/") {
found = true
break
}
}
}
if !found {
t.Errorf("必需包 %s 不在主仓 vendored 范围内(删过头了):%s/%s",
req, vendoredSDKRoot, req)
}
}
}
// go.mod 的 replace 必须仍指向 vendored 路径 —— 决策 A 依赖它。
func TestGoModStillReplacesSDKToVendoredPath(t *testing.T) {
data, err := os.ReadFile(filepath.Join(repoRoot(t), "go.mod"))
if err != nil {
t.Fatalf("读 go.mod: %v", err)
}
content := string(data)
if !strings.Contains(content, vendoredSDKRoot) {
t.Errorf("go.mod 里没有 replace 到 %s:\n%s", vendoredSDKRoot, content)
}
}