| 1 | package cli |
| 2 | |
| 3 | import ( |
| 4 | "io" |
| 5 | "log/slog" |
| 6 | "os" |
| 7 | "path/filepath" |
| 8 | "strings" |
| 9 | "testing" |
| 10 | |
| 11 | "reasonix/internal/bot" |
| 12 | "reasonix/internal/botruntime" |
| 13 | "reasonix/internal/config" |
| 14 | ) |
| 15 | |
| 16 | func TestRememberBotRemoteStoresIncomingChatID(t *testing.T) { |
| 17 | isolateBotUserConfig(t) |
| 18 | cfg := config.Default() |
| 19 | cfg.Bot.Connections = []config.BotConnectionConfig{ |
| 20 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Label: "飞书", Enabled: true, Status: "connected"}, |
| 21 | {ID: "weixin-weixin", Provider: "weixin", Domain: "weixin", Label: "微信", Enabled: true, Status: "connected"}, |
| 22 | } |
| 23 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 24 | t.Fatalf("save config: %v", err) |
| 25 | } |
| 26 | |
| 27 | msg := bot.InboundMessage{ |
| 28 | Platform: bot.PlatformWeixin, |
| 29 | ChatType: bot.ChatDM, |
| 30 | ChatID: "wx-chat-1", |
| 31 | UserID: "wx-user-1", |
| 32 | } |
| 33 | if err := botruntime.RememberInbound(msg); err != nil { |
| 34 | t.Fatalf("rememberBotInbound: %v", err) |
| 35 | } |
| 36 | if err := botruntime.RememberInbound(msg); err != nil { |
| 37 | t.Fatalf("rememberBotRemote duplicate: %v", err) |
| 38 | } |
| 39 | |
| 40 | got := config.LoadForEdit(config.UserConfigPath()) |
| 41 | if len(got.Bot.Connections) != 2 { |
| 42 | t.Fatalf("connections = %d, want 2", len(got.Bot.Connections)) |
| 43 | } |
| 44 | var wx config.BotConnectionConfig |
| 45 | var fs config.BotConnectionConfig |
| 46 | for _, conn := range got.Bot.Connections { |
| 47 | switch conn.ID { |
| 48 | case "weixin-weixin": |
| 49 | wx = conn |
| 50 | case "feishu-feishu": |
| 51 | fs = conn |
| 52 | } |
| 53 | } |
| 54 | if len(fs.SessionMappings) != 0 { |
| 55 | t.Fatalf("feishu mappings = %+v, want none", fs.SessionMappings) |
| 56 | } |
| 57 | if len(wx.SessionMappings) != 1 { |
| 58 | t.Fatalf("weixin mappings = %+v, want one", wx.SessionMappings) |
| 59 | } |
| 60 | if m := wx.SessionMappings[0]; m.RemoteID != "wx-chat-1" || m.Scope != "global" || m.WorkspaceRoot != "" || m.UpdatedAt == "" { |
| 61 | t.Fatalf("weixin mapping = %+v, want global wx-chat-1 with timestamp", m) |
| 62 | } |
| 63 | if got := got.Bot.Allowlist.WeixinUsers; len(got) != 1 || got[0] != "wx-user-1" { |
| 64 | t.Fatalf("weixin users = %+v, want wx-user-1", got) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | func TestRememberBotRemoteKeepsProjectScopedConnection(t *testing.T) { |
| 69 | isolateBotUserConfig(t) |
| 70 | workspace := filepath.Join(t.TempDir(), "project") |
| 71 | cfg := config.Default() |
| 72 | cfg.Bot.Connections = []config.BotConnectionConfig{{ |
| 73 | ID: "feishu-project", |
| 74 | Provider: "feishu", |
| 75 | Domain: "feishu", |
| 76 | Label: "飞书", |
| 77 | Enabled: true, |
| 78 | Status: "connected", |
| 79 | WorkspaceRoot: workspace, |
| 80 | }} |
| 81 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 82 | t.Fatalf("save config: %v", err) |
| 83 | } |
| 84 | |
| 85 | if err := botruntime.RememberInbound(bot.InboundMessage{ |
| 86 | Platform: bot.PlatformFeishu, |
| 87 | ChatType: bot.ChatDM, |
| 88 | ChatID: "oc-chat-1", |
| 89 | UserID: "ou-user-1", |
| 90 | }); err != nil { |
| 91 | t.Fatalf("rememberBotInbound: %v", err) |
| 92 | } |
| 93 | |
| 94 | got := config.LoadForEdit(config.UserConfigPath()) |
| 95 | if len(got.Bot.Connections) != 1 || len(got.Bot.Connections[0].SessionMappings) != 1 { |
| 96 | t.Fatalf("connections = %+v, want one project mapping", got.Bot.Connections) |
| 97 | } |
| 98 | if m := got.Bot.Connections[0].SessionMappings[0]; m.RemoteID != "oc-chat-1" || m.Scope != "project" || m.WorkspaceRoot != workspace { |
| 99 | t.Fatalf("mapping = %+v, want project scoped remote", m) |
| 100 | } |
| 101 | if got := got.Bot.Allowlist.FeishuUsers; len(got) != 1 || got[0] != "ou-user-1" { |
| 102 | t.Fatalf("feishu users = %+v, want ou-user-1", got) |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | func TestRememberBotInboundStoresGroupAllowlist(t *testing.T) { |
| 107 | isolateBotUserConfig(t) |
| 108 | cfg := config.Default() |
| 109 | cfg.Bot.Connections = []config.BotConnectionConfig{ |
| 110 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Label: "飞书", Enabled: true, Status: "connected"}, |
| 111 | } |
| 112 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 113 | t.Fatalf("save config: %v", err) |
| 114 | } |
| 115 | |
| 116 | msg := bot.InboundMessage{ |
| 117 | Platform: bot.PlatformFeishu, |
| 118 | ChatType: bot.ChatGroup, |
| 119 | ChatID: "oc-group-1", |
| 120 | UserID: "ou-user-1", |
| 121 | } |
| 122 | if err := botruntime.RememberInbound(msg); err != nil { |
| 123 | t.Fatalf("rememberBotInbound: %v", err) |
| 124 | } |
| 125 | if err := botruntime.RememberInbound(msg); err != nil { |
| 126 | t.Fatalf("rememberBotInbound duplicate: %v", err) |
| 127 | } |
| 128 | |
| 129 | got := config.LoadForEdit(config.UserConfigPath()) |
| 130 | if users := got.Bot.Allowlist.FeishuUsers; len(users) != 1 || users[0] != "ou-user-1" { |
| 131 | t.Fatalf("feishu users = %+v, want one ou-user-1", users) |
| 132 | } |
| 133 | if groups := got.Bot.Allowlist.FeishuGroups; len(groups) != 1 || groups[0] != "oc-group-1" { |
| 134 | t.Fatalf("feishu groups = %+v, want one oc-group-1", groups) |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | func TestBotDoctorReportsSessionMappingCounts(t *testing.T) { |
| 139 | isolateBotUserConfig(t) |
| 140 | cfg := config.Default() |
| 141 | cfg.Bot.Connections = []config.BotConnectionConfig{ |
| 142 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Label: "飞书", Enabled: true, Status: "connected"}, |
| 143 | {ID: "weixin-weixin", Provider: "weixin", Domain: "weixin", Label: "微信", Enabled: true, Status: "connected"}, |
| 144 | } |
| 145 | cfg.Bot.Connections[0].SessionMappings = []config.BotConnectionSessionMapping{{RemoteID: "oc-chat-1", Scope: "global"}} |
| 146 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 147 | t.Fatalf("save config: %v", err) |
| 148 | } |
| 149 | |
| 150 | out := captureStdout(t, func() { |
| 151 | if rc := botDoctor([]string{"--json"}); rc != 0 { |
| 152 | t.Fatalf("botDoctor rc = %d, want 0", rc) |
| 153 | } |
| 154 | }) |
| 155 | for _, want := range []string{ |
| 156 | `"name":"bot.connections","status":"ok","detail":"enabled=2 total=2"`, |
| 157 | `"name":"bot.connection.feishu-feishu.session_mappings","status":"ok","detail":"provider=feishu mappings=1"`, |
| 158 | `"name":"bot.connection.weixin-weixin.session_mappings","status":"missing","detail":"provider=weixin mappings=0"`, |
| 159 | } { |
| 160 | if !strings.Contains(out, want) { |
| 161 | t.Fatalf("bot doctor output missing %s:\n%s", want, out) |
| 162 | } |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | func TestBotDoctorDeepReportsPairingAndRoles(t *testing.T) { |
| 167 | isolateBotUserConfig(t) |
| 168 | cfg := config.Default() |
| 169 | cfg.Bot.Enabled = true |
| 170 | cfg.Bot.Pairing.Enabled = true |
| 171 | cfg.Bot.Allowlist.Enabled = true |
| 172 | cfg.Bot.Allowlist.FeishuUsers = []string{"ou-user"} |
| 173 | cfg.Bot.Allowlist.FeishuApprovers = []string{"ou-approver"} |
| 174 | cfg.Bot.Allowlist.FeishuAdmins = []string{"ou-admin"} |
| 175 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 176 | t.Fatalf("save config: %v", err) |
| 177 | } |
| 178 | |
| 179 | if _, _, err := bot.CreateOrRefreshPairingRequest(bot.InboundMessage{ |
| 180 | Platform: bot.PlatformFeishu, |
| 181 | ConnectionID: "feishu-feishu", |
| 182 | ChatType: bot.ChatDM, |
| 183 | ChatID: "chat", |
| 184 | UserID: "pending-user", |
| 185 | }, bot.PairingConfig{Enabled: true}); err != nil { |
| 186 | t.Fatalf("create pairing: %v", err) |
| 187 | } |
| 188 | |
| 189 | out := captureStdout(t, func() { |
| 190 | if rc := botDoctor([]string{"--json", "--deep"}); rc != 0 { |
| 191 | t.Fatalf("botDoctor rc = %d, want 0", rc) |
| 192 | } |
| 193 | }) |
| 194 | for _, want := range []string{ |
| 195 | `"name":"bot.pairing.pending","status":"ok","detail":"1 pending"`, |
| 196 | `"name":"bot.roles","status":"ok","detail":"approvers=1 admins=1"`, |
| 197 | `"name":"bot.config.user","status":"ok"`, |
| 198 | } { |
| 199 | if !strings.Contains(out, want) { |
| 200 | t.Fatalf("bot doctor deep output missing %s:\n%s", want, out) |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | func TestBotPairingApproveAddsAllowlistAndFirstAdmin(t *testing.T) { |
| 206 | isolateBotUserConfig(t) |
| 207 | cfg := config.Default() |
| 208 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 209 | t.Fatalf("save config: %v", err) |
| 210 | } |
| 211 | req, _, err := bot.CreateOrRefreshPairingRequest(bot.InboundMessage{ |
| 212 | Platform: bot.PlatformWeixin, |
| 213 | ChatType: bot.ChatDM, |
| 214 | ChatID: "wx-chat", |
| 215 | UserID: "wx-user", |
| 216 | }, bot.PairingConfig{Enabled: true}) |
| 217 | if err != nil { |
| 218 | t.Fatalf("create pairing: %v", err) |
| 219 | } |
| 220 | |
| 221 | if rc := botPairing([]string{"approve", req.Code}); rc != 0 { |
| 222 | t.Fatalf("botPairing approve rc = %d, want 0", rc) |
| 223 | } |
| 224 | got := config.LoadForEdit(config.UserConfigPath()) |
| 225 | if users := got.Bot.Allowlist.WeixinUsers; len(users) != 1 || users[0] != "wx-user" { |
| 226 | t.Fatalf("weixin users = %+v, want wx-user", users) |
| 227 | } |
| 228 | if admins := got.Bot.Allowlist.WeixinAdmins; len(admins) != 1 || admins[0] != "wx-user" { |
| 229 | t.Fatalf("weixin admins = %+v, want first paired admin", admins) |
| 230 | } |
| 231 | if approvers := got.Bot.Allowlist.WeixinApprovers; len(approvers) != 1 || approvers[0] != "wx-user" { |
| 232 | t.Fatalf("weixin approvers = %+v, want first paired approver", approvers) |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | func TestBotPairingApproveAddsUserToConnectionAccess(t *testing.T) { |
| 237 | isolateBotUserConfig(t) |
| 238 | cfg := config.Default() |
| 239 | cfg.Bot.Connections = []config.BotConnectionConfig{{ |
| 240 | ID: "feishu-lark", |
| 241 | Provider: "feishu", |
| 242 | Domain: "lark", |
| 243 | Label: "Lark", |
| 244 | Enabled: true, |
| 245 | Status: "connected", |
| 246 | Access: config.BotAccessConfig{ |
| 247 | Enabled: true, |
| 248 | PairingEnabled: true, |
| 249 | Users: []string{"ou-existing"}, |
| 250 | }, |
| 251 | }} |
| 252 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 253 | t.Fatalf("save config: %v", err) |
| 254 | } |
| 255 | req, _, err := bot.CreateOrRefreshPairingRequest(bot.InboundMessage{ |
| 256 | Platform: bot.PlatformFeishu, |
| 257 | ConnectionID: "feishu-lark", |
| 258 | Domain: "lark", |
| 259 | ChatType: bot.ChatDM, |
| 260 | ChatID: "oc-chat", |
| 261 | UserID: "ou-new", |
| 262 | }, bot.PairingConfig{Enabled: true}) |
| 263 | if err != nil { |
| 264 | t.Fatalf("create pairing: %v", err) |
| 265 | } |
| 266 | |
| 267 | if rc := botPairing([]string{"approve", req.Code}); rc != 0 { |
| 268 | t.Fatalf("botPairing approve rc = %d, want 0", rc) |
| 269 | } |
| 270 | got := config.LoadForEdit(config.UserConfigPath()) |
| 271 | if users := got.Bot.Allowlist.FeishuUsers; len(users) != 0 { |
| 272 | t.Fatalf("global feishu users = %+v, want unchanged global allowlist", users) |
| 273 | } |
| 274 | if len(got.Bot.Connections) != 1 { |
| 275 | t.Fatalf("connections = %+v, want one connection", got.Bot.Connections) |
| 276 | } |
| 277 | access := got.Bot.Connections[0].Access |
| 278 | if !access.Enabled { |
| 279 | t.Fatal("connection access disabled after approval, want enabled") |
| 280 | } |
| 281 | for _, want := range []string{"ou-existing", "ou-new"} { |
| 282 | if !hasTestString(access.Users, want) { |
| 283 | t.Fatalf("connection users = %+v, want %s", access.Users, want) |
| 284 | } |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | func TestBotDoctorPrefersUserBotSettingsOverProjectBotConfig(t *testing.T) { |
| 289 | isolateBotUserConfig(t) |
| 290 | userCfg := config.Default() |
| 291 | userCfg.Bot.Enabled = true |
| 292 | userCfg.Bot.Allowlist.Enabled = true |
| 293 | userCfg.Bot.Allowlist.FeishuUsers = []string{"ou-user"} |
| 294 | userCfg.Bot.Connections = []config.BotConnectionConfig{ |
| 295 | {ID: "feishu-lark", Provider: "feishu", Domain: "lark", Label: "Lark", Enabled: true, Status: "connected"}, |
| 296 | } |
| 297 | if err := userCfg.SaveTo(config.UserConfigPath()); err != nil { |
| 298 | t.Fatalf("save user config: %v", err) |
| 299 | } |
| 300 | |
| 301 | project := t.TempDir() |
| 302 | if err := os.WriteFile(filepath.Join(project, "reasonix.toml"), []byte(` |
| 303 | [bot] |
| 304 | enabled = false |
| 305 | `), 0o644); err != nil { |
| 306 | t.Fatalf("write project config: %v", err) |
| 307 | } |
| 308 | t.Chdir(project) |
| 309 | |
| 310 | out := captureStdout(t, func() { |
| 311 | if rc := botDoctor([]string{"--json"}); rc != 0 { |
| 312 | t.Fatalf("botDoctor rc = %d, want 0", rc) |
| 313 | } |
| 314 | }) |
| 315 | for _, want := range []string{ |
| 316 | `"name":"bot.enabled","status":"ok"`, |
| 317 | `"name":"bot.connections","status":"ok","detail":"enabled=1 total=1"`, |
| 318 | `"name":"bot.connection.feishu-lark.session_mappings","status":"missing","detail":"provider=feishu mappings=0"`, |
| 319 | } { |
| 320 | if !strings.Contains(out, want) { |
| 321 | t.Fatalf("bot doctor output missing %s:\n%s", want, out) |
| 322 | } |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | func TestBotDoctorUsesProjectBotConfigWhenUserBotIsUnconfigured(t *testing.T) { |
| 327 | isolateBotUserConfig(t) |
| 328 | projectCfg := config.Default() |
| 329 | projectCfg.Bot.Enabled = true |
| 330 | projectCfg.Bot.Allowlist.AllowAll = true |
| 331 | projectCfg.Bot.Connections = []config.BotConnectionConfig{ |
| 332 | {ID: "weixin-weixin", Provider: "weixin", Domain: "weixin", Label: "微信", Enabled: true, Status: "connected"}, |
| 333 | } |
| 334 | if err := projectCfg.SaveTo("reasonix.toml"); err != nil { |
| 335 | t.Fatalf("save project config: %v", err) |
| 336 | } |
| 337 | |
| 338 | out := captureStdout(t, func() { |
| 339 | if rc := botDoctor([]string{"--json"}); rc != 0 { |
| 340 | t.Fatalf("botDoctor rc = %d, want 0", rc) |
| 341 | } |
| 342 | }) |
| 343 | for _, want := range []string{ |
| 344 | `"name":"bot.enabled","status":"ok"`, |
| 345 | `"name":"bot.connections","status":"ok","detail":"enabled=1 total=1"`, |
| 346 | `"name":"bot.allowlist","status":"open"`, |
| 347 | } { |
| 348 | if !strings.Contains(out, want) { |
| 349 | t.Fatalf("bot doctor output missing %s:\n%s", want, out) |
| 350 | } |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | func TestBotDoctorUsesProjectBotConfigWhenUserConfigOnlyHasBotDefaults(t *testing.T) { |
| 355 | isolateBotUserConfig(t) |
| 356 | userCfg := config.Default() |
| 357 | if err := userCfg.SaveTo(config.UserConfigPath()); err != nil { |
| 358 | t.Fatalf("save user config: %v", err) |
| 359 | } |
| 360 | projectCfg := config.Default() |
| 361 | projectCfg.Bot.Enabled = true |
| 362 | projectCfg.Bot.Allowlist.AllowAll = true |
| 363 | projectCfg.Bot.Connections = []config.BotConnectionConfig{ |
| 364 | {ID: "feishu-lark", Provider: "feishu", Domain: "lark", Label: "Lark", Enabled: true, Status: "connected"}, |
| 365 | } |
| 366 | if err := projectCfg.SaveTo("reasonix.toml"); err != nil { |
| 367 | t.Fatalf("save project config: %v", err) |
| 368 | } |
| 369 | |
| 370 | out := captureStdout(t, func() { |
| 371 | if rc := botDoctor([]string{"--json"}); rc != 0 { |
| 372 | t.Fatalf("botDoctor rc = %d, want 0", rc) |
| 373 | } |
| 374 | }) |
| 375 | for _, want := range []string{ |
| 376 | `"name":"bot.enabled","status":"ok"`, |
| 377 | `"name":"bot.connections","status":"ok","detail":"enabled=1 total=1"`, |
| 378 | `"name":"bot.allowlist","status":"open"`, |
| 379 | } { |
| 380 | if !strings.Contains(out, want) { |
| 381 | t.Fatalf("bot doctor output missing %s:\n%s", want, out) |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | func TestBotConnectionChannelConfigsKeepFeishuAndLarkSeparate(t *testing.T) { |
| 387 | connections := []config.BotConnectionConfig{ |
| 388 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Enabled: true, Model: "feishu-model", WorkspaceRoot: "/feishu"}, |
| 389 | {ID: "feishu-lark", Provider: "feishu", Domain: "lark", Enabled: true, Model: "lark-model", WorkspaceRoot: "/lark"}, |
| 390 | } |
| 391 | channels := botruntime.ConnectionChannelConfigs(connections, true, true) |
| 392 | if channels["feishu-feishu"].Model != "feishu-model" || channels["feishu-feishu"].WorkspaceRoot != "/feishu" { |
| 393 | t.Fatalf("feishu channel = %+v, want feishu override", channels["feishu-feishu"]) |
| 394 | } |
| 395 | if channels["feishu-lark"].Model != "lark-model" || channels["feishu-lark"].WorkspaceRoot != "/lark" { |
| 396 | t.Fatalf("lark channel = %+v, want lark override", channels["feishu-lark"]) |
| 397 | } |
| 398 | } |
| 399 | |
| 400 | func TestBotAdapterBindingsCreateSeparateFeishuAndLarkInstances(t *testing.T) { |
| 401 | cfg := config.Default() |
| 402 | cfg.Bot.Connections = []config.BotConnectionConfig{ |
| 403 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Enabled: true, Credential: config.BotConnectionCredential{AppID: "cli-feishu", AppSecretEnv: "FEISHU_BOT_APP_SECRET"}}, |
| 404 | {ID: "feishu-lark", Provider: "feishu", Domain: "lark", Enabled: true, Credential: config.BotConnectionCredential{AppID: "cli-lark", AppSecretEnv: "LARK_BOT_APP_SECRET"}}, |
| 405 | {ID: "weixin-weixin", Provider: "weixin", Domain: "weixin", Enabled: true, Credential: config.BotConnectionCredential{AccountID: "wx-account", TokenEnv: "WEIXIN_BOT_TOKEN"}}, |
| 406 | } |
| 407 | logger := slog.New(slog.NewTextHandler(io.Discard, nil)) |
| 408 | bindings := botruntime.AdapterBindings(cfg, map[bot.Platform]bool{bot.PlatformFeishu: true, bot.PlatformWeixin: true}, nil, logger) |
| 409 | |
| 410 | got := map[string]bot.AdapterBinding{} |
| 411 | for _, binding := range bindings { |
| 412 | got[binding.ID] = binding |
| 413 | } |
| 414 | for _, id := range []string{"feishu-feishu", "feishu-lark", "weixin-weixin"} { |
| 415 | if got[id].Adapter == nil { |
| 416 | t.Fatalf("binding %s missing from %+v", id, bindings) |
| 417 | } |
| 418 | } |
| 419 | if got["feishu-feishu"].Domain != "feishu" || got["feishu-lark"].Domain != "lark" { |
| 420 | t.Fatalf("domains = feishu:%q lark:%q, want separate domains", got["feishu-feishu"].Domain, got["feishu-lark"].Domain) |
| 421 | } |
| 422 | } |
| 423 | |
| 424 | func TestBotAdapterBindingsIsolateRequestedFeishuDomain(t *testing.T) { |
| 425 | cfg := config.Default() |
| 426 | cfg.Bot.Connections = []config.BotConnectionConfig{ |
| 427 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Enabled: true, Credential: config.BotConnectionCredential{AppID: "cli-feishu", AppSecretEnv: "FEISHU_BOT_APP_SECRET"}}, |
| 428 | {ID: "feishu-lark", Provider: "feishu", Domain: "lark", Enabled: true, Credential: config.BotConnectionCredential{AppID: "cli-lark", AppSecretEnv: "LARK_BOT_APP_SECRET"}}, |
| 429 | } |
| 430 | logger := slog.New(slog.NewTextHandler(io.Discard, nil)) |
| 431 | enabled := map[bot.Platform]bool{bot.PlatformFeishu: true} |
| 432 | |
| 433 | larkOnly := botruntime.AdapterBindings(cfg, enabled, botruntime.RequestedFeishuDomains([]string{"lark"}), logger) |
| 434 | if len(larkOnly) != 1 || larkOnly[0].ID != "feishu-lark" { |
| 435 | t.Fatalf("--channels lark bindings = %+v, want only feishu-lark", larkOnly) |
| 436 | } |
| 437 | |
| 438 | feishuOnly := botruntime.AdapterBindings(cfg, enabled, botruntime.RequestedFeishuDomains([]string{"feishu"}), logger) |
| 439 | if len(feishuOnly) != 1 || feishuOnly[0].ID != "feishu-feishu" { |
| 440 | t.Fatalf("--channels feishu bindings = %+v, want only feishu-feishu", feishuOnly) |
| 441 | } |
| 442 | } |
| 443 | |
| 444 | func TestRememberBotInboundUsesConnectionID(t *testing.T) { |
| 445 | isolateBotUserConfig(t) |
| 446 | cfg := config.Default() |
| 447 | cfg.Bot.Connections = []config.BotConnectionConfig{ |
| 448 | {ID: "feishu-feishu", Provider: "feishu", Domain: "feishu", Label: "飞书", Enabled: true, Status: "connected"}, |
| 449 | {ID: "feishu-lark", Provider: "feishu", Domain: "lark", Label: "Lark", Enabled: true, Status: "connected"}, |
| 450 | } |
| 451 | if err := cfg.SaveTo(config.UserConfigPath()); err != nil { |
| 452 | t.Fatalf("save config: %v", err) |
| 453 | } |
| 454 | |
| 455 | if err := botruntime.RememberInbound(bot.InboundMessage{ |
| 456 | Platform: bot.PlatformFeishu, |
| 457 | ConnectionID: "feishu-lark", |
| 458 | Domain: "lark", |
| 459 | ChatType: bot.ChatDM, |
| 460 | ChatID: "oc-lark-chat", |
| 461 | UserID: "ou-lark-user", |
| 462 | }); err != nil { |
| 463 | t.Fatalf("rememberBotInbound: %v", err) |
| 464 | } |
| 465 | |
| 466 | got := config.LoadForEdit(config.UserConfigPath()) |
| 467 | var feishuConn, larkConn config.BotConnectionConfig |
| 468 | for _, conn := range got.Bot.Connections { |
| 469 | switch conn.ID { |
| 470 | case "feishu-feishu": |
| 471 | feishuConn = conn |
| 472 | case "feishu-lark": |
| 473 | larkConn = conn |
| 474 | } |
| 475 | } |
| 476 | if len(feishuConn.SessionMappings) != 0 { |
| 477 | t.Fatalf("feishu mappings = %+v, want none", feishuConn.SessionMappings) |
| 478 | } |
| 479 | if len(larkConn.SessionMappings) != 1 || larkConn.SessionMappings[0].RemoteID != "oc-lark-chat" { |
| 480 | t.Fatalf("lark mappings = %+v, want lark chat only", larkConn.SessionMappings) |
| 481 | } |
| 482 | } |
| 483 | |
| 484 | func isolateBotUserConfig(t *testing.T) { |
| 485 | t.Helper() |
| 486 | home := t.TempDir() |
| 487 | t.Setenv("HOME", home) |
| 488 | t.Setenv("USERPROFILE", home) |
| 489 | t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config")) |
| 490 | t.Setenv("AppData", filepath.Join(home, "AppData")) |
| 491 | t.Chdir(t.TempDir()) |
| 492 | } |
| 493 | |
| 494 | func hasTestString(values []string, want string) bool { |
| 495 | for _, value := range values { |
| 496 | if value == want { |
| 497 | return true |
| 498 | } |
| 499 | } |
| 500 | return false |
| 501 | } |
| 502 |