Files
Cyrene_v2/internal/services/backthink/selfwake_prompt_test.go
T
AskaEth 2d211c2d16 feat(backthink,browser): 自排工具面 C1-3 + 浏览器两处偏离收口(X2+X3 同文件一并落)
X2 C1-3(自排工具面与提示词):
- request_think(thinktool.go,照 transcode/tool.go 范式)+ reason 必填/urgency 闭集 + 双守门
  (安全闸 + owner 判定:群聊/陌生人一律拒)
- **成对**进两张唤醒态允许集(quiet + awake;CAPACITY_TOOL_SETS 是整体替换)
- 提示词抽共用段落 + 自排前缀优先(不枚举工具清单);面板加 self_wake 标签与筛选轴
- 4 个变异自检(拿掉 owner 判定/只加一张表/放宽 urgency/前端标签恒等)

X3 收口(与 X2 共用 wire_services.go ⇒ 一并落):
- 删掉第三份 SSRF 实现(guard.go),改由 io.ValidateRequestURL 唯一判定 + 薄门面(保两个错误身份)
- 浏览器会话挂进 a.close(真机:真 Chrome 被关、无残留、生产未扰动)
- a.wireBrowser() 归位到 wire_runtime.go,删掉 ioToolOptions 的临时挂点
- 5 个变异自检

顺带补提交 C1-2 漏掉的 internal/config/runtimeconfig/backthink_test.go(ed50053 漏 add)。
2026-10-06 21:16:23 +08:00

142 lines
5.9 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 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)
}
}
}