package channelbridge // coalesce_owner_test.go 是 M23 的**红线**断言集:主人私聊必须**结构性**地没有静默窗口。 // // 断言的层次(塔 2026-10-24 要求「断言状态,不是断言结果」): // // ① `timers.callCount() == 0` —— 定时器工厂一次都没被调用(窗口**从未被创建**); // ② `lane.batcher == nil` —— 那条车道的类型里根本没有窗口状态; // ③ `CoalesceStats().WindowsCreated == 0` —— 计数面也证明没有窗口; // ④ 消息立刻被回答(没有 2s 迟滞)。 // // 变异检验(塔说会亲自做):把车道类型判定(ownerPrivateLane)改成「主人私聊也走 // batch 车道」⇒ ①②③ 立刻红。 import ( "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig" ) // laneForTest 在锁内取一条车道(测试用;车道可能已退休 ⇒ nil)。 func laneForTest(b *Bridge, key string) *lane { b.lanes.mu.Lock() defer b.lanes.mu.Unlock() return b.lanes.lanes[key] } // sent 返回该适配器已经送出去的回复(并发安全)。 func (a *laneAdapter) sent() []adapter.UnifiedMessage { a.mu.Lock() defer a.mu.Unlock() return append([]adapter.UnifiedMessage(nil), a.sends...) } // ownerPrivateBridge 构造一个「主人私聊」夹具:主人白名单 telegram/100,会话层报 // Owner=true(与生产 SessionManager 的派生一致)。 func ownerPrivateBridge(t *testing.T, chat *laneChatter, p func(string) *runtimeconfig.CoalescePolicy) *coalesceBridge { t.Helper() return newCoalesceBridge(t, Options{ Loop: chat, Sessions: &laneSessions{owner: true}, Owner: testOwnerProfile("telegram", "100"), Coalesce: CoalesceOptions{For: p}, }) } // assertNoTimer 在「等第一轮开跑」的同时盯住定时器工厂:一旦它被调用就立刻失败 // (变异体的**第一种红**正是这里:主人私聊被塞进 batch 车道 ⇒ 窗口被建出来)。 func assertNoTimer(t *testing.T, f *coalesceBridge, chat *laneChatter) { t.Helper() deadline := time.Now().Add(300 * time.Millisecond) for time.Now().Before(deadline) { if calls := f.timers.callCount(); calls != 0 { t.Fatalf("coalesce timer factory called %d times for owner private traffic, want 0", calls) } if chat.runCount() > 0 { return } time.Sleep(time.Millisecond) } } // TestOwnerPrivateNeverCreatesWindow 断言红线:主人私聊**不建窗口、不建定时器**, // 且消息立刻被回答。 func TestOwnerPrivateNeverCreatesWindow(t *testing.T) { t.Parallel() gate := make(chan struct{}) chat := &laneChatter{} chat.onRun = func(string) { <-gate } f := ownerPrivateBridge(t, chat, coalesceFor(coalesceOn(2000, 6000, 24))) f.send(ownerMsg("主人说话")) assertNoTimer(t, f, chat) chat.waitRuns(t, 1, 3*time.Second) // 车道还在(回合卡住)⇒ 这一刻的类型判定是可观察的。 if l := laneForTest(f.b, ownerPrivateLaneKey); l == nil { t.Fatalf("owner private lane %q is missing while its turn is running", ownerPrivateLaneKey) } else if l.batcher != nil { t.Fatalf("owner private lane has a batch window (%+v): the type must be direct", l.batcher) } if calls := f.timers.callCount(); calls != 0 { t.Fatalf("coalesce timer factory called %d times for owner private traffic, want 0", calls) } if stats := f.b.CoalesceStats(); stats.WindowsCreated != 0 { t.Fatalf("windows_created = %d for owner private traffic, want 0", stats.WindowsCreated) } close(gate) f.waitIdle(t) if got := f.ad.sent(); len(got) != 1 { t.Fatalf("replies = %d, want 1 (owner private must be answered immediately)", len(got)) } } // TestOwnerPrivateIgnoresEnabledConfig 断言「配置无权翻转车道类型」:把配置开到最激进 // (enabled / applyToStrangers 全开、窗口 10s),主人私聊仍然没有定时器、没有窗口; // 而同一条配置对**群聊**是生效的(同一个定时器工厂会被调用)—— 后者是这条用例的 // **对照组**,用来证明 calls==0 不是「工厂没接上」这种伪结论。 func TestOwnerPrivateIgnoresEnabledConfig(t *testing.T) { t.Parallel() aggressive := coalesceOn(10000, 30000, 24) aggressive.Enabled = true aggressive.ApplyToStrangers = true gate := make(chan struct{}) chat := &laneChatter{} chat.onRun = func(string) { <-gate } f := ownerPrivateBridge(t, chat, coalesceFor(aggressive)) f.send(ownerMsg("主人私聊")) assertNoTimer(t, f, chat) chat.waitRuns(t, 1, 3*time.Second) if calls := f.timers.callCount(); calls != 0 { t.Fatalf("timer factory called %d times with an aggressive config, want 0", calls) } if l := laneForTest(f.b, ownerPrivateLaneKey); l == nil || l.batcher != nil { t.Fatalf("owner private lane = %+v, want a direct lane (batcher == nil)", l) } // 对照组:同一条配置对群聊**生效**(定时器被请求并按点到发车)。 f.send(coalesceGroupMsg("telegram", "-20", "9", "张三", "群里的消息")) f.advanceAndFireUntil(t) close(gate) chat.waitRuns(t, 2, 3*time.Second) f.waitIdle(t) if calls := f.timers.callCount(); calls == 0 { t.Fatal("control group: group traffic created no timer, so the fixture proves nothing") } if stats := f.b.CoalesceStats(); stats.WindowsCreated != 1 { t.Fatalf("windows_created = %d, want 1 (only the group batch waited)", stats.WindowsCreated) } }