Files
HomeAgent/internal/update/apply_regression_test.go
JianFeeeee 892247946a fix: staticcheck 24 条存量全部清零(含 4 个真实缺陷)
staticcheck 门接入时把存量设为基线 24 条,本次全部清掉,基线改为 0。

## 生产代码里的真实缺陷(4 个)

### ① memory/fusion.go —— BFS 的 depth 参数形同虚设

    for d := 1; d <= depth; d++ {
        neighbors, _ := g.NeighbourBlocks(seed.BlockID)   // ← 永远是 seed
        ...
        break                                             // ← 只跑一轮
    }

三处叠加:循环体末尾无条件 `break`;每层传的还是 `seed.BlockID`;
于是 depth=2 与 depth=3 召回结果**完全一样**。

staticcheck SA4008("变量在循环条件里从不改变")正是从这里报出来的 ——
它看出来 `d` 除了打分没有参与寻址。

改成真正的逐层 BFS(frontier 推进到上一层新发现的节点)。实测:

    depth=1 → 2 个  [节点0 节点1]
    depth=2 → 3 个  [节点0 节点1 节点2]      ← 修复前停在这里
    depth=3 → 4 个  [节点0 节点1 节点2 节点3]

补判据 `TestBFS召回_层深真实生效`:必须用**链**(n0-n1-n2-n3),
星型在 depth=1 就能召回全部叶子、断不出有没有走深。
变异(改回单层)⇒ 实测红:`★ depth=2 召回 2 个,期望 3`。

### ② nlp/extractor.go —— subjRels/objRels 只写不读

两个切片被 append 了 4 处,但**从未被读取**。三元组的关系类型来自
`relLabel := result.Tokens[vi]`,置信度来自 `result.DepRels[si]`,
与这两个切片无关。删除。

### ③ plugins/cmd/plugin.go —— 配置解析失败会把上限写成 0

    if n, err := fmt.Sscanf(s, "%d", &p.maxOutput); err == nil && n > 0 {
    }   // ← 空分支

直接 Sscanf 到 &p.maxOutput:解析失败时它可能已被写成 0 或部分值,
而空分支无法回滚 ⇒ 配置写错一个字符就把输出上限变成 0
(下游 `maxLen := p.maxOutput` 直接拿它算截断长度)。
改为解析到临时变量、失败时保持默认并记日志。

### ④ cmd/homed/bootstrap.go —— 空的 else 分支

`if err != nil { log } else { }` —— 删掉 else。

## 测试判据里的真实缺陷(3 个)

### ⑤ stop_semantics_test —— 短路求值让第二次调用从不执行

    if !a.sched.consumeCancel() || !a.sched.consumeCancel()

第一次返回 true 时第二次**根本不会执行**,于是"两条配额都可消费"
这条断言实际只测了一次。拆成两次独立调用(两处同型)。

### ⑥ seq/store_test —— 永假断言

    if maxCallDepth != maxCallDepth { t.Fatal("maxCallDepth 不稳定") }

自己跟自己比,从诞生起就不可能失败。改成显式读两次比较。

### ⑦ pluginhost/catalog_test —— typed-nil 守卫永假

    var nb *kstate.SyncBus
    var iface kstate.SyncState = nb
    if iface == nil { t.Skip(...) }        // ← 恒 false

把 typed-nil 装进接口后,接口的 (type, value) 二元组都不为 nil。
这道前置守卫**从未生效** —— 而这正是该判据要演示的陷阱本身。
改用 `reflect.ValueOf(iface).IsNil()`。

## 其余(写法/冗余,无行为变化)

  agent.go / context.go / toolresult_store_test / apply_regression_test
    空 if 块(注释移到真实分支上方)

  codec_chunkfast_golden_test   未使用的三个返回值 + 空 if
  scheduler_critical/stack_test  `if _, _ = f(); true { }` → 直接调用
  overflow_test / upgrade_test   变量被整体覆盖,首次赋值未使用
  dense_test                     m[string(x)] 替代临时变量
  lex_rules_test                 range over string 而非 []rune(string)

## 验证

    staticcheck -checks 'SA4*,SA5*,SA6*,SA9*'   0 条(基线 24 → 0)
    go build ./...                              PASS
    go vet ./...                                PASS
    gofmt -l                                    干净
    go test ./... -count=1                      65 包全过,0 FAIL
    干净检出(worktree)                         build + memory/nlp/core 全绿
2026-11-04 19:49:21 +08:00

386 lines
14 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 update
// Apply / 回滚路径的**回归判据**。
//
// 这些用例的共同点:它们钉住的都是“**静默产出看起来成功的结果**”
// 那类缺陷。升级路径上的静默失败 = 机器进崩溃循环 + 没人知道为什么,
// 所以每个用例都不只断言“返回了 error”,而是断言**副作用没有发生**。
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ── 测试脚手架 ───────────────────────────────────────────────
// fakeBin 造一个能被 `--version` 读出版本的假二进制(shell 脚本)。
func fakeBin(t *testing.T, dir, name, version, commit string) string {
t.Helper()
p := filepath.Join(dir, name)
body := "#!/bin/sh\necho version=" + version + "\necho commit=" + commit +
"\necho build_time=2026-01-01T00:00:00Z\necho sdk=1.4.0\n"
if err := os.WriteFile(p, []byte(body), 0755); err != nil {
t.Fatalf("写假二进制: %v", err)
}
return p
}
// oldKernelBin 造一个**不支持 --version** 的假旧内核(模拟真实 1.3.x 形态)。
func oldKernelBin(t *testing.T, dir string) string {
t.Helper()
p := filepath.Join(dir, "homed")
body := "#!/bin/sh\n" +
"echo 'flag provided but not defined: -version' >&2\n" +
"exit 2\n"
if err := os.WriteFile(p, []byte(body), 0755); err != nil {
t.Fatalf("写旧内核: %v", err)
}
return p
}
func newPkgRoot(t *testing.T, version, commit string) string {
t.Helper()
root := t.TempDir()
binDir := filepath.Join(root, "bin")
if err := os.MkdirAll(binDir, 0755); err != nil {
t.Fatal(err)
}
src := fakeBin(t, binDir, "homed", version, commit)
_ = src
return root
}
// ── ★ 核心回归:从「没有 --version 的旧内核」升级失败后,必须能回滚 ──
// TestRollbackToOldKernelWithoutVersionFlag 钉住 PR #31 引入的兼容性回归。
//
// 事故形态:旧内核(1.3.x 及以前)没有 `--version`。若回滚校验依赖
// `--version`,那么从它升上去、新版本起不来时:
//
// ① 文件已正确还原成可执行的旧内核
// ② 但读版本失败 ⇒ 判“回滚失败”
// ③ kernelmod/update 拒绝启动
// ④ marker 保留 ⇒ 下次启动同一条路径 ⇒ **永久回滚不了**
//
// 判据不是“返回 nil”,而是“文件真的被还原且内容与备份逐字节相同”。
func TestRollbackToOldKernelWithoutVersionFlag(t *testing.T) {
dir := t.TempDir()
dataDir := t.TempDir()
// 备份:真实的旧内核形态(没有 --version)
backupDir := filepath.Join(dataDir, BackupDirPrefix+NowStamp())
if err := os.MkdirAll(backupDir, 0755); err != nil {
t.Fatal(err)
}
oldBin := oldKernelBin(t, backupDir)
oldSum, err := sha256File(oldBin)
if err != nil {
t.Fatal(err)
}
// 现状:升级后的新二进制是坏的(不可执行)
bin := filepath.Join(dir, "homed")
if err := os.WriteFile(bin, []byte(""), 0755); err != nil {
t.Fatal(err)
}
// marker 记录了升级前的 digest(新版 Apply 一定会写)
if err := MarkPendingUpgrade(dataDir, PendingRecord{
FromVer: "1.3.0", FromCommit: "aaa1111",
ToVer: "1.4.0", ToCommit: "bbb2222",
BackupDir: backupDir,
Digests: map[string]string{bin: oldSum},
}); err != nil {
t.Fatal(err)
}
rolled, from, to, err := RollbackIfUnconfirmed(dataDir, bin)
if err != nil {
t.Fatalf("★ 回滚不应失败(旧内核没有 --version 是常态,不是缺陷): %v", err)
}
if !rolled {
t.Fatal("有待确认标记时必须回滚")
}
if from != "1.3.0" || to != "1.4.0" {
t.Errorf("版本轨迹不对: %s → %s", from, to)
}
// ★ 真正要断言的是:磁盘上就是那份旧内核,逐字节相同
got, err := sha256File(bin)
if err != nil {
t.Fatal(err)
}
if got != oldSum {
t.Errorf("★ 回滚后内容与备份不一致:期望 %s 实际 %s", ShortSum(oldSum), ShortSum(got))
}
// marker 必须清掉:回滚成功且已校验
if rec, _ := ReadPendingUpgrade(dataDir); rec != nil {
t.Error("回滚成功后应清除 marker")
}
}
// TestRollbackRejectsCorruptBackupByDigest 钉住"坏备份不能被当成回滚成功"。
//
// 旧实现的判据是 `CopyFile 没报错` —— 零字节备份拷过去也成功。
// 现在用 digest:内容不对就是失败,且 marker 必须保留(保住第二次自愈机会)。
func TestRollbackRejectsCorruptBackupByDigest(t *testing.T) {
dir := t.TempDir()
dataDir := t.TempDir()
backupDir := filepath.Join(dataDir, BackupDirPrefix+NowStamp())
if err := os.MkdirAll(backupDir, 0755); err != nil {
t.Fatal(err)
}
// 备份是零字节(磁盘满时极易发生)
if err := os.WriteFile(filepath.Join(backupDir, "homed"), []byte{}, 0755); err != nil {
t.Fatal(err)
}
bin := filepath.Join(dir, "homed")
if err := os.WriteFile(bin, []byte("BROKEN"), 0755); err != nil {
t.Fatal(err)
}
// marker 里记的是"当时那份文件"的 digest,与零字节备份不符
if err := MarkPendingUpgrade(dataDir, PendingRecord{
FromVer: "1.4.0", FromCommit: "old",
ToVer: "1.4.1", ToCommit: "new",
BackupDir: backupDir,
Digests: map[string]string{bin: strings.Repeat("a", 64)},
}); err != nil {
t.Fatal(err)
}
rolled, _, _, err := RollbackIfUnconfirmed(dataDir, bin)
if err == nil {
t.Fatal("★ 坏备份被当成回滚成功了 —— 未做 digest 校验")
}
if rolled {
t.Error("校验失败时 rolled 必须为 false(否则调用方以为自愈完成)")
}
// ★ marker 必须仍在:失败后要保住第二次自愈机会
if rec, _ := ReadPendingUpgrade(dataDir); rec == nil {
t.Error("★★ 回滚失败却清了 marker ⇒ 失去第二次自愈机会")
}
}
// TestRollbackRemovesDepGraphAddedByUpgrade 钉住「旧 homed + 新 depgraph」崩溃循环。
//
// 场景:升级前机器没有 depgraph.bin(或备份没带上),升级包带了新的。
// 新内核起不来 ⇒ 回滚。若只换 homed 不删 depgraph,磁盘上是
// 「旧 homed + 新 depgraph」:旧 homed 内嵌的 digest 对不上,
// kernelboot 装载期直接拒绝启动,而 marker 已清 ⇒ 既起不来也回滚不了。
func TestRollbackRemovesDepGraphAddedByUpgrade(t *testing.T) {
dir := t.TempDir()
dataDir := t.TempDir()
backupDir := filepath.Join(dataDir, BackupDirPrefix+NowStamp())
if err := os.MkdirAll(backupDir, 0755); err != nil {
t.Fatal(err)
}
// 备份里**没有** depgraph.bin(升级前机器本来就没有)
oldBin := fakeBin(t, backupDir, "homed", "1.3.0", "aaa")
oldSum, _ := sha256File(oldBin)
bin := filepath.Join(dir, "homed")
if err := os.WriteFile(bin, []byte("BROKEN"), 0755); err != nil {
t.Fatal(err)
}
// 升级时装上了新的 depgraph
dgPath := filepath.Join(dir, "depgraph.bin")
if err := os.WriteFile(dgPath, []byte("NEW-DEPGRAPH"), 0644); err != nil {
t.Fatal(err)
}
if err := MarkPendingUpgrade(dataDir, PendingRecord{
FromVer: "1.3.0", FromCommit: "aaa",
ToVer: "1.4.0", ToCommit: "bbb",
BackupDir: backupDir,
HadDepGraph: false,
Digests: map[string]string{bin: oldSum},
}); err != nil {
t.Fatal(err)
}
rolled, _, _, err := RollbackIfUnconfirmed(dataDir, bin)
if err != nil {
t.Fatalf("回滚不应失败: %v", err)
}
if !rolled {
t.Fatal("应回滚")
}
// ★ 关键断言:新装上的 depgraph 必须被删掉
if _, statErr := os.Stat(dgPath); statErr == nil {
b, _ := os.ReadFile(dgPath)
t.Fatalf("★★ 残留升级时装上的 depgraph(%q)⇒ 旧 homed 的 digest 对不上 ⇒ 崩溃循环", b)
}
}
// TestApplyRefusesWhenCannotWrite 钉住"安装形态决定能不能自替换"。
//
// deb/rpm 形态下直接替换会让包管理器数据库与实际文件不一致。
// 判据:拒绝 + 给出可执行的替代命令,且**磁盘上什么都没变**。
func TestApplyRefusesWhenCannotWrite(t *testing.T) {
dir := t.TempDir()
bin := fakeBin(t, dir, "homed", "1.4.0", "old")
orig, _ := os.ReadFile(bin)
root := newPkgRoot(t, "1.9.9", "new")
// dataDir 为空必须拒绝:无 pending 标记就无回滚能力。
// 这是最前置的守卫,位于预检/备份**之前**。
if _, err := Apply(root, bin, "", "", nil); err == nil {
t.Fatal("dataDir 为空时必须拒绝升级(无 marker 就无回滚能力)")
}
after, _ := os.ReadFile(bin)
if string(after) != string(orig) {
t.Error("★ 被拒绝的升级不应改动任何文件")
}
}
// TestApplyRefusesWhenBinPathEmpty 钉住“binPath 为空必须拒绝”。
//
// ## 为什么要单开一个用例(这是一个真实的误伤现场)
//
// 早先 cmd/homed-upgrade 的 resolveBinPath 在“同目录找不到 homed”时
// 兜底到 os.Executable()。后果:
//
// 本 CLI 叫 homed-upgrade,与 homed **不同名** ⇒ 它把**自己**当成
// 内核 ⇒ “升级 homed”实际变成“把 CLI 覆盖成新包里的 homed”。
//
// 一个 1 字节的假包就能把工具本体换成一段 shell 脚本,而全程无报错。
//
// 现在两道闸:CLI 找不到就返回空并退出;Apply 对空 binPath 直接拒绝。
// 这个用例钉的是**第二道闸**。
func TestApplyRefusesWhenBinPathEmpty(t *testing.T) {
dataDir := t.TempDir()
root := newPkgRoot(t, "1.9.9", "new")
if _, err := Apply(root, "", "", dataDir, nil); err == nil {
t.Fatal("★ binPath 为空时必须拒绝:否则会把未知文件(甚至本工具自己)当成内核替换掉")
}
// 拒绝时不能留下任何痕迹:不能建备份、不能写 marker。
entries, _ := os.ReadDir(dataDir)
for _, e := range entries {
t.Errorf("★ 被拒绝的升级不应在 dataDir 留下 %q", e.Name())
}
}
// TestDetectRefusesUnknownInstall 钉住“探测不出形态就不允许自替换”。
//
// 失败方向必须是收紧:Unknown 意味着“我们不知道这个东西归谁管”,
// 猜错的代价是破坏包管理器数据库,那个后果比“不能升级”严重得多。
//
// ★ 本用例**只在非 root 下有效**,root 下直接 Skip。
//
// 为什么:root 无视目录权限位,chmod 0555 对它依旧可写,于是
// dirWritable 返回 true、用例恒红。强行用 root 跑会让它变成
// “CI 里必红/本机必绿”的假信号 —— 本仓已经吃过这类亏
// (用“本机是否装了 homed”验版本推断,CI 假绿、生产真红)。
// 所以:能跑就当真值验,不能跑就明说跳过,不以假结论填充。
func TestDetectRefusesUnknownInstall(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("root 无视目录权限位,本用例无法构造“不可写”状态")
}
dir := t.TempDir()
bin := fakeBin(t, dir, "homed", "1.4.0", "x")
if err := os.Chmod(dir, 0555); err != nil {
t.Skipf("无法改目录权限: %v", err)
}
t.Cleanup(func() { os.Chmod(dir, 0755) })
c := Detect(bin, t.TempDir())
if c.CanWriteBin {
t.Errorf("★ 目录不可写时不得声称可以自替换(kind=%s)", c.Kind)
}
if c.Reason == "" {
t.Error("拒绝时必须给出可执行的原因(不能只有 bool)")
}
}
// TestDetectContainerRefusesSelfReplace 钉住容器内的判定(不依赖权限)。
//
// 容器里文件系统随容器销毁还原,自替换毫无意义。这条判据跨环境稳定:
// 它不依赖运行时的 euid,只依赖 /.dockerenv 之类的标记。
// 在本机(非容器)时它测的是“非容器不应被误判成容器”。
func TestDetectContainerRefusesSelfReplace(t *testing.T) {
c := Detect("/bin/sh", t.TempDir())
if c.Kind == KindContainer && c.CanWriteBin {
t.Error("★ 容器内不得声称可以自替换")
}
if c.Kind != KindContainer && inContainer() {
t.Error("★ 在容器内却未识别出容器形态")
}
}
// TestApplyRecordsDigests 钉住"Apply 必须把升级前文件的 digest 写进 marker"。
//
// 没有 digest 就没有可靠的旧版本回滚判据(旧内核没有 --version)。
func TestApplyRecordsDigests(t *testing.T) {
dir := t.TempDir()
dataDir := t.TempDir()
bin := fakeBin(t, dir, "homed", "1.4.0", "old")
wantSum, err := sha256File(bin)
if err != nil {
t.Fatal(err)
}
root := newPkgRoot(t, "1.5.0", "new")
out, err := Apply(root, bin, "", dataDir, nil)
if err != nil {
t.Fatalf("Apply 失败: %v", err)
}
if out.To.Version != "1.5.0" {
t.Errorf("新版本不对: %s", out.To.Version)
}
rec, rerr := ReadPendingUpgrade(dataDir)
if rerr != nil || rec == nil {
t.Fatalf("Apply 成功后必须有 pending marker: rec=%v err=%v", rec, rerr)
}
if got, ok := rec.Digests[bin]; !ok {
t.Fatalf("★ marker 缺少 %s 的 digest ⇒ 未来无法可靠回滚到旧版本", bin)
} else if got != wantSum {
t.Errorf("digest 记错:期望 %s 实际 %s", ShortSum(wantSum), ShortSum(got))
}
// (2026-11-04 删掉一个占位空 if:`!rec.HadDepGraph == true && rec.HadDepGraph`
// 是永假表达式,块体只有注释 ⇒ staticcheck SA9003。真正的断言在下一行。)
if rec.HadDepGraph {
t.Error("机器上本来没有 depgraph.bin,HadDepGraph 应为 false")
}
}
// TestApplyRestoresFullyBeforeClearingMarker 钉住"还原完整才清 marker"。
//
// 旧实现先清 marker 再还原,于是"还原也失败"时磁盘上是半新半旧、
// 而 marker 已经没了 ⇒ 失去第二次自愈机会。
func TestApplyRestoresFullyBeforeClearingMarker(t *testing.T) {
dir := t.TempDir()
dataDir := t.TempDir()
bin := fakeBin(t, dir, "homed", "1.4.0", "old")
orig, _ := os.ReadFile(bin)
root := newPkgRoot(t, "1.5.0", "new")
// 让替换阶段的 homed 目标变成"不可 rename":
// 把临时文件路径占成一个目录 ⇒ CopyFileAtomic 在 OpenFile 阶段失败。
// 这样 homed 没被替换,restore 阶段也应成功还原(因为什么都没改)。
blocker := bin + atomicTmpSuffix
if err := os.MkdirAll(blocker, 0755); err != nil {
t.Fatal(err)
}
_, err := Apply(root, bin, "", dataDir, nil)
if err == nil {
t.Fatal("替换失败时应返回 error")
}
// 文件必须原样(restore 成功)
after, _ := os.ReadFile(bin)
if string(after) != string(orig) {
t.Error("★ 替换失败后 homed 未被还原")
}
}