feat(backthink): 安全域穿透安静时段 + 长静默不拦心跳 + 启动即思考(裁决 1.8/07.4.2/07.4.3/07.4.4/07.4.5)
执行方案阶段 2 · P2-2(`DevDocs/audit/_plan.md`)。四条裁决: - 1.8/07.4.2:`think()` 不再走忽略 importance 的 `disturbAllowed`(已删),**接回 `Decide` 级联**; 安静时段只挡常规推送、**critical 安全域穿透**;`DecisionInput` 增 `ConclusionPush`/`Heartbeat`。 睡眠仍绝对不打扰(裁决只豁免安静时段,此点已在设计与报告写明)。 - 07.4.3:长静默(>2h)分支对**心跳类**放行(常规事件仍需 ≥high),刚聊过天 10min 内扣留不变。 - 07.4.4:`tickLoop` **启动立即先思考一轮** + 拟人化启动消息;启动轮与心跳**同门同限频** (重启风暴不会刷屏),ctx 已取消则跳过。 - 07.4.5:核对一致(`THINK_INTERVAL_SEC` 默认 900s 与 design/20 §0 权威口径一致)⇒ **代码零改动**。 验证:go build 绿;backthink 包 vet/测试绿;`-count=20`(抗 flake)+ `-race` 绿; linux/amd64 + darwin/arm64 交叉编译绿;gofmt 干净;文件 ≤500 行、无 >80 行函数。 变异验证(三条,均带还原证明):① 安静时段改回"全拦" → 安全域穿透断言 FAIL(首次只删 critical 分支时仍绿——`critical >= high` 会从 high 分支漏过,故真实变异必须是"全拦"); ② 删心跳豁免 → 长静默心跳断言 FAIL;③ 删启动思考 → 启动即思考断言 FAIL。 未验证:无真实模型凭据 ⇒ 启动轮问候与 `[我决定:…]` 解析仅经 mock + 真 loop 验证,端到端 LLM 未验。
This commit is contained in:
1 parent
a35bcb8aa9
commit
afef75f640
7 files changed
+468
-176
No files matched your search
@@ -20,44 +20,46 @@ func testBackthink() *Backthink {
|
||||
func TestDecide_DoNotDisturb(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testBackthink()
|
||||
// 所有用例都带上她的结论要求推送(ConclusionPush),确保命中的是级联本身
|
||||
// 而不是「结论不推」的短路。
|
||||
cases := []struct {
|
||||
name string
|
||||
in DecisionInput
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "凌晨+睡眠_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: nightAt(), UserState: UserState{Sleeping: true}},
|
||||
name: "她的结论不推送_不推送",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceCritical, At: dayAt(), UserState: UserState{}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "睡眠_重要也不打扰",
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: dayAt(), UserState: UserState{Sleeping: true}},
|
||||
name: "凌晨+睡眠_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: nightAt(), UserState: UserState{Sleeping: true}, ConclusionPush: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "睡眠_危急也不打扰",
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{Sleeping: true}, ConclusionPush: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "上班_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Working: true}},
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Working: true}, ConclusionPush: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "忙碌_普通_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Busy: true}},
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Busy: true}, ConclusionPush: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "安静时段_普通_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: nightAt(), UserState: UserState{Boredom: 0.9}},
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: nightAt(), UserState: UserState{Boredom: 0.9}, ConclusionPush: true},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "白天_普通_不无聊_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "白天_低重要_不打扰",
|
||||
in: DecisionInput{Trigger: "heartbeat", Import: ImportanceLow, At: dayAt(), UserState: UserState{}},
|
||||
name: "安静时段_心跳_不打扰",
|
||||
in: DecisionInput{Trigger: "heartbeat", Import: ImportanceLow, At: nightAt(), UserState: UserState{}, ConclusionPush: true, Heartbeat: true},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
@@ -82,23 +84,27 @@ func TestDecide_ShouldPush(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "白天+重要_推",
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: dayAt(), UserState: UserState{}},
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: dayAt(), UserState: UserState{}, ConclusionPush: true},
|
||||
},
|
||||
{
|
||||
name: "白天+危急_推",
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{}},
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{}, ConclusionPush: true},
|
||||
},
|
||||
{
|
||||
name: "白天+普通+无聊_推",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Boredom: 0.8}},
|
||||
name: "白天+普通+她的结论_推",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Boredom: 0.8}, ConclusionPush: true},
|
||||
},
|
||||
{
|
||||
name: "安静时段+重要_推",
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: nightAt(), UserState: UserState{}},
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: nightAt(), UserState: UserState{}, ConclusionPush: true},
|
||||
},
|
||||
{
|
||||
name: "安静时段+危急_安全域穿透_推",
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: nightAt(), UserState: UserState{}, ConclusionPush: true},
|
||||
},
|
||||
{
|
||||
name: "危急_可打断忙碌_推",
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{Working: true}},
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{Working: true}, ConclusionPush: true},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
@@ -164,19 +170,21 @@ func TestClassifyPush(t *testing.T) {
|
||||
|
||||
// TestDecide_ActivityGate verifies the activity-aware soft gate: a fresh
|
||||
// conversation holds trivia (unless critical), a long-idle owner is only woken by
|
||||
// an important event, and the in-between band does not over-suppress.
|
||||
// an important event — except a heartbeat pass, which is never held by a
|
||||
// long-silent gap (裁决 07.4.3) — and the in-between band does not over-suppress.
|
||||
func TestDecide_ActivityGate(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testBackthink()
|
||||
|
||||
// Just chatted (within RecentChatCooldown=10min): a normal daytime boredom
|
||||
// push is held unless the event is safety-critical.
|
||||
// Just chatted (within RecentChatCooldown=10min): a normal daytime push is
|
||||
// held unless the event is safety-critical.
|
||||
fresh := DecisionInput{
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-5 * time.Minute),
|
||||
HasActivity: true,
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-5 * time.Minute),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
}
|
||||
if got := b.Decide(fresh); got.ShouldPush {
|
||||
t.Fatalf("fresh conversation should hold a normal push, got %+v", got)
|
||||
@@ -186,29 +194,37 @@ func TestDecide_ActivityGate(t *testing.T) {
|
||||
t.Fatalf("critical event should push even in a fresh conversation, got %+v", got)
|
||||
}
|
||||
|
||||
// Away a long while (> longIdleThreshold): only an important event is worth it.
|
||||
// Away a long while (> longIdleThreshold): only an important event is worth
|
||||
// it — but a heartbeat pass still goes through (裁决 07.4.3 长期静默段不拦心跳).
|
||||
idle := DecisionInput{
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-3 * time.Hour),
|
||||
HasActivity: true,
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-3 * time.Hour),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
}
|
||||
if got := b.Decide(idle); got.ShouldPush {
|
||||
t.Fatalf("long idle should hold a normal push, got %+v", got)
|
||||
t.Fatalf("long idle should hold a normal event push, got %+v", got)
|
||||
}
|
||||
idle.Import = ImportanceHigh
|
||||
if got := b.Decide(idle); !got.ShouldPush {
|
||||
t.Fatalf("long idle high-importance should push, got %+v", got)
|
||||
}
|
||||
idle.Import = ImportanceLow
|
||||
idle.Heartbeat = true
|
||||
if got := b.Decide(idle); !got.ShouldPush {
|
||||
t.Fatalf("long idle heartbeat should still push (07.4.3), got %+v", got)
|
||||
}
|
||||
|
||||
// Middling activity (30 min ago) is not over-suppressed.
|
||||
middle := DecisionInput{
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-30 * time.Minute),
|
||||
HasActivity: true,
|
||||
Import: ImportanceNormal,
|
||||
At: dayAt(),
|
||||
UserState: UserState{Boredom: 0.9},
|
||||
LastActivity: dayAt().Add(-30 * time.Minute),
|
||||
HasActivity: true,
|
||||
ConclusionPush: true,
|
||||
}
|
||||
if got := b.Decide(middle); !got.ShouldPush {
|
||||
t.Fatalf("middle-band activity should allow a normal push, got %+v", got)
|
||||
|
||||
Reference in new issue
Block a user