package channelbridge import ( "context" "errors" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/session" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop" "git.yeij.top/AskaEth/Cyrene_v2/internal/user" ) var errReload = errors.New("build failed") // stubFactory 返回某平台的固定适配器集合,使重载测试 // 无需真实网络 I/O 即可驱动热替换。 type stubFactory struct { adpts []adapter.PlatformAdapter err error } func (f *stubFactory) Build(_ string) ([]adapter.PlatformAdapter, error) { return f.adpts, f.err } func newReloadBridge(t *testing.T, factory PlatformFactory) *Bridge { t.Helper() b, err := New(Options{ Loop: &stubChatter{res: &loop.Result{Text: "hi"}}, Sessions: &stubSessions{sess: session.Session{SessionID: "s", Owner: true, MemWritable: true}}, Owner: &user.OwnerProfile{}, Factory: factory, }) if err != nil { t.Fatalf("New: %v", err) } t.Cleanup(func() { b.Stop(context.Background()) }) return b } func adapterIn(b *Bridge, name string) bool { for _, a := range b.snapshot() { if a.Name() == name { return true } } return false } func TestReloadReplacesAdapterWithNewConfig(t *testing.T) { t.Parallel() old := &fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "old"}} factory := &stubFactory{} b := newReloadBridge(t, factory) b.Add(old) b.Start() if !b.IsConnected("kook:conf0") { t.Fatal("expected old adapter connected after Start") } newConf0 := &fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "new"}} newConf1 := &fakeAdapter{name: "kook:conf1", cfg: map[string]any{"bot_token": "next"}} factory.adpts = []adapter.PlatformAdapter{newConf0, newConf1} if err := b.Reload("kook"); err != nil { t.Fatalf("Reload: %v", err) } // 新适配器已连接并注册。 if !newConf0.IsConnected() || !newConf1.IsConnected() { t.Fatalf("new adapters not connected: conf0=%v conf1=%v", newConf0.IsConnected(), newConf1.IsConnected()) } if !b.IsConnected("kook:conf0") || !b.IsConnected("kook:conf1") { t.Fatalf("bridge does not report new adapters connected") } // 旧适配器实例已断开(其读取循环已停止)。 if old.IsConnected() { t.Fatal("old adapter should be disconnected after reload") } } func TestReloadRemovesRemovedAccount(t *testing.T) { t.Parallel() conf0 := &fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "t0"}} conf1 := &fakeAdapter{name: "kook:conf1", cfg: map[string]any{"bot_token": "t1"}} factory := &stubFactory{} b := newReloadBridge(t, factory) b.Add(conf0) b.Add(conf1) b.Start() // 新配置只保留 conf1(conf0 被删除)。 factory.adpts = []adapter.PlatformAdapter{ &fakeAdapter{name: "kook:conf1", cfg: map[string]any{"bot_token": "t1b"}}, } if err := b.Reload("kook"); err != nil { t.Fatalf("Reload: %v", err) } if adapterIn(b, "kook:conf0") { t.Fatal("removed account's adapter should be dropped from the registry") } if !adapterIn(b, "kook:conf1") { t.Fatal("kept account's adapter should still be registered") } if !b.IsConnected("kook:conf1") { t.Fatal("kept account should be connected") } } func TestReloadOtherPlatformUntouched(t *testing.T) { t.Parallel() tg := &fakeAdapter{name: "telegram", cfg: map[string]any{"bot_token": "tg"}} kook := &fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "k"}} factory := &stubFactory{} b := newReloadBridge(t, factory) b.Add(tg) b.Add(kook) b.Start() factory.adpts = []adapter.PlatformAdapter{ &fakeAdapter{name: "kook:conf0", cfg: map[string]any{"bot_token": "k2"}}, } if err := b.Reload("kook"); err != nil { t.Fatalf("Reload: %v", err) } if !adapterIn(b, "telegram") || !tg.IsConnected() { t.Fatal("reloading kook should leave the telegram adapter untouched") } } func TestReloadNilFactoryErrors(t *testing.T) { t.Parallel() b := newReloadBridge(t, nil) if err := b.Reload("kook"); err != ErrNilFactory { t.Fatalf("err = %v, want ErrNilFactory", err) } } func TestReloadBeforeStartErrors(t *testing.T) { t.Parallel() b := newReloadBridge(t, &stubFactory{}) if err := b.Reload("kook"); err != ErrBridgeClosed { t.Fatalf("err = %v, want ErrBridgeClosed (not started)", err) } } func TestReloadBuildError(t *testing.T) { t.Parallel() b := newReloadBridge(t, &stubFactory{err: errReload}) if err := b.Reload("kook"); err == nil { t.Fatal("expected a build error to surface") } }