package backthink import ( "context" "strings" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/urgency" ) // 本文件守住 design/12 §N C 的**判定侧**(切片 C1-1):自我唤醒是追加的第三个 // TriggerKind,它的档位折算与总线事件共用同一条通道(foldDeclared → escalateImport), // 判定侧只多认一个 kind、不加第二条档位通道。对应 C.8 判据 1 / 4 / 6 / 9。 // // ⚠️ 本片**不产生**自排轮:WakeNow / worker / 工具面分别属 C1-2 / C1-3,因此这里的 // 「生产路径」用例直接构造 selfWakeTrigger —— 它证明的是链路已就绪,不是能力已交付。 // TestTriggerKind_SelfWakeAppendedWithStableLabels 钉住标签与编号口径(C.6 落地清单): // 落库值(Record.Trigger)、面板值与审计前缀共用同一个真源;新 kind 是**追加**的, // 既有两个值的编号与标签逐字不变(改编号会静默改写已落库记录的含义)。 func TestTriggerKind_SelfWakeAppendedWithStableLabels(t *testing.T) { t.Parallel() cases := []struct { kind TriggerKind want string }{ {TriggerHeartbeat, "heartbeat"}, {TriggerEvent, "event"}, {TriggerSelfWake, SelfWakeSource}, } for _, tc := range cases { if got := tc.kind.String(); got != tc.want { t.Fatalf("TriggerKind(%d).String() = %q, want %q", int(tc.kind), got, tc.want) } } if !(TriggerHeartbeat < TriggerEvent && TriggerEvent < TriggerSelfWake) { t.Fatalf("kind 编号不再是追加的:heartbeat=%d event=%d self_wake=%d", int(TriggerHeartbeat), int(TriggerEvent), int(TriggerSelfWake)) } if SelfWakeSource != "self_wake" { t.Fatalf("SelfWakeSource = %q, want %q(面板与文档按此值登记)", SelfWakeSource, "self_wake") } if SelfWakeSummaryPrefix != SelfWakeSource+": " { t.Fatalf("SelfWakeSummaryPrefix = %q, want %q", SelfWakeSummaryPrefix, SelfWakeSource+": ") } } // TestSelfWakeTrigger_ShapeAndUndeclaredDefault 钉住自排轮的形状(C.1/C.2):来源与 // 摘要用稳定标签、主题级默认是 Normal(不是 Low —— 不新造一档「比例行还低」的语义)、 // Kind 是 TriggerSelfWake;零值声明不上抬。 func TestSelfWakeTrigger_ShapeAndUndeclaredDefault(t *testing.T) { t.Parallel() tr := selfWakeTrigger(SelfWakeRequest{Reason: "想看看她今天的安排"}) if tr.Source != SelfWakeSource { t.Fatalf("Source = %q, want %q", tr.Source, SelfWakeSource) } if tr.Kind != TriggerSelfWake { t.Fatalf("Kind = %v, want %v", tr.Kind, TriggerSelfWake) } if tr.Import != ImportanceNormal { t.Fatalf("Import = %v, want %v(未声明档位时不上抬)", tr.Import, ImportanceNormal) } if tr.Summary != SelfWakeSummaryPrefix+"想看看她今天的安排" { t.Fatalf("Summary = %q, want the stable prefix + 她写的理由", tr.Summary) } // 理由为空也要带前缀:context.go 靠前缀认轮次,不靠理由内容。 if empty := selfWakeTrigger(SelfWakeRequest{}); !strings.HasPrefix(empty.Summary, SelfWakeSummaryPrefix) { t.Fatalf("empty-reason Summary = %q, want the stable prefix", empty.Summary) } } // TestSelfWakeRequest_DeclarerChannel 钉住自排**没有**第二条档位通道:她的声明经 // urgency.Declarer 被 foldDeclared 读出(与 IoT 设备跃迁同一个接口),因此 // SelfWakeRequest 必须满足该接口。 func TestSelfWakeRequest_DeclarerChannel(t *testing.T) { t.Parallel() var d urgency.Declarer = SelfWakeRequest{Reason: "r", Declared: urgency.LevelWarning} if got := d.Urgency(); got != urgency.LevelWarning { t.Fatalf("Urgency() = %v, want %v", got, urgency.LevelWarning) } if got := (SelfWakeRequest{}).Urgency(); got != urgency.LevelRoutine { t.Fatalf("zero-value Urgency() = %v, want %v(未声明 = routine)", got, urgency.LevelRoutine) } } // TestBackthink_SelfWakeRoundProductionPath 走生产路径 think():一轮自排思考落库为 // trigger="self_wake",长期静默段里例常自排被扣下(C.4),而声明 warning 后放行且 // 落库档位是**折算后**的 warning(C.6)。对应 C.8 判据 6 / 9。 // // 变异:把 backthink.go 的 `Kind: tr.Kind` 改回传入常量(如 TriggerHeartbeat)⇒ 第一段 // 的「例常自排被扣下」变红;把 SelfWakeSource 改回心跳值 ⇒ 落库断言变红。 func TestBackthink_SelfWakeRoundProductionPath(t *testing.T) { t.Parallel() pusher := &mockPusher{result: PushResult{Pushed: true, Route: RouteActiveClient}} b, _ := newBackthink(t, Config{}, Deps{ Thinker: &mockThinker{text: "[我决定:push]\n我想再看看她今天的安排"}, State: &mockState{state: UserState{}}, Pusher: pusher, Recent: &fakeRecent{msgs: []history.Message{{ Role: history.RoleUser, Content: "我先去忙了", CreatedAt: dayAt().Add(-3 * time.Hour).UnixMilli(), }}}, Now: func() time.Time { return dayAt() }, }) b.think(context.Background(), selfWakeTrigger(SelfWakeRequest{Reason: "复盘一下今天的对话"})) if got := pusher.count(); got != 0 { t.Fatalf("pusher calls = %d, want 0(长期静默段里例常自排被扣下)", got) } recs, err := b.List(context.Background(), 10) if err != nil { t.Fatalf("List: %v", err) } if len(recs) != 1 { t.Fatalf("records = %+v, want exactly one self-wake record", recs) } if recs[0].Trigger != SelfWakeSource { t.Fatalf("Record.Trigger = %q, want %q", recs[0].Trigger, SelfWakeSource) } if recs[0].ShouldPush { t.Fatalf("record = %+v, want the routine self wake held", recs[0]) } if !strings.Contains(recs[0].PushResult.Detail, "self-initiated wake") { t.Fatalf("PushResult.Detail = %q, want it to name the self-initiated wake hold", recs[0].PushResult.Detail) } // 同一轮只换声明:warning ⇒ 折算 High ⇒ 长期静默段放行,且落库档位是折算后的值。 b.think(context.Background(), selfWakeTrigger(SelfWakeRequest{ Reason: "她可能正需要人陪", Declared: urgency.LevelWarning, })) recs, err = b.List(context.Background(), 10) if err != nil { t.Fatalf("List: %v", err) } if !recs[0].ShouldPush || !recs[0].PushResult.Pushed { t.Fatalf("declared self wake = %+v, want a delivered push", recs[0]) } if recs[0].Urgency != urgency.LevelWarning.String() { t.Fatalf("Record.Urgency = %q, want %q(落库的是该轮实际生效的档位)", recs[0].Urgency, urgency.LevelWarning.String()) } if got := pusher.count(); got != 1 { t.Fatalf("pusher calls = %d, want 1", got) } }