- 只翻注释/文档字符串:剥掉注释后代码行逐字节不变(机械校验只剩允许的行尾注释形态) - 术语对齐既有译法(owner→主人、persona→角色、engage→参与度判定、quiet hours→安静时段、backoff→退避) - 复核:go build 绿 + channelbridge 包测试绿;gofmt 干净
129 lines
4.1 KiB
Go
129 lines
4.1 KiB
Go
package channelbridge
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
|
)
|
|
|
|
// deadSessionAdapter 模拟桥接器运行期间会话死掉的渠道
|
|
// ——目前就是 iLink 会话被拒(errcode=-14):Connect
|
|
// 报告成功,随后读取循环自行返回(没有任何东西取消它),
|
|
// 从那一刻起 IsConnected 报告 false。
|
|
type deadSessionAdapter struct {
|
|
name string
|
|
mu sync.Mutex
|
|
up bool
|
|
}
|
|
|
|
func (a *deadSessionAdapter) Name() string { return a.name }
|
|
func (a *deadSessionAdapter) Config() map[string]any { return map[string]any{"bot_token": "tok"} }
|
|
func (a *deadSessionAdapter) Connect(context.Context) error {
|
|
a.mu.Lock()
|
|
a.up = true
|
|
a.mu.Unlock()
|
|
return nil
|
|
}
|
|
func (a *deadSessionAdapter) Disconnect(context.Context) error {
|
|
a.mu.Lock()
|
|
a.up = false
|
|
a.mu.Unlock()
|
|
return nil
|
|
}
|
|
func (a *deadSessionAdapter) IsConnected() bool {
|
|
a.mu.Lock()
|
|
defer a.mu.Unlock()
|
|
return a.up
|
|
}
|
|
func (a *deadSessionAdapter) HealthCheck(context.Context) error { return adapter.ErrNotConnected }
|
|
func (a *deadSessionAdapter) ReadMessages(context.Context, func(adapter.UnifiedMessage)) {
|
|
// 会话已死:读取循环自行结束,没有任何取消参与其中。
|
|
a.mu.Lock()
|
|
a.up = false
|
|
a.mu.Unlock()
|
|
}
|
|
func (a *deadSessionAdapter) SendMessage(context.Context, adapter.UnifiedResponse) error {
|
|
return adapter.ErrNotConnected
|
|
}
|
|
func (a *deadSessionAdapter) SendProactive(context.Context, adapter.UnifiedMessage) error {
|
|
return adapter.ErrNotConnected
|
|
}
|
|
func (a *deadSessionAdapter) SetMessageHandler(adapter.MessageHandler) {}
|
|
func (a *deadSessionAdapter) Capabilities() adapter.Capabilities {
|
|
return adapter.DefaultCapabilities()
|
|
}
|
|
func (a *deadSessionAdapter) ListChannels(context.Context) ([]adapter.ChannelInfo, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// signaledAdapter 是一个 fakeAdapter,其读取循环在返回时发出信号,
|
|
// 使测试能确定性地等待被替换适配器的退出。
|
|
type signaledAdapter struct {
|
|
*fakeAdapter
|
|
exited chan struct{}
|
|
}
|
|
|
|
func (a *signaledAdapter) ReadMessages(ctx context.Context, _ func(adapter.UnifiedMessage)) {
|
|
<-ctx.Done()
|
|
close(a.exited)
|
|
}
|
|
|
|
// TestReadLoopExitClearsConnectedFlag 是针对「适配器已哑掉、桥接器
|
|
// 却仍声称它已连接」的回归测试:读取循环在会话中断、
|
|
// 无人取消的情况下结束,因此 IsConnected/ConnectedCount 必须停止
|
|
// 报告该渠道存活。
|
|
func TestReadLoopExitClearsConnectedFlag(t *testing.T) {
|
|
t.Parallel()
|
|
b := newReloadBridge(t, nil)
|
|
ad := &deadSessionAdapter{name: "weixin-ilink"}
|
|
b.Add(ad)
|
|
b.Start()
|
|
|
|
waitDeadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(waitDeadline) {
|
|
if !b.IsConnected("weixin-ilink") {
|
|
if n := b.ConnectedCount(); n != 0 {
|
|
t.Fatalf("ConnectedCount() = %d, want 0 after the only read loop died", n)
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("bridge still reports weixin-ilink as connected after its read loop ended")
|
|
}
|
|
|
|
// TestReplacedAdapterExitKeepsReplacementConnected 守住另一方向:
|
|
// 读取循环退出统计绝不能撤销热重载装入的替换适配器。
|
|
// 被替换适配器的循环由 Reload 取消(刻意停止),因此它的
|
|
// 退出对如今占用该名称的适配器毫无说明力。
|
|
func TestReplacedAdapterExitKeepsReplacementConnected(t *testing.T) {
|
|
t.Parallel()
|
|
old := &signaledAdapter{
|
|
fakeAdapter: &fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "old"}},
|
|
exited: make(chan struct{}),
|
|
}
|
|
factory := &stubFactory{adpts: []adapter.PlatformAdapter{
|
|
&fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "new"}},
|
|
}}
|
|
b := newReloadBridge(t, factory)
|
|
b.Add(old)
|
|
b.Start()
|
|
|
|
if err := b.Reload("kook"); err != nil {
|
|
t.Fatalf("Reload: %v", err)
|
|
}
|
|
<-old.exited
|
|
// 退出统计在读取路径返回后立即运行;短促的
|
|
// 静置窗口即可捕获,无需依赖特定的交错时序。
|
|
settle := time.Now().Add(50 * time.Millisecond)
|
|
for time.Now().Before(settle) {
|
|
if !b.IsConnected("kook:conf0") {
|
|
t.Fatal("the replaced adapter's read-loop exit cleared the replacement's connected flag")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|