mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 09:17:31 +00:00
fix(skillmgr): 内核旧版 skill 永不更新 —— 无 stamp 不等于"用户改过"
## 缺陷(实测,非推测)
给 kernel-plugins 补上 `skill_info(name, section=...)` 的用法说明后,
改了嵌入内容,但**生产上永远不会更新**:
/home/hologramagent/skills/kernel-plugins/ mtime 2026-10-26,无 .builtin-sha256
嵌入版本 1.1.0 vs 磁盘版本 1.0.0
`materializeBuiltinSkills` 的判断链是:
builtinSkillUnmodified(dir) → 读 .builtin-sha256
没有 stamp ⇒ false
⇒ continue("用户改过或无法确认,保留人的版本")
而 stamp 机制是 387ae94(今天早些时候)引入的。生产机上这份 skill
是**更早的内核**写的 —— 它没有 stamp,于是被永久判为"来历不明"。
后果不只是这一份文档:任何在 stamp 机制之前部署的机器,其内核自带
skill 都停在旧版。而 kernel-* 系列正是教模型怎么用工具/记忆/终端的
文档 —— 内核改进了文档,模型永远看不到。
## 修法:用 frontmatter version 区分两种"没有 stamp"
原判据把两件事混成一件:
用户改过措辞 ⇒ 必须保留
内核在 stamp 之前写的 ⇒ 应该升级
新增 builtinSkillOutdated(dir, embedded):只在**没有 stamp** 时被调用,
判据是磁盘 frontmatter 的 `version` 严格低于内嵌版本。
安全性分析(写在代码注释里):
人改措辞 通常不动 version ⇒ 不低于 ⇒ 保留 ✓
人把 version 改高 ⇒ 保留 ✓
内核旧版(无 stamp) ⇒ 升级 ✓(这正是要修的)
解析不出版本号 ⇒ 返回 false ⇒ 保留(保守)
误升级的唯一条件是"人把 version 改低",而那本身就意味着放弃内核版本。
原有的四条保守判据(含 NoStampIsConservative)全部保持绿。
## 判据抓到了我自己写的 bug
TestCompareVersion 立刻红了:
compareVersion("1.0","1.0.0")=-1,期望 0
原因:缺失段留成空串 ⇒ Atoi("") 失败 ⇒ 落到字符串比较 ⇒ `"" < "0"`
⇒ 版本号少写一段的 skill 会被无谓地重复升级。改为缺失段视为 "0"。
## 变异验证
builtinSkillOutdated 恒 return false(= 修复前行为)
⇒ TestMaterializeBuiltinSkills_UpgradesStaleKernelVersion 红:
"★ 没有 stamp 的内核旧版(version 0.0.1)没被升级"
compareVersion 缺失段改回空串
⇒ TestCompareVersion 红:"compareVersion(\"1.0\",\"1.0.0\")=-1,期望 0"
## 顺带:kernel-plugins v1.1.0
补一节「Reading a skill without burning context」,说明:
- 长文档返回目录 + 开头 30 行,目录每行带 (N 行)
- `section` 可按编号或标题子串取,大小写不敏感,容忍 `##` 前缀
- 顶层章节返回整棵子树
- 未命中会列出全部可用章节
- 章节边界尊重围栏代码块
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + skillmgr 全绿
(包数 63 → 62 是因 clawhubadapter 已在 a0077a2 迁出为外部插件,与本改动无关)
This commit is contained in:
@ -29,6 +29,7 @@ package skillmgr
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -176,3 +177,99 @@ func TestMaterializeBuiltinSkills_Idempotent(t *testing.T) {
|
||||
t.Fatal("内容未变却重写了文件(mtime 变了)")
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 没有 stamp 的**内核旧版**必须能升级(2026-11-04 修的实测缺陷)
|
||||
//
|
||||
// 背景:stamp 机制由 387ae94 引入,而生产机上 kernel-plugins 是更早的
|
||||
// 内核写的(mtime 2026-10-26),没有 .builtin-sha256 ⇒ unmodified 恒为
|
||||
// false ⇒ 这份 skill **永不更新**。后果:内核改进了 skill 文档,已部署
|
||||
// 的机器永远拿不到 —— 而它正是教模型怎么用工具的文档。
|
||||
//
|
||||
// 与 TestMaterializeBuiltinSkills_NoStampIsConservative 的关系:
|
||||
// 那条判据守"来历不明的旧内容别覆盖",这条守"内核旧版要升级"。
|
||||
// 两者的分界线是 frontmatter 里的 version:版本更高 = 内核发布了新版。
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
// TestMaterializeBuiltinSkills_UpgradesStaleKernelVersion 钉住升级路径。
|
||||
//
|
||||
// 变异:把 builtinSkillOutdated 的返回值改成恒 false ⇒ 本例红。
|
||||
func TestMaterializeBuiltinSkills_UpgradesStaleKernelVersion(t *testing.T) {
|
||||
dir := writeBuiltinFixture(t)
|
||||
|
||||
// 模拟"没有 stamp 的内核旧版":version 刻意写低。
|
||||
stale := "---\nname: kernel-memory\ndescription: old\nversion: 0.0.1\n---\n\n# 旧内容\n"
|
||||
putSkill(t, dir, stale, false)
|
||||
|
||||
materializeBuiltinSkills(dir)
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(dir, "kernel-memory", skillFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("读回: %v", err)
|
||||
}
|
||||
if strings.Contains(string(got), "旧内容") {
|
||||
t.Fatalf("★ 没有 stamp 的内核旧版(version 0.0.1)没被升级 ——\n"+
|
||||
"这正是生产上 kernel-plugins 永不更新的原因。\n实际内容: %q", string(got))
|
||||
}
|
||||
if !strings.Contains(string(got), "version: 1.") && !strings.Contains(string(got), "version: 2.") {
|
||||
t.Logf("升级后版本: %s", skillVersionOf(string(got)))
|
||||
}
|
||||
// 升级后必须补上 stamp,否则下次又会走"无 stamp"分支
|
||||
if _, err := os.Stat(filepath.Join(dir, "kernel-memory", builtinStampName)); err != nil {
|
||||
t.Fatalf("★ 升级后没写 stamp:%v(下次仍会被当成来历不明)", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMaterializeBuiltinSkills_KeepsHigherUserVersion 钉住反向:
|
||||
// 用户把 version 改得比内嵌更高时,**必须保留**(那是人明确要的版本)。
|
||||
//
|
||||
// 变异:把 compareVersion < 0 改成 <= 0 ⇒ 相等也升级 ⇒ 本例如改高则红。
|
||||
func TestMaterializeBuiltinSkills_KeepsHigherUserVersion(t *testing.T) {
|
||||
dir := writeBuiltinFixture(t)
|
||||
custom := "---\nname: kernel-memory\ndescription: mine\nversion: 99.0.0\n---\n\n# 我的定制版\n"
|
||||
putSkill(t, dir, custom, false)
|
||||
|
||||
materializeBuiltinSkills(dir)
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(dir, "kernel-memory", skillFileName))
|
||||
if err != nil {
|
||||
t.Fatalf("读回: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(got), "我的定制版") {
|
||||
t.Fatalf("★ 用户把 version 改成 99.0.0 后仍被覆盖(现在 %q)——\n"+
|
||||
"version 更高意味着人明确要这个版本,必须保留", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestCompareVersion 钉住版本比较本身(判据的基础)。
|
||||
func TestCompareVersion(t *testing.T) {
|
||||
cases := []struct {
|
||||
a, b string
|
||||
want int
|
||||
}{
|
||||
{"1.0.0", "1.0.0", 0},
|
||||
{"1.0.0", "1.0.1", -1},
|
||||
{"1.1.0", "1.0.0", 1},
|
||||
{"1.0", "1.0.0", 0}, // 缺段视为 0
|
||||
{"2", "10", -1}, // 数值比较,不是字符串("2" > "10" 是字符串陷阱)
|
||||
{"1.0.0", "1.0.0-beta", -1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := compareVersion(c.a, c.b); got != c.want {
|
||||
t.Errorf("compareVersion(%q,%q)=%d,期望 %d", c.a, c.b, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSkillVersionOfOnlyReadsFrontmatter 钉住版本解析只看 frontmatter。
|
||||
//
|
||||
// 正文里的 `version:` 可能是示例代码,误读会让升级判据做出错误决定。
|
||||
func TestSkillVersionOfOnlyReadsFrontmatter(t *testing.T) {
|
||||
content := "---\nname: x\nversion: 1.2.3\n---\n\n正文\n\n```yaml\nversion: 9.9.9\n```\n"
|
||||
if got := skillVersionOf(content); got != "1.2.3" {
|
||||
t.Fatalf("★ 解析到 %q,期望 1.2.3(正文里的 version 不应被读)", got)
|
||||
}
|
||||
if got := skillVersionOf("没有 frontmatter 的文档\nversion: 5.0.0\n"); got != "" {
|
||||
t.Fatalf("★ 无 frontmatter 却解析出 %q,期望空", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user