Files
Cyrene_v2/internal/services/backthink/selfwake_status_test.go
T
AskaEth 7d9b2132bb test(testhealth): 替身竞态加锁 + 墙钟断言硬度(M33)
四处测试健康债务,只改 *_test.go,不动生产代码、不降并发:

- channelbridge:stubChatter / stubSessions 的跨 goroutine 字段一律经 mu 访问
  (Run/GetOrCreate 在车道 worker 上写、测试 goroutine 读,两者无 happens-before;
  负向断言「等一会儿再看循环有没有被驱动」尤其如此)。读写走访问器,Messages()
  返回副本;t.Parallel 与并发度不变。
- plugins/tools/io:claimProbeTimeout 2s(真实时间预算)→ 60s,并由 claimProbeWait
  按 t.Deadline 收缩。探针只等 goroutine 被调度(无时间语义),放大余量把
  「机器快慢」从断言里去掉,死锁仍以断言失败暴露。
- backthink:TestSelfWakeGate_WindowExpiry 窗口 120ms → 6s(给 waitFor 的 4s 上界
  留 2s 余量),到期等待改为按首次受理时刻推算 window+250ms,不再盲睡 3×窗口。
  另修 TestSelfWakeStatus_MatchesReceiptAndTracksPendingSlot:worker 先 take 再进
  Think,卡 Think 并不占待跑槽 —— 改为「上一轮把 worker 占住再断言 Pending」。
- memory:墙钟实测用例显式给闸 5s 预算(measureGateBudget),不再因默认 150ms
  超时 fail-open 打炸「每轮都真扫真判」前置断言;并把 GateFailed 纳入该断言。
2026-10-24 22:24:14 +08:00

189 lines
8.0 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 (
"context"
"testing"
"time"
)
// 本文件守切片 C2 的**用量可见面**(design/12 §N C.5 的读侧):自排闸的只读快照
// 必须与工具收据(SelfWakeReceipt)**同一口径**(同一个时间源、同一个窗口、
// 同一个「消耗之后取值」),并且**读它不改变闸的状态**。
// TestSelfWakeStatus_MatchesReceiptAndTracksPendingSlot 是主判据:
// 排一轮后 `Used` 必须等于收据里的 Used,待跑槽随之变占用;额度用尽后
// `Used` 停在 `Limit`(不会因为一次拒绝而虚增)。
//
// 变异:把 SelfWakeStatus 的 Used 硬编码成 0(或改成 `len(granted)` 而不丢弃
// 过期记录)⇒ 本用例在 `Used != receipt.Used` 处变红。
func TestSelfWakeStatus_MatchesReceiptAndTracksPendingSlot(t *testing.T) {
t.Parallel()
// 让自排轮卡在 Think 里:worker 因此**忙**着,后面排进去的那一轮不会被取走,
// `pending` 才能确定地断言。
//
// 🔴 卡住 Think 只能让 Running 恒真,**不足以**占住待跑槽:worker 是先 take(把槽取空)
// 再进 Think 的。旧写法只卡 Think 就断言「排一轮后 Pending=true」,实际是在赌
// 「WakeNow 返回得比 worker 取槽快」—— -race 下 worker 常常先跑赢,Pending 已经是
// false(本轮实测 3/3 必红)。因此这里改成:第一轮先让 worker 忙起来,第二轮才断言
// 待跑槽被占用 —— 断言口径(快照如实报告待跑槽)一字未改,只是把它放到可确定的相位上。
w := newWakeThinker()
w.holdSelfWake = make(chan struct{})
b, _ := startSelfWake(t, Config{SelfWakeLimit: 2, SelfWakeWindow: time.Hour}, w)
// 放行卡住的那一轮,好让 Stop 的 t.Cleanup 能排空 worker。
defer close(w.holdSelfWake)
// 启动即有一轮启动轮在跑(tickLoop 上的 TickImmediately),但它不占自排槽。
before := mustStatus(t, b)
if !before.Running {
t.Fatal("已 Start 的服务其快照必须报告 Running=true")
}
if before.Used != 0 || before.Pending {
t.Fatalf("刚启动的快照 = %+v, want Used=0 / Pending=false", before)
}
if before.Limit != 2 || before.WindowSec != 3600 {
t.Fatalf("快照口径 = %+v, want Limit=2 / WindowSec=3600", before)
}
if before.LimitIsDefault {
t.Fatalf("上限 2 不该被算成默认值:%+v", before)
}
// 窗口仍取了代码默认(1 小时)⇒ WindowIsDefault 就是 true(这是「生效值恰好等于
// 默认值」如实上报,而不是「配置文件没写」的断言)。
if !before.WindowIsDefault {
t.Fatalf("窗口取了默认值却没标记:%+v", before)
}
if before.DefaultLimit != defaultSelfWakeLimit || before.DefaultWindowSec != int(defaultSelfWakeWindow/time.Second) {
t.Fatalf("默认值口径 = %+v, want %d / %d", before,
defaultSelfWakeLimit, int(defaultSelfWakeWindow/time.Second))
}
receipt, err := b.WakeNow(ownerCtx(), SelfWakeRequest{Reason: "排一轮"})
if err != nil {
t.Fatalf("WakeNow: %v", err)
}
if !receipt.Accepted {
t.Fatalf("收据 = %+v, want Accepted", receipt)
}
// 等这一轮真的被 worker 取走并卡进 Think(= 槽已空、worker 已忙):这是下一段的**前置件**,
// 等不到说明 worker 没被调度(那是另一个失败,由此处明确报出)。
w.waitFor(t, 2, 5*time.Second)
after := mustStatus(t, b)
if after.Used != receipt.Used || after.Used != 1 {
t.Fatalf("快照 Used = %d, 收据 Used = %d, want 1(同一口径)", after.Used, receipt.Used)
}
// worker 正忙 ⇒ 这一轮留在待跑槽里:快照必须如实报告 Pending。
pending, err := b.WakeNow(ownerCtx(), SelfWakeRequest{Reason: "占住待跑槽"})
if err != nil {
t.Fatalf("WakeNow 占槽: %v", err)
}
if !pending.Accepted {
t.Fatalf("收据 = %+v, want Accepted(worker 正忙,槽是空的)", pending)
}
occupied := mustStatus(t, b)
if !occupied.Pending {
t.Fatalf("排入后待跑槽应报告占用:%+v", occupied)
}
if occupied.Used != pending.Used || occupied.Used != 2 {
t.Fatalf("快照 Used = %d, 收据 Used = %d, want 2(同一口径,槽满=额度用尽)",
occupied.Used, pending.Used)
}
// 撞上满槽 ⇒ 合并(不消耗额度),快照的 Used 不得变。
if _, err := b.WakeNow(ownerCtx(), SelfWakeRequest{Reason: "再排一轮"}); err != nil {
t.Fatalf("WakeNow 合并: %v", err)
}
merged := mustStatus(t, b)
if merged.Used != 2 {
t.Fatalf("合并后 Used = %d, want 2(合并不额外占额度,停在 Limit)", merged.Used)
}
if merged.Limit != 2 {
t.Fatalf("合并后 Limit = %d, want 2", merged.Limit)
}
}
// TestSelfWakeStatus_StoppedServiceIsNotRunning 钉住「未运行」这一态:
// 服务在跑之前(以及 Stop 之后)快照必须如实报 Running=false,好让面板显示
// 「此刻排不出新的一轮」而不是「已用 0 次」。
//
// 变异:把 SelfWakeStatus 的 Running 直接写 true ⇒ 本用例变红。
func TestSelfWakeStatus_StoppedServiceIsNotRunning(t *testing.T) {
t.Parallel()
b, _ := newBackthink(t, Config{}, Deps{Thinker: newWakeThinker(), State: &mockState{}})
st := mustStatus(t, b)
if st.Running {
t.Fatalf("未 Start 的快照 = %+v, want Running=false", st)
}
// 未运行时额度必然是 0,且上限已 normalize(面板仍能看到「上限 4/小时」)。
if st.Used != 0 {
t.Fatalf("未运行时 Used = %d, want 0", st.Used)
}
if st.Limit != defaultSelfWakeLimit || !st.LimitIsDefault || !st.WindowIsDefault {
t.Fatalf("零配置的快照 = %+v, want 默认口径 %d/%v", st,
defaultSelfWakeLimit, defaultSelfWakeWindow)
}
b.Start(context.Background())
if !mustStatus(t, b).Running {
t.Fatal("Start 之后快照必须报告 Running=true")
}
if err := b.Stop(context.Background()); err != nil {
t.Fatalf("Stop: %v", err)
}
if mustStatus(t, b).Running {
t.Fatal("Stop 之后快照必须报告 Running=false")
}
}
// TestSelfWakeStatus_ReadDoesNotConsumeBudget 钉住「读是纯读」:反复读快照
// 不能让额度减少,也不能让待跑槽变化。
//
// 变异:把快照实现改成调用 limiter.Allow()(成功即消耗)⇒ 本用例在 Used 处变红。
func TestSelfWakeStatus_ReadDoesNotConsumeBudget(t *testing.T) {
t.Parallel()
b, _ := newBackthink(t, Config{SelfWakeLimit: 1, SelfWakeWindow: time.Hour},
Deps{Thinker: newWakeThinker(), State: &mockState{}})
b.Start(context.Background())
t.Cleanup(func() { _ = b.Stop(context.Background()) })
for i := 0; i < 5; i++ {
if got := mustStatus(t, b).Used; got != 0 {
t.Fatalf("第 %d 次读快照后 Used = %d, want 0(读不改闸)", i+1, got)
}
}
// 读完之后额度仍然可用:唯一那一格必须能真排上一轮。
receipt, err := b.WakeNow(ownerCtx(), SelfWakeRequest{Reason: "读完之后再排"})
if err != nil {
t.Fatalf("WakeNow: %v", err)
}
if !receipt.Accepted || receipt.Used != 1 {
t.Fatalf("收据 = %+v, want Accepted 且 Used=1(读快照没有偷走额度)", receipt)
}
}
// TestSelfWakeStatus_UnreadableOnlyWhenGateMissing 钉住第二个返回值的诚实语义:
// 正常构造的服务一定有闸(New 里建),只有零值 Backthink 才报告「看不到」。
//
// 变异:把 `return SelfWakeStatus{}, false` 改成 `true` ⇒ 本用例变红
// (调用方会把 0/0 当成真实用量)。
func TestSelfWakeStatus_UnreadableOnlyWhenGateMissing(t *testing.T) {
t.Parallel()
if _, ok := (&Backthink{}).SelfWakeStatus(); ok {
t.Fatal("零值 Backthink 没有闸,必须报告 ok=false(不得伪造 0/0)")
}
b, _ := newBackthink(t, Config{}, Deps{Thinker: newWakeThinker(), State: &mockState{}})
if _, ok := b.SelfWakeStatus(); !ok {
t.Fatal("经 New 构造的服务一定有闸,必须报告 ok=true")
}
}
// mustStatus 取快照并在「闸不在」时直接失败(正常构造的服务不该走到那一步)。
func mustStatus(t *testing.T, b *Backthink) SelfWakeStatus {
t.Helper()
st, ok := b.SelfWakeStatus()
if !ok {
t.Fatal("SelfWakeStatus 报告闸不在:正常构造的服务不该这样")
}
return st
}