package weixin_ilink import ( "context" "errors" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter" ) func TestAdapter_Name_And_DefaultCapabilities(t *testing.T) { t.Parallel() ad, err := New(Config{BaseURL: "https://ilinkai.weixin.qq.com"}, nil, nil) if err != nil { t.Fatalf("New: %v", err) } if got := ad.Name(); got != "weixin-ilink" { t.Fatalf("Name() = %q, want weixin-ilink", got) } caps := ad.Capabilities() if !caps.Proactive { t.Error("Capabilities().Proactive = false, want true (iLink has a 24h window)") } if !caps.Image { t.Error("Capabilities().Image = false, want true") } if got := caps.Normalize().MaxMessageLength; got <= 0 { t.Fatalf("MaxMessageLength = %d, want > 0", got) } } func TestAdapter_New_RejectsBadBaseURL(t *testing.T) { t.Parallel() _, err := New(Config{BaseURL: "not-a-url"}, nil, nil) if !errors.Is(err, ErrInvalidConfig) { t.Fatalf("err = %v, want ErrInvalidConfig", err) } } func TestAdapter_Config_ReturnsSnapshot(t *testing.T) { t.Parallel() ad, err := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotID: "bot-1"}, nil, nil) if err != nil { t.Fatalf("New: %v", err) } cfg := ad.Config() if cfg["bot_id"] != "bot-1" { t.Fatalf("Config()[bot_id] = %v, want bot-1", cfg["bot_id"]) } if cfg["base_url"] != "https://ilinkai.weixin.qq.com" { t.Fatalf("Config()[base_url] = %v", cfg["base_url"]) } // 经 DecodeConfig 往返必须保留类型化配置。 var back Config if err := adapter.DecodeConfig(cfg, &back); err != nil { t.Fatalf("DecodeConfig: %v", err) } if back.BaseURL != "https://ilinkai.weixin.qq.com" || back.BotID != "bot-1" { t.Fatalf("decoded config = %+v, want base_url/bot_id preserved", back) } } func TestAdapter_Connect_WithToken_IsIdempotent(t *testing.T) { t.Parallel() ad, err := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotToken: "tok-1"}, nil, nil) if err != nil { t.Fatalf("New: %v", err) } if ad.IsConnected() { t.Fatal("IsConnected() = true before Connect") } if err := ad.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if !ad.IsConnected() { t.Fatal("IsConnected() = false after Connect") } // 第二次调用是无副作用的成功。 if err := ad.Connect(context.Background()); err != nil { t.Fatalf("second Connect: %v", err) } } func TestAdapter_HealthCheck(t *testing.T) { t.Parallel() t.Run("disconnected", func(t *testing.T) { t.Parallel() ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com"}, nil, nil) if err := ad.HealthCheck(context.Background()); !errors.Is(err, adapter.ErrNotConnected) { t.Fatalf("HealthCheck = %v, want ErrNotConnected", err) } }) t.Run("connected_with_token", func(t *testing.T) { t.Parallel() ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotToken: "tok-1"}, nil, nil) if err := ad.Connect(context.Background()); err != nil { t.Fatalf("Connect: %v", err) } if err := ad.HealthCheck(context.Background()); err != nil { t.Fatalf("HealthCheck = %v, want nil", err) } }) } func TestAdapter_Disconnect_IsIdempotent(t *testing.T) { t.Parallel() ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotToken: "tok-1"}, nil, nil) _ = ad.Connect(context.Background()) if err := ad.Disconnect(context.Background()); err != nil { t.Fatalf("Disconnect: %v", err) } if ad.IsConnected() { t.Fatal("IsConnected() = true after Disconnect") } if err := ad.Disconnect(context.Background()); err != nil { t.Fatalf("second Disconnect: %v", err) } } func TestAdapter_ListChannels(t *testing.T) { t.Parallel() ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com", BotID: "bot-9"}, nil, nil) chans, err := ad.ListChannels(context.Background()) if err != nil { t.Fatalf("ListChannels: %v", err) } if len(chans) != 1 { t.Fatalf("ListChannels() = %d channels, want 1", len(chans)) } if chans[0].ChannelID != "bot-9" || chans[0].ChannelType != adapter.ChannelDirect { t.Fatalf("channel = %+v, want {bot-9 direct}", chans[0]) } } func TestAdapter_SetMessageHandler_And_RecordInbound(t *testing.T) { t.Parallel() ad, _ := New(Config{BaseURL: "https://ilinkai.weixin.qq.com"}, nil, nil) called := false ad.SetMessageHandler(func(ctx context.Context, m adapter.UnifiedMessage) { called = true }) if ad.handlerLocked() == nil { t.Fatal("handlerLocked() = nil after SetMessageHandler") } ad.SetMessageHandler(nil) if ad.handlerLocked() != nil { t.Fatal("handlerLocked() != nil after clearing") } if called { t.Fatal("handler called before any message") } // recordInbound 必须刷新按对端的 token 与最后消息时间。 ad.recordInbound("tok-abc", adapter.UnifiedMessage{ SenderID: "user-1", Timestamp: ad.clock(), }) if got := ad.contextFor("user-1"); got != "tok-abc" { t.Fatalf("contextFor(user-1) = %q, want tok-abc", got) } } func TestBuildClientVersion(t *testing.T) { t.Parallel() cases := map[string]string{ "2.4.8": "132104", // 官方 @tencent-weixin/openclaw-weixin 2.4.8 "1.0.11": "65547", // 官方注释示例 0x0001000B "1.2.3": "66051", "0.0.0": "0", "bad": "0", } for in, want := range cases { if got := buildClientVersion(in); got != want { t.Fatalf("buildClientVersion(%q) = %q, want %q", in, got, want) } } } func TestAdapter_Recipient_ResolvesGroupAndSender(t *testing.T) { t.Parallel() tests := []struct { name string msg adapter.UnifiedMessage wantPeer string wantGroup bool }{ {name: "direct sender", msg: adapter.UnifiedMessage{SenderID: "u1"}, wantPeer: "u1"}, {name: "group id", msg: adapter.UnifiedMessage{ChannelType: adapter.ChannelGroup, GroupID: "g1"}, wantPeer: "g1", wantGroup: true}, {name: "default channel id", msg: adapter.UnifiedMessage{ChannelID: "c1"}, wantPeer: "c1"}, } for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() peer, group := recipient(tc.msg) if peer != tc.wantPeer || group != tc.wantGroup { t.Fatalf("recipient() = (%q,%v), want (%q,%v)", peer, group, tc.wantPeer, tc.wantGroup) } }) } }