package backthink import ( "fmt" "strings" "testing" ) // 本文件是 design/12 §N C.3 的**提示词判据**:三处指引必须共用同一批段落(抽取而不是 // 三次复制),自排轮必须说清自己是谁排的、只排一次、有上限;并且**不得**枚举工具清单 // 或写死上限数字(design/04 §4.1 + C.3 的「文案不写具体上限数字」)。 // promptsUnderTest 返回三处指引的(名字 → 文本)。 func promptsUnderTest() map[string]string { return map[string]string{ "通用轮": guidancePrompt("heartbeat"), "启动轮": guidancePrompt(StartupSummary), "自排轮": guidancePrompt(selfWakeSummary("稍后回看她的日程")), } } // TestPromptsShareTheExtractedParagraphs 断言「三处引用共用段落」是**结构事实**: // 每处文本都必须逐字包含 toolFaceHint() 与它该有的 proactiveDecisionTail(...)。 // // 变异:把某一处的共用段落改成复制粘贴的另一份措辞 ⇒ 本用例变红(这正是「三次复制必须 // 抽取」要防的漂移)。 func TestPromptsShareTheExtractedParagraphs(t *testing.T) { t.Parallel() face := toolFaceHint() withUrgency := proactiveDecisionTail(true) withoutUrgency := proactiveDecisionTail(false) for name, p := range promptsUnderTest() { if !strings.Contains(p, face) { t.Fatalf("%s 没有逐字引用共用的工具面段落(toolFaceHint)", name) } want := withUrgency if name == "启动轮" { want = withoutUrgency } if !strings.Contains(p, want) { t.Fatalf("%s 没有逐字引用共用的决策尾段落", name) } } } // TestPromptsAdvertiseTheSelfWakeTool 断言三处都指名了 `request_think` 与自查入口 // `capability_list`:工具注册了而提示词不说,她不知道能自排(C1-3 的目的就是让她真能自排)。 func TestPromptsAdvertiseTheSelfWakeTool(t *testing.T) { t.Parallel() for name, p := range promptsUnderTest() { if !strings.Contains(p, ThinkToolName) { t.Fatalf("%s 没有提到 %s(她不知道能给自己排思考)", name, ThinkToolName) } if !strings.Contains(p, "capability_list") { t.Fatalf("%s 没有给出可验证的自查入口 capability_list", name) } if !strings.Contains(p, "上限") { t.Fatalf("%s 没有说明自排有次数上限", name) } } } // TestPromptsDoNotWriteTheLimitNumber 断言提示词里**不写具体上限数字**(默认 4 次/小时 // 也不写):提示词与配置分叉就是撒谎的源头,所以只写「有上限」。 // // 变异:在任一提示词里写下默认上限字面量 ⇒ 本用例变红。 func TestPromptsDoNotWriteTheLimitNumber(t *testing.T) { t.Parallel() digits := fmt.Sprintf("%d", defaultSelfWakeLimit) for name, p := range promptsUnderTest() { if strings.Contains(p, digits) { t.Fatalf("%s 把上限数字 %s 写进了提示词:上限由配置决定,写死必然漂移", name, digits) } } } // TestGuidancePrompt_RoutesSelfWakeByPrefix 断言路由:带 SelfWakeSummaryPrefix 的触发 // 走自排专属指引并**原样带回她写下的理由**;其余稳定标识不受影响(两个标识不重叠)。 func TestGuidancePrompt_RoutesSelfWakeByPrefix(t *testing.T) { t.Parallel() const reason = "稍后回看她提过的体检安排" p := guidancePrompt(selfWakeSummary(reason)) if !strings.Contains(p, "[自主思考 · 你自己排的一轮]") { t.Fatalf("自排轮没有走专属指引:%s", p) } if !strings.Contains(p, "理由:"+reason) { t.Fatalf("自排轮没有带回她写下的理由:%s", p) } if strings.Contains(p, "刚刚回来") { t.Fatalf("自排轮落到了启动轮指引上:%s", p) } if strings.Contains(p, "无人在旁") { t.Fatalf("自排轮落到了通用轮指引上:%s", p) } if !strings.Contains(p, proactiveDecisionTail(true)) { t.Fatalf("自排轮的结论必须能留在穿透路径上(含 [紧急:…] 行):%s", p) } if !strings.Contains(p, "一轮只排一次") { t.Fatalf("自排轮没有说清「一轮只排一次、不要连排」:%s", p) } // 启动轮仍走自己的分支(前缀判定没有把它吞掉)。 if q := guidancePrompt(StartupSummary); !strings.Contains(q, "刚刚回来") { t.Fatalf("启动轮路由被自排前缀判定破坏了:%s", q) } } // TestSelfWakeReasonOf_FallsBackHonestly 断言理由缺失时给**如实的**占位,而不是编一个。 func TestSelfWakeReasonOf_FallsBackHonestly(t *testing.T) { t.Parallel() if got := selfWakeReasonOf(SelfWakeSummaryPrefix); got == "" || !strings.Contains(got, "没有留下理由") { t.Fatalf("空理由的占位 = %q, want 如实的「没有留下理由」", got) } if got := selfWakeReasonOf(selfWakeSummary(" 看看 ")); got != "看看" { t.Fatalf("理由 = %q, want 去空白后的 %q", got, "看看") } } // TestStartupPrompt_KeepsItsUrgencyScope 钉住启动轮的**刻意取舍**:它的引导不含 // `[紧急:…]` 行(见 startupPrompt 的注释 —— 启动轮的口径是「打个招呼」,把档位声明写进 // 它的引导等于给启动轮开一条主动穿透安静时段的路,属产品口径变更,不在 C1-3 授权内)。 // // 主人若决定统一三处,把 startupPrompt 的实参改成 true 并**同时**更新本用例即可。 func TestStartupPrompt_KeepsItsUrgencyScope(t *testing.T) { t.Parallel() p := guidancePrompt(StartupSummary) if strings.Contains(p, proactiveDecisionTail(true)) { t.Fatalf("启动轮的决策尾不该含 [紧急:…] 行:%s", p) } if !strings.Contains(p, "[我决定:push]") || !strings.Contains(p, "[渠道:") { t.Fatalf("启动轮的决策尾缺了基本的两行:%s", p) } // 通用轮与自排轮必须有那一行(否则她无法在自排轮上匿名穿过安静时段)。 for _, name := range []string{"通用轮", "自排轮"} { if !strings.Contains(promptsUnderTest()[name], "[紧急:") { t.Fatalf("%s 的决策尾缺了 [紧急:…] 行", name) } } }