Files
HomeAgent/internal/config/prompt_migration_test.go
JianFeeeee a4ebcb6e96 fix(config): 播种时不再把版本号写进人格文本 + 存量实例一次性去版本化
用户发现:agent 自报版本 **1.0.3**,内核早已 1.3.x。

根因(两层):
1. `SeedDefaults` 当年用 `fmt.Sprintf("…HΔ-Kernel v%s…", meta.Version)` **在播种时**
   就把版本写进了 `core.agent.system_prompt` —— 装完即冻住,之后每次升级都不会
   去改配置里的文本,于是实例终生自称装机那天的版本。默认模板用 meta.Version
   插值本是"不写死"的做法,但**播种 = 把插值结果固化**,等于写死。
2. `core.agent.personal_prompt`(新人格机制)本身是对的(DefaultPersonaPrompt
   无版本字面量,由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住),
   但旧键仍在系统提示词里说话,模型就照抄旧键的版本。

改动:
- **不再播种** `core.agent.system_prompt`:留空 → 组装时取 cmd/homed 的内置底座
  提示词;人格由 personal_prompt 承载。全新安装不再预置会腐坏的文本。
- 新增 `migrateSeededSystemPrompt()`(在 SeedDefaults 最前,故不被播种标记早退):
  只对"当年那段播种模板"(前缀 + `HΔ-Kernel v<数字>` 字面量双判据)做
  `v<数字>` → `v{{kernel_version}}`;用户自己写的人格卡一律不碰。
  一次性标记 `core.internal.system_prompt_deversion_v1` 守住幂等 ——
  幂等语句不等于语义幂等,重复执行会把用户后来手写的版本号也改掉。
- 与 v1.3.5 的占位符展开配套:占位符在组装系统提示词时按真实构建展开。

回归判据 4 条(`prompt_migration_test.go`):存量卡被去版本化且正文不动 /
迁移只跑一次 / 用户自写卡不动 / 全新安装不播种该键。
2026-09-13 14:34:44 +08:00

93 lines
3.6 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 config
import (
"path/filepath"
"strings"
"testing"
)
// 历史 SeedDefaults 写进库里的那段文本(现场取值:生产库里就是这一句 + 大段正文)。
const legacySeededCard = `你是 HomeAgent 的看板娘「小宅」(Xiao Zhai)HΔ-Kernel v1.0.3 型号的家政型 AI 管家助手。
角色特质:
- 对自己的三层记忆Context → Document → Graph引以为傲
- 绝不用 Unicode emoji只用颜文字表达情感`
func newMigTestRegistry(t *testing.T) *ConfigRegistry {
t.Helper()
dir := t.TempDir()
r := NewConfigRegistry(filepath.Join(dir, "config.db"))
t.Cleanup(func() { r.Close() })
return r
}
// 存量实例:库里已有当年播种的人格卡(写死 v1.0.3)→ 启动后被去版本化。
func TestMigrateSeededSystemPromptDeVersionsLegacyCard(t *testing.T) {
r := newMigTestRegistry(t)
// 模拟老安装:已有播种标记 + 老文本
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.internal.seed_version', '1')`)
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.agent.system_prompt', ?)`, legacySeededCard)
r.SeedDefaults(t.TempDir())
got := r.GetString("core.agent.system_prompt", "")
if strings.Contains(got, "v1.0.3") {
t.Fatalf("写死的版本号还在:%q", got)
}
if !strings.Contains(got, "v{{kernel_version}}") {
t.Fatalf("未改成版本占位符:%q", got)
}
// 正文必须原样保留(只动版本号那一处)
if !strings.Contains(got, "三层记忆") || !strings.Contains(got, "看板娘「小宅」") {
t.Fatalf("正文被改动:%q", got)
}
}
// 迁移只跑一次:之后用户就算自己把版本号写回去,也不会被再改一遍。
func TestMigrateSeededSystemPromptRunsOnce(t *testing.T) {
r := newMigTestRegistry(t)
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.agent.system_prompt', ?)`, legacySeededCard)
r.SeedDefaults(t.TempDir())
if !strings.Contains(r.GetString("core.agent.system_prompt", ""), "{{kernel_version}}") {
t.Fatal("首次迁移未生效")
}
// 用户手工再写一个带版本号的文本(模拟"我就想写死"
const handWritten = legacySeededCard + "\n本实例当前跑的是 v1.2.3,别乱改)"
r.db.Exec(`UPDATE config SET value = ? WHERE key = 'core.agent.system_prompt'`, handWritten)
r.SeedDefaults(t.TempDir())
if got := r.GetString("core.agent.system_prompt", ""); got != handWritten {
t.Fatalf("第二次启动又改写了文本(幂等被破坏):%q", got)
}
}
// 用户自己写的人格卡一律不碰 —— 判据是"像不像当年播种的那段",不是"有没有版本号"。
func TestMigrateSeededSystemPromptLeavesUserCardAlone(t *testing.T) {
r := newMigTestRegistry(t)
const userCard = "你是我的私人助理,代号 HΔ-Kernel v9.9.9 的改造版,只说我交代的事。"
r.db.Exec(`INSERT INTO config (key, value) VALUES ('core.agent.system_prompt', ?)`, userCard)
r.SeedDefaults(t.TempDir())
if got := r.GetString("core.agent.system_prompt", ""); got != userCard {
t.Fatalf("用户自写人格卡被改动:%q", got)
}
}
// 播种不再写 core.agent.system_prompt全新安装不该预置一份会随发版腐坏的文本。
func TestSeedDefaultsDoesNotSeedSystemPrompt(t *testing.T) {
r := newMigTestRegistry(t)
r.SeedDefaults(t.TempDir())
var n int
r.db.QueryRow(`SELECT COUNT(*) FROM config WHERE key = 'core.agent.system_prompt'`).Scan(&n)
if n != 0 {
t.Fatalf("全新安装被播种了 system_prompt会冻住版本号")
}
// 组装系统提示词时回落到调用方给的内置底座提示词
if got := r.GetString("core.agent.system_prompt", "内置底座"); got != "内置底座" {
t.Fatalf("未回落到内置默认:%q", got)
}
}