package backthink import ( "context" "testing" "time" ) // 本文件守切片 C2 的两条读侧能力(design/12 §N C.5/C.6): // 1. **触发源取值域**(trigger_domain.go 的唯一真源)—— 内建标签 + 订阅的事件主题, // 它是 admin `?trigger=` 筛选白名单的依据; // 2. **按触发源读取**(ListByTrigger 下推到 SQL)—— `limit` 必须是**匹配**条数, // 而不是「先取一页再筛掉不匹配的」(后者会给面板一个静默的假空值)。 // TestTriggerFilterValues_CoversBuiltinsAndSubscribedSubjects 钉住白名单的**构成**: // 三个内建标签(心跳 / 启动 / 自排)+ 本服务订阅的全部事件主题。 // // 变异:把 SelfWakeSource 从列表里摘掉 ⇒ 本用例变红(面板将筛不到自排轮, // 而它恰恰是这一档最想看的);少列一个订阅主题 ⇒ 同红。 func TestTriggerFilterValues_CoversBuiltinsAndSubscribedSubjects(t *testing.T) { t.Parallel() values := TriggerFilterValues() set := map[string]bool{} for _, v := range values { if set[v] { t.Fatalf("白名单里有重复取值 %q:%v", v, values) } set[v] = true } for _, want := range []string{HeartbeatSource, StartupSource, SelfWakeSource} { if !set[want] { t.Fatalf("白名单缺内建标签 %q:%v", want, values) } } subjects := EventSubjects() if len(subjects) == 0 { t.Fatal("订阅主题表为空:白名单会漏掉全部事件轮") } for _, s := range subjects { if !set[s] { t.Fatalf("白名单缺订阅主题 %q:%v", s, values) } } // 反向:白名单里的每个取值都必须真的可能出现在 Record.Trigger 上 // (= 内建标签 或 订阅主题)—— 多列一个「不可能出现」的值会让筛选永远返回空。 for _, v := range values { if v == HeartbeatSource || v == StartupSource || v == SelfWakeSource { continue } if !isEventSubject(v) { t.Fatalf("白名单含一个不可能是触发源的取值 %q:%v", v, values) } } } // isEventSubject 只在本测试里做反向核对(生产侧没有这个判定:白名单由 // TriggerFilterValues 拼出来,不需要反查)。 func isEventSubject(v string) bool { for _, s := range EventSubjects() { if s == v { return true } } return false } // TestIsTriggerValue_RejectsUnknown 钉住白名单判定:已知取值通行,未知/空串一律拒。 // // 变异:把 IsTriggerValue 改成恒 true ⇒ 服务端筛选会「接受一切但筛不中」, // 网关那条 400 的用例随之变红。 func TestIsTriggerValue_RejectsUnknown(t *testing.T) { t.Parallel() for _, ok := range []string{HeartbeatSource, StartupSource, SelfWakeSource, "iot.device_update"} { if !IsTriggerValue(ok) { t.Fatalf("IsTriggerValue(%q) = false, want true", ok) } } for _, bad := range []string{"", "event", "self-wake", "HEARTBEAT", "all", "iot"} { if IsTriggerValue(bad) { t.Fatalf("IsTriggerValue(%q) = true, want false", bad) } } } // TestStore_ListByTrigger_IsServerSideFilter 钉住**下推**语义:筛选后 `limit` 数的是 // 匹配条数(而不是「本页里恰好匹配的条数」)。 // // 变异:把 ListByTrigger 改回「List 之后内存筛」的退化实现(拿 limit 条再筛)⇒ // 下面的 `limit=1` 断言会拿到 0 条(最新一条是心跳,被筛掉后没有别的自排)⇒ 变红。 func TestStore_ListByTrigger_IsServerSideFilter(t *testing.T) { t.Parallel() ctx := context.Background() s, err := NewStore(ctx, newTestDB(t), discardLog()) if err != nil { t.Fatalf("NewStore: %v", err) } base := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC) // 交替写入:心跳(最新)/ 自排 / 心跳 / 自排 / 心跳(最旧)。 triggers := []string{HeartbeatSource, SelfWakeSource, HeartbeatSource, SelfWakeSource, HeartbeatSource} for i, tr := range triggers { if _, err := s.Append(ctx, Record{Trigger: tr, At: base.Add(time.Duration(i) * time.Minute)}); err != nil { t.Fatalf("Append %d: %v", i, err) } } got, err := s.ListByTrigger(ctx, SelfWakeSource, 10) if err != nil { t.Fatalf("ListByTrigger: %v", err) } if len(got) != 2 { t.Fatalf("自排记录 = %d 条, want 2(%+v)", len(got), got) } for _, r := range got { if r.Trigger != SelfWakeSource { t.Fatalf("筛选结果含非自排行:%+v", r) } } // 最新的在前(与 List 同口径)。 if got[0].ID != 4 || got[1].ID != 2 { t.Fatalf("顺序 = %d,%d, want 4,2(最新在前)", got[0].ID, got[1].ID) } // 🔴 下推的关键判据:limit 数的是**匹配**条数。 one, err := s.ListByTrigger(ctx, SelfWakeSource, 1) if err != nil { t.Fatalf("ListByTrigger limit=1: %v", err) } if len(one) != 1 || one[0].ID != 4 { t.Fatalf("limit=1 结果 = %+v, want 只有 id=4(匹配条数,不是先取一页再筛)", one) } // 未出现过的合法取值 ⇒ 空列表(不是错误)。 none, err := s.ListByTrigger(ctx, "reminder.trigger", 10) if err != nil { t.Fatalf("ListByTrigger(reminder.trigger): %v", err) } if len(none) != 0 { t.Fatalf("无匹配时应返回空列表,得到 %+v", none) } } // TestBackthink_ListByTrigger_DelegatesToStore 钉住服务层的读口真的接到存储 // (网关的可选接口 `backthinkTriggerLister` 靠的就是这个方法签名)。 // // 变异:把 `Backthink.ListByTrigger` 改成忽略 trigger 直接 List ⇒ 本用例变红。 func TestBackthink_ListByTrigger_DelegatesToStore(t *testing.T) { t.Parallel() b, _ := newBackthink(t, Config{}, Deps{Thinker: newWakeThinker(), State: &mockState{}}) ctx := context.Background() for i := 0; i < 3; i++ { if _, err := b.store.Append(ctx, Record{Trigger: HeartbeatSource, At: time.UnixMilli(int64(i))}); err != nil { t.Fatalf("Append: %v", err) } } if _, err := b.store.Append(ctx, Record{Trigger: SelfWakeSource, At: time.UnixMilli(99)}); err != nil { t.Fatalf("Append: %v", err) } got, err := b.ListByTrigger(ctx, SelfWakeSource, 50) if err != nil { t.Fatalf("ListByTrigger: %v", err) } if len(got) != 1 || got[0].Trigger != SelfWakeSource { t.Fatalf("got = %+v, want 只有那条自排记录", got) } }