mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 17:27:31 +00:00
fix(update): dataDir 取内核权威源 + 回滚可信校验 + marker 原子写
承上一个 commit(回退 85fd38b)。本 commit 是 R2 主体:B0/B1/B2 三项修复,
以及为 P1–P4 补齐的**行为级**判据。
━━━ B0 · dataDir 不再推导,取内核真实 -data ━━━
`internal/update/kernel.go`
· 删除 ResolveDataDir(env → `<exe>/data` 兜底)。
· 新增 SetKernelDataDir / KernelDataDir:值由内核适配层在启动时
用 ks.DataDir 注入。进程内调用方(pluginmgr / webui)直接取,
不做任何推导;取不到返回**空串**。
· DataDirEnv 保留但降级为诊断参考(不再是合法来源)。
`internal/kernelmod/update/mod.go`
· Init 里 upd.SetKernelDataDir(ks.DataDir);为空时打响亮告警
(不阻断启动 —— 第 0 层职责是回滚判定,不是参数校验)。
`internal/plugins/pluginmgr/kernel_update.go`、`internal/plugins/webui/kernel_update.go`
· kernelDataDir / dataDirOf 都改为 return update.KernelDataDir()。
`cmd/homed-upgrade/main.go`
· upgradeDataDir(explicit):① `-data <dir>`;② config.db 的
core.daemon.data_dir(只读连接 + PRAGMA query_only=1,不建表不迁移)。
· 两者都拿不到 ⇒ 返回空串 ⇒ `-apply` 在动手之前就 fail 并说明怎么修。
→ 这样 P1 的空值守卫重新变成**活的**:拿不到真实 dataDir 就真的会拦。
━━━ B1 · 回滚可信:回读校验是唯一判据(判据 B1)━━━
`internal/update/rollback.go` — RollbackIfUnconfirmed
· rollback.go:315 `rb, verr := readBinVersionStrict(binPath)`
替换后**必须**回读成功且 rb.Version == rec.FromVer,否则
rolled=false + error + **不清 marker**(下次启动再试,并让人看到日志)。
CopyFile 返回 nil 只代表字节搬完了,不代表搬过去的东西能执行。
· rollback.go:254 确认分支同样改用 readBinVersionStrict。
这里曾经用 ReadBinVersion —— 它含 InferVersionFromRunning 回退,
已实测:binPath 是不可执行残文件、marker.ToVer 恰好等于本机 waiter
版本时,会从 waiter **借到**版本号 ⇒ confirmed=true ⇒ 清 marker、
不回滚 ⇒ 机器带着跑不起来的二进制继续启动且失去自愈机会。
该缺陷环境相关(CI 没装 homed/waiter 会绿),故判据不能依赖本机环境。
· 备份缺失 / 回读失败 / 版本不符 / depgraph 回滚失败 —— 四种情形
一律不清 marker。
· 新增 readBinVersionStrict(kernel.go):只认「这个文件自己能报出版本」,
无任何回退;ReadBinVersion 改为其包装器(保留探测用回退),
注释写明它**不能**用作回滚校验。
━━━ B2 · marker 原子 + 损坏不卡启动(判据 B2)━━━
`internal/update/rollback.go` — MarkPendingUpgrade
· 旧实现 os.WriteFile 直接覆写(O_TRUNC),掉电留半截 JSON。
· 现改为:写 <path>.tmp → fsync(文件) → rename(原子) → fsync(目录)。
目录 fsync 失败仅告警(容器/tmpfs 下 EINVAL 不该阻断升级)。
rollback.go:112 `if err := f.Sync(); err != nil {` 即这条链路的第一道闸。
· RollbackIfUnconfirmed 遇到解析不了的 marker:quarantineMarker 改名
留存(不删,保留现场)+ 告警 + return false,"","",nil
⇒ 当作「无待确认升级」继续启动。把内核卡在启动阶段是最差选择:
能执行到这行已证明当前二进制可执行。
`internal/kernelmod/update/mod.go`
· 补注释钉住语义:rerr != nil 必须中止启动;但 marker **损坏**不算
失败(已由 RollbackIfUnconfirmed 吞掉并改名留存)。
━━━ 判据补齐(行为级,修掉 M6 真空洞)━━━
`internal/update/kernel_apply_test.go`
· P1 TestApplyRefusesEmptyDataDir 加强:切到 t.TempDir() 并拍快照,
断言拒绝升级时**根本没写任何文件**(旧用例只查「目标二进制没变」,
把守卫挪到预检之后依旧绿 —— 因为 filepath.Join("", prefix) 会把
备份目录建到 CWD)。
· P3 新增 TestApplyPrunesOldBackups:预置 5 个历史备份后跑真实 Apply,
断言旧的被 GC。★ 这是补 M6 的真空洞 —— 旧的变异脚本只跑
`-run TestPruneOldBackups`(只证明函数能跑,3 毫秒),把 Apply 里
那一行 PruneOldBackups(dataDir) 删掉它照样绿;新脚本必须同时跑
TestApplyPrunesOldBackups,删掉调用后立刻报红。
「函数能跑」≠「Apply 真的调了它」。
· B1 新增 TestRollback_RefusesCorruptBackup(零字节/坏备份 ⇒ 必须
回滚失败且不清 marker)、TestRollback_RefusesVersionMismatch
(备份版本 ≠ FromVer ⇒ 同样失败)、
TestRollback_ConfirmedBranchIgnoresInferredVersion 与
TestConfirmedBranch_StrictProbeBitesOnUnparseable(确认分支不得
使用推断值;用可注入的 inferHomedPath/inferWaiterPath 造出
「能借到版本」的场景,不依赖本机是否装了 homed/waiter)。
· B2 新增 TestMarkPendingUpgrade_IsAtomic、
TestRollback_CorruptMarkerDoesNotBlockBoot。
`internal/update/rollback_test.go`
· TestRollback_RestoresBackup / TestRollback_RollsBackWhenRunningOldVersion
的备份从纯字节 "OLD-GOOD" 改为**真正可执行**的假二进制(1.4.0),
并按版本而非按字节断言。旧的写法只能测「字节搬对了」,测不到
「搬过去的东西能跑」—— 而后者才是回滚能救机器的前提。
`internal/update/kernel.go`
· inferHomedPath / inferWaiterPath 提为包级变量(生产默认值不变),
唯一目的是让「确认分支不得使用推断值」这条判据可稳定复现。
━━━ 判据锚点自检(grep -F 命中数均为 1,行号已核对)━━━
P1 kernel.go:348 if dataDir == "" {
P2 kernel.go:370 backup := filepath.Join(dataDir, BackupDirPrefix+ts)
P3 kernel.go:449 PruneOldBackups(dataDir)
P4 kernel.go:445 publishResult(bus, "kernel", "", true, …)
P4 events.go:69 "error": errMsg,
B1 rollback.go:254 if cur, cerr := readBinVersionStrict(binPath); cerr == nil {
B1 rollback.go:315 rb, verr := readBinVersionStrict(binPath)
B2 rollback.go:112 if err := f.Sync(); err != nil {
验证:go build ./... 通过;go test ./internal/update/ 通过。
This commit is contained in:
@ -106,6 +106,22 @@ func TestApplyRefusesEmptyDataDir(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ★★★ 关键:在**临时目录**里跑,并拍下跑之前的快照。
|
||||
//
|
||||
// 为什么必须切 CWD:守卫万一被挪到后面(先预检/先备份才判空),
|
||||
// filepath.Join("", prefix) 会得到一个**相对路径**,于是备份目录被
|
||||
// 建到当前目录下。旧用例只查了"目标二进制没变",这种 "什么都没写"
|
||||
// 的违规完全看不到(变异实测:把守卫挪到预检之后,旧用例依旧绿)。
|
||||
workDir := t.TempDir()
|
||||
oldWD, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chdir(workDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { os.Chdir(oldWD) })
|
||||
|
||||
out, err := Apply(root, binPath, newDG, "", nil)
|
||||
if err == nil {
|
||||
t.Fatalf("dataDir 为空时必须拒绝升级,实际返回 out=%+v, err=nil", out)
|
||||
@ -131,6 +147,21 @@ func TestApplyRefusesEmptyDataDir(t *testing.T) {
|
||||
if FileExists(binPath + ".tmp-upgrade") {
|
||||
t.Error("拒绝升级却留下了 .tmp-upgrade 临时文件")
|
||||
}
|
||||
// ④ ★ 根本没有写任何东西:备份目录/pending 标记都不许出现。
|
||||
// "不写任何文件"是 H1 的原始承诺(拒绝要在**预检之前**发生)。
|
||||
// 备份目录会因为 filepath.Join("", …) 而变成相对路径 —— 那种情况下
|
||||
// 它会在 CWD 里,所以这里必须盯住 CWD。
|
||||
entries, derr := os.ReadDir(workDir)
|
||||
if derr != nil {
|
||||
t.Fatal(derr)
|
||||
}
|
||||
if len(entries) != 0 {
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
names = append(names, e.Name())
|
||||
}
|
||||
t.Errorf("拒绝升级却写了文件(dataDir 为空时的守卫必须发生在动任何文件之前): %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
// ── H2:备份目录必须落在 dataDir 下,不能是 /root ────────────
|
||||
@ -393,3 +424,351 @@ func TestApplySuccessEmitsResult(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── M6:Apply 必须**真的调用** PruneOldBackups(行为级,不是"函数能跑")──
|
||||
|
||||
// TestApplyPrunesOldBackups 钉住"Apply 成功路径真的执行了 GC"。
|
||||
//
|
||||
// ★ 这一条是补 M6 真空洞的。此前只有 TestPruneOldBackups,它直接调
|
||||
// PruneOldBackups —— 把 Apply 里那一行 `PruneOldBackups(dataDir)` 整个删掉,
|
||||
// 它照样绿。也就是说旧判据只证明了"函数能跑",**没证明 Apply 调了它**,
|
||||
// 而"没人调用"恰恰是 H3 的原始缺陷形态(注释承诺了 GC,代码里没有)。
|
||||
//
|
||||
// 做法:预置超量备份(含一个很旧的),跑一次真实 Apply,断言旧的被清掉。
|
||||
// 这条路只有 Apply 内部真调了 GC 才会绿。
|
||||
func TestApplyPrunesOldBackups(t *testing.T) {
|
||||
binPath, _ := installFakeKernel(t)
|
||||
root := newPkgRoot(t, "1.9.9", "newcommit")
|
||||
dataDir := t.TempDir()
|
||||
|
||||
// 预置 5 个"历史备份",最旧的那个应该在本次 Apply 后被 GC 掉
|
||||
oldTS := []string{
|
||||
"20200101-010101", "20200102-010101", "20200103-010101",
|
||||
"20200104-010101", "20200105-010101",
|
||||
}
|
||||
for _, ts := range oldTS {
|
||||
if err := os.MkdirAll(filepath.Join(dataDir, BackupDirPrefix+ts), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Apply(root, binPath, "", dataDir, nil); err != nil {
|
||||
t.Fatalf("Apply 应成功: %v", err)
|
||||
}
|
||||
|
||||
// 5 个旧的 + 本次新增 1 个 = 6 个;保留最近 3 个 ⇒ 最旧的 3 个必须没了。
|
||||
remaining := listStampDirs(t, dataDir, BackupDirPrefix)
|
||||
if len(remaining) != 3 {
|
||||
t.Fatalf("Apply 后应只剩 3 个备份(GC 未被调用?),实际 %d 个: %v", len(remaining), remaining)
|
||||
}
|
||||
for _, ts := range oldTS[:3] {
|
||||
if FileExists(filepath.Join(dataDir, BackupDirPrefix+ts)) {
|
||||
t.Errorf("Apply 后最旧的备份 %s 应已被 GC 清除", ts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── B1:回滚必须回读校验,校验不过不得清 marker ─────────────
|
||||
|
||||
// TestRollback_RefusesCorruptBackup 钉住"拷贝成功 ≠ 回滚成功"。
|
||||
//
|
||||
// ★ 这是已复现过的事故形态:备份是**零字节**文件时,CopyFile 会成功
|
||||
// (字节确实搬完了),旧代码于是清掉 marker、返回 rolled=true, err=nil,
|
||||
// 日志还打 "rolled back to 1.4.0"。而磁盘上躺着的是一个不可执行的
|
||||
// 二进制 ⇒ 机器带坏二进制继续启动,且 marker 已清 ⇒ **失去第二次自愈机会**。
|
||||
//
|
||||
// 正确行为:回读 ReadBinVersion 失败 ⇒ 返回 error、rolled=false、
|
||||
// **marker 必须还在**(下一次启动还能再试 / 人能介入)。
|
||||
func TestRollback_RefusesCorruptBackup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// 当前跑的是 1.4.0,与标记的 ToVer(1.4.1) 不同 ⇒ 进入回滚分支
|
||||
bin := writeVersionScript(t, dir, "homed", "1.4.0", "oldcommit")
|
||||
orig, _ := os.ReadFile(bin)
|
||||
|
||||
backupDir := filepath.Join(dir, "backup")
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 零字节的坏备份:CopyFile 会"成功"
|
||||
if err := os.WriteFile(filepath.Join(backupDir, "homed"), []byte{}, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := MarkPendingUpgrade(dir, PendingRecord{
|
||||
FromVer: "1.4.0", FromCommit: "oldcommit",
|
||||
ToVer: "1.4.1", ToCommit: "newcommit",
|
||||
BackupDir: backupDir,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rolled, _, _, err := RollbackIfUnconfirmed(dir, bin)
|
||||
if err == nil {
|
||||
t.Fatal("★★ 拿不可执行的备份\"回滚成功\"了 —— 未做回读校验")
|
||||
}
|
||||
if rolled {
|
||||
t.Error("回滚校验失败时 rolled 必须为 false(否则调用方以为自愈完成)")
|
||||
}
|
||||
// ★ marker 必须仍在:失败后要保住第二次自愈机会
|
||||
if rec, _ := ReadPendingUpgrade(dir); rec == nil {
|
||||
t.Error("★★ 回滚失败却清了 marker ⇒ 失去第二次自愈机会")
|
||||
}
|
||||
// 错误必须可执行:指出是回读失败 + 让人去换二进制
|
||||
msg := err.Error()
|
||||
if !strings.Contains(msg, "回读") && !strings.Contains(msg, "不可执行") {
|
||||
t.Errorf("错误应说明回读校验失败,实际: %v", msg)
|
||||
}
|
||||
_ = orig
|
||||
}
|
||||
|
||||
// TestRollback_RefusesVersionMismatch 钉住"版本非得是标记里的 FromVer"。
|
||||
//
|
||||
// 备份存在且可执行,但版本不是当初记录的 FromVer(备份与 marker 不匹配,
|
||||
// 例如备份目录被别的升级覆盖过)。这种情况不算回滚成功 —— 装回去的是
|
||||
// 一个谁也不认识的版本,而 marker 里承诺的是 FromVer。
|
||||
func TestRollback_RefusesVersionMismatch(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := writeVersionScript(t, dir, "homed", "1.4.0", "oldcommit")
|
||||
|
||||
backupDir := filepath.Join(dir, "backup")
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 备份是**可执行**的,但版本是 1.3.0(≠ marker.FromVer 1.4.0)
|
||||
src := writeVersionScript(t, t.TempDir(), "homed", "1.3.0", "weird")
|
||||
if err := CopyFile(src, filepath.Join(backupDir, "homed")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := MarkPendingUpgrade(dir, PendingRecord{
|
||||
FromVer: "1.4.0", FromCommit: "oldcommit",
|
||||
ToVer: "1.4.1", ToCommit: "newcommit",
|
||||
BackupDir: backupDir,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rolled, _, _, err := RollbackIfUnconfirmed(dir, bin)
|
||||
if err == nil {
|
||||
t.Fatal("版本与标记的 FromVer 不符时必须报错")
|
||||
}
|
||||
if rolled {
|
||||
t.Error("版本不符时 rolled 必须为 false")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "1.3.0") {
|
||||
t.Errorf("错误应报出实际版本,实际: %v", err)
|
||||
}
|
||||
if rec, _ := ReadPendingUpgrade(dir); rec == nil {
|
||||
t.Error("版本不符时 marker 必须保留")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRollback_CorruptMarkerDoesNotBlockBoot 钉住 B2 的第二半:
|
||||
// marker 损坏(掉电截断)**不得**让内核拒绝启动。
|
||||
//
|
||||
// ★ 已复现的事故形态:marker 写成 `{"from_version":"1.4.0","to_` 时,
|
||||
// ReadPendingUpgrade 返回 error,旧代码原样上抛,mod.go `return rerr`
|
||||
// ⇒ 内核拒启动,而且走不到回滚分支 ⇒ **既起不来也回滚不了**。
|
||||
//
|
||||
// 正确行为:返回 (false, "", "", nil) —— 当作"无待确认升级"继续启动,
|
||||
// 同时把坏 marker 改名留存(不删,保留现场)。
|
||||
func TestRollback_CorruptMarkerDoesNotBlockBoot(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
bin := writeVersionScript(t, dir, "homed", "1.4.0", "oldcommit")
|
||||
|
||||
p := pendingPath(dir)
|
||||
bad := []byte(`{"from_version":"1.4.0","to_`)
|
||||
if err := os.WriteFile(p, bad, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rolled, from, to, err := RollbackIfUnconfirmed(dir, bin)
|
||||
if err != nil {
|
||||
t.Fatalf("★★ marker 损坏不得让内核拒启动(既起不来也回滚不了): %v", err)
|
||||
}
|
||||
if rolled {
|
||||
t.Error("marker 读不懂时无法判定回滚目标,rolled 应为 false")
|
||||
}
|
||||
if from != "" || to != "" {
|
||||
t.Errorf("无回滚时版本轨迹应为空,实际 %s → %s", from, to)
|
||||
}
|
||||
// 坏 marker 应被改名留存(保留现场),而不是原样留着(否则下次启动又报一次)
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
t.Error("损坏的 marker 应被隔离改名,否则每次启动都会再撞一次")
|
||||
}
|
||||
entries, _ := os.ReadDir(dir)
|
||||
foundQuarantine := false
|
||||
for _, e := range entries {
|
||||
if strings.Contains(e.Name(), ".corrupt-") {
|
||||
foundQuarantine = true
|
||||
}
|
||||
}
|
||||
if !foundQuarantine {
|
||||
t.Error("损坏的 marker 应被改名留存(排查\"上次升级发生了什么\"的唯一线索)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMarkPendingUpgrade_IsAtomic 钉住 B2 的第一半:写 marker 不留半截。
|
||||
//
|
||||
// 掉电时最坏的结果必须是"完整的旧版"或"完整的新版",不能是半截 JSON。
|
||||
// 判据:写完不留下 .tmp 残留,且内容可被完整解析。
|
||||
// (真正的掉电无法在单测里模拟,这里钉住原子写带来的可观测后果。)
|
||||
func TestMarkPendingUpgrade_IsAtomic(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
rec := PendingRecord{FromVer: "1.4.0", FromCommit: "a", ToVer: "1.4.1", ToCommit: "b"}
|
||||
if err := MarkPendingUpgrade(dir, rec); err != nil {
|
||||
t.Fatalf("写标记: %v", err)
|
||||
}
|
||||
// ① 不留 .tmp 残留(rename 成功)
|
||||
if FileExists(pendingPath(dir) + ".tmp") {
|
||||
t.Error("写完后不应残留 .tmp 临时文件")
|
||||
}
|
||||
// ② 内容完整可解析
|
||||
got, err := ReadPendingUpgrade(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("读回: %v", err)
|
||||
}
|
||||
if got == nil || got.FromVer != "1.4.0" || got.ToVer != "1.4.1" {
|
||||
t.Errorf("读回内容不对: %+v", got)
|
||||
}
|
||||
// ③ 覆写已有 marker 也要成功(旧版 WriteFile 截断写,这里同样不能坏)
|
||||
if err := MarkPendingUpgrade(dir, PendingRecord{FromVer: "2.0.0", ToVer: "2.1.0"}); err != nil {
|
||||
t.Fatalf("覆写标记: %v", err)
|
||||
}
|
||||
got2, err := ReadPendingUpgrade(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("覆写后读回: %v", err)
|
||||
}
|
||||
if got2.FromVer != "2.0.0" {
|
||||
t.Errorf("覆写未生效: %+v", got2)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 确认分支不得使用推断值(B1 的同一个病,转移到了 confirmed)──
|
||||
|
||||
// TestRollback_ConfirmedBranchIgnoresInferredVersion 钉住"确认新版已跑起来"
|
||||
// 只能靠**binPath 自己**报版本,不能用推断回退借来的值。
|
||||
//
|
||||
// ★ 已实证的事故形态(环境相关,最难查的那类):
|
||||
// - binPath 写成不可执行的残 file
|
||||
// - marker.ToVer 恰等于本机可推断到的版本(如 /usr/local/bin/waiter 是 1.4.0)
|
||||
// - ReadBinVersion 失败后走 InferVersionFromRunning 回退,从 waiter 借到
|
||||
// 1.4.0 ⇒ 与 ToVer 相等 ⇒ confirmed=true ⇒ 清 marker、不回滚
|
||||
// - 实测输出:rolled=false err=nil,marker 被清除
|
||||
// ⇒ 机器带着跑不起来的二进制继续启动,且失去自愈机会。
|
||||
//
|
||||
// ★★ 判据自身必须**不依赖本机装没装 homed/waiter**,否则 CI 假绿、生产真红。
|
||||
// 探针路径(inferWaiterPath/inferHomedPath)是包级变量,这里临时指到
|
||||
// t.TempDir() 下自己造的假 waiter,跑完还原。这样判据在任何机器上都
|
||||
// **确实**会走到"能借到版本"的成立条件,而不是靠环境碰运气:
|
||||
// - confirmed 用 ReadBinVersion ⇒ 借到 9.9.9 ⇒ 判据 RED
|
||||
// - confirmed 用 readBinVersionStrict ⇒ 借不到 ⇒ 走回滚 ⇒ 判据 GREEN
|
||||
func TestRollback_ConfirmedBranchIgnoresInferredVersion(t *testing.T) {
|
||||
// 把推断逻辑可读的两个探针都指到可写临时目录(并断言前提成立)。
|
||||
savedHomed, savedWaiter := inferHomedPath, inferWaiterPath
|
||||
t.Cleanup(func() { inferHomedPath, inferWaiterPath = savedHomed, savedWaiter })
|
||||
|
||||
probe := t.TempDir()
|
||||
inferHomedPath = filepath.Join(probe, "homed") // 不存在:手段 1 必然失效
|
||||
inferWaiterPath = filepath.Join(probe, "waiter") // 下面种一个能报版本的
|
||||
body := "#!/bin/sh\nprintf 'waiter 9.9.9 (commit deadbeef, built 2026-01-01T00:00:00Z)\\n'\n"
|
||||
if err := os.WriteFile(inferWaiterPath, []byte(body), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// ① 前提校验:推断逻辑必须**真的**能从它借到版本。
|
||||
// 借不到就说明这条判据无法区分两种实现(会假绿)—— 直接 Fatal 而不是 Skip:
|
||||
// 现在探针路径是我们自己控制的,借不到就是判据自身坏了。
|
||||
if v := InferVersionFromRunning(); v == nil || v.Version != "9.9.9" {
|
||||
t.Fatalf("判据前提不成立:推断逻辑未从假 waiter 借到 9.9.9(得到 %+v)", v)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "homed")
|
||||
// 不可执行的残 file:strict 读出版本必然失败
|
||||
if err := os.WriteFile(bin, []byte("THIS-IS-NOT-A-BINARY"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 可用的备份(版本 = FromVer),保证"走回滚"这条路能成功,
|
||||
// 这样 confirmed 判错的后果只体现在"该不该回滚"上,不被别的失败掩盖。
|
||||
backupDir := filepath.Join(dir, "backup")
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeVersionScript(t, backupDir, "homed", "1.4.0", "oldcommit")
|
||||
|
||||
// marker.ToVer 恰好等于推断逻辑能借到的版本 9.9.9
|
||||
if err := MarkPendingUpgrade(dir, PendingRecord{
|
||||
FromVer: "1.4.0", FromCommit: "oldcommit",
|
||||
ToVer: "9.9.9", ToCommit: "deadbeef",
|
||||
BackupDir: backupDir,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rolled, from, to, err := RollbackIfUnconfirmed(dir, bin)
|
||||
if err != nil {
|
||||
t.Fatalf("备份可用时回滚不应失败: %v", err)
|
||||
}
|
||||
// ★ 核心断言:binPath 报不出版本 ⇒ 必须回滚,绝不能因为"借来的版本"而确认
|
||||
if !rolled {
|
||||
t.Fatalf("★★★ binPath 不可执行,却被判为\"新版已跑起来\"(confirmed 用了推断值)⇒ 机器带坏二进制继续启动")
|
||||
}
|
||||
if from != "1.4.0" || to != "9.9.9" {
|
||||
t.Errorf("版本轨迹不对: %s → %s", from, to)
|
||||
}
|
||||
// ② 回读(严格)证明 binPath 真的被换成了可执行的 1.4.0
|
||||
if v, verr := readBinVersionStrict(bin); verr != nil {
|
||||
t.Errorf("回滚后 binPath 应可执行: %v", verr)
|
||||
} else if v.Version != "1.4.0" {
|
||||
t.Errorf("应回滚到 1.4.0,实际 %s", v.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfirmedBranch_StrictProbeBitesOnUnparseable 是上一条的"无反依赖"版本:
|
||||
// 直接把探针指向一个**报不出版本**的假 waiter,并断言 confirmed 仍为 false。
|
||||
//
|
||||
// 为什么还要单独一条:上一条依赖"能借到版本"这个前提成立。这一条不依赖
|
||||
// 任何前提,专门钉住"探针都失效时也不能确认"。
|
||||
func TestConfirmedBranch_StrictProbeBitesOnUnparseable(t *testing.T) {
|
||||
savedHomed, savedWaiter := inferHomedPath, inferWaiterPath
|
||||
t.Cleanup(func() { inferHomedPath, inferWaiterPath = savedHomed, savedWaiter })
|
||||
|
||||
probe := t.TempDir()
|
||||
inferHomedPath = filepath.Join(probe, "homed")
|
||||
inferWaiterPath = filepath.Join(probe, "waiter")
|
||||
// 能跑但输出里没有语义化版本 ⇒ versionFromWaiter 返回 nil
|
||||
if err := os.WriteFile(inferWaiterPath, []byte("#!/bin/sh\necho no-version-here\n"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := InferVersionFromRunning(); v != nil {
|
||||
t.Fatalf("判据前提不成立:应推断不出,实际 %+v", v)
|
||||
}
|
||||
|
||||
dir := t.TempDir()
|
||||
bin := filepath.Join(dir, "homed")
|
||||
if err := os.WriteFile(bin, []byte("THIS-IS-NOT-A-BINARY"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backupDir := filepath.Join(dir, "backup")
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeVersionScript(t, backupDir, "homed", "1.4.0", "oldcommit")
|
||||
if err := MarkPendingUpgrade(dir, PendingRecord{
|
||||
FromVer: "1.4.0", FromCommit: "oldcommit",
|
||||
ToVer: "1.4.0", ToCommit: "oldcommit", // ToVer 恰等于备份版本(最易误判的形态)
|
||||
BackupDir: backupDir,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
rolled, _, _, err := RollbackIfUnconfirmed(dir, bin)
|
||||
if err != nil {
|
||||
t.Fatalf("回滚不应失败: %v", err)
|
||||
}
|
||||
if !rolled {
|
||||
t.Error("★★ binPath 不可执行时必须回滚(不能因 ToVer 与备份版本相同就确认)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user