feat(backthink): 后台自主思考(heartbeat+事件触发 + 打扰决策 fail-closed + 主动推送活跃→IM 兜底 + 限制 + 思考记录)
This commit is contained in:
1 parent
05e23ea590
commit
6f97fded3a
13 files changed
+1968
No files matched your search
@@ -0,0 +1,139 @@
|
||||
package backthink
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// dayAt returns a mid-afternoon time in UTC (outside the quiet-hours window).
|
||||
func dayAt() time.Time { return time.Date(2026, 9, 6, 14, 0, 0, 0, time.UTC) }
|
||||
|
||||
// nightAt returns a 02:00 time in UTC (inside the default 23-7 quiet window).
|
||||
func nightAt() time.Time { return time.Date(2026, 9, 6, 2, 0, 0, 0, time.UTC) }
|
||||
|
||||
// testBackthink builds a Backthink with a normalized empty Config so Decide uses
|
||||
// the default quiet-hours window (23:00-07:00).
|
||||
func testBackthink() *Backthink {
|
||||
return &Backthink{cfg: normalize(Config{})}
|
||||
}
|
||||
|
||||
func TestDecide_DoNotDisturb(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testBackthink()
|
||||
cases := []struct {
|
||||
name string
|
||||
in DecisionInput
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "凌晨+睡眠_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: nightAt(), UserState: UserState{Sleeping: true}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "睡眠_重要也不打扰",
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: dayAt(), UserState: UserState{Sleeping: true}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "上班_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Working: true}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "忙碌_普通_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Busy: true}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "安静时段_普通_不打扰",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: nightAt(), UserState: UserState{Boredom: 0.9}},
|
||||
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{}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := b.Decide(tc.in)
|
||||
if got.ShouldPush != tc.want {
|
||||
t.Fatalf("Decide(%+v).ShouldPush = %v, want %v (reason %q)", tc.in, got.ShouldPush, tc.want, got.Reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecide_ShouldPush(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := testBackthink()
|
||||
cases := []struct {
|
||||
name string
|
||||
in DecisionInput
|
||||
}{
|
||||
{
|
||||
name: "白天+重要_推",
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: dayAt(), UserState: UserState{}},
|
||||
},
|
||||
{
|
||||
name: "白天+危急_推",
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{}},
|
||||
},
|
||||
{
|
||||
name: "白天+普通+无聊_推",
|
||||
in: DecisionInput{Trigger: "iot.device_update", Import: ImportanceNormal, At: dayAt(), UserState: UserState{Boredom: 0.8}},
|
||||
},
|
||||
{
|
||||
name: "安静时段+重要_推",
|
||||
in: DecisionInput{Trigger: "reminder.trigger", Import: ImportanceHigh, At: nightAt(), UserState: UserState{}},
|
||||
},
|
||||
{
|
||||
name: "危急_可打断忙碌_推",
|
||||
in: DecisionInput{Trigger: "system.event", Import: ImportanceCritical, At: dayAt(), UserState: UserState{Working: true}},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := b.Decide(tc.in)
|
||||
if !got.ShouldPush {
|
||||
t.Fatalf("Decide(%+v).ShouldPush = false, want true (reason %q)", tc.in, got.Reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInQuietHours(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := &Backthink{cfg: Config{QuietStartHour: 23, QuietEndHour: 7}}
|
||||
cases := []struct {
|
||||
name string
|
||||
at time.Time
|
||||
want bool
|
||||
}{
|
||||
{name: "午夜", at: time.Date(2026, 9, 6, 0, 0, 0, 0, time.UTC), want: true},
|
||||
{name: "凌晨两点", at: nightAt(), want: true},
|
||||
{name: "早上七点_偏出", at: time.Date(2026, 9, 6, 7, 0, 0, 0, time.UTC), want: false},
|
||||
{name: "傍晚", at: time.Date(2026, 9, 6, 18, 30, 0, 0, time.UTC), want: false},
|
||||
{name: "深夜十一点", at: time.Date(2026, 9, 6, 23, 0, 0, 0, time.UTC), want: true},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := b.inQuietHours(tc.at); got != tc.want {
|
||||
t.Fatalf("inQuietHours(%v) = %v, want %v", tc.at, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user