fix: KOOK 连不上根因——空token裸账号不注册/占名 + open指向带token账号 + 前端token不覆盖真值
agent-119 后端: - buildAccountAdapter: token平台(kook/telegram/weixin)BotToken空→跳过build(不注册不占名), channelAccountsFromEnv 无tokenenv不合成账号(杜绝裸kook id="") - runtimeconfig cleanAccounts: LoadChannels/SaveChannels 丢弃token平台空token账号(清理channels.json现有裸kook, 下次保存抹掉) - findChannel: 裸平台名(kook)回退到该平台第一个带token账号, open"kook"不再命中裸的missing token 我补 前端token掩码不就覆真值: - openConfig 不再把'(已配置,仅掩码显示)'当token回填(setToken空)→save token空不写bot_token保留真值 - 加 tokenHint(显示'已配置留空不改'/重填换); 修之前channels.json kook:3fc8 token变掩码串的bug 全仓 build/vet/test绿+前端build绿(463行)
This commit is contained in:
1 parent
5398486fdf
commit
b614633432
7 files changed
+314
-30
No files matched your search
+44
-24
@@ -19,6 +19,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/channel/adapter"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
||||
@@ -84,25 +85,33 @@ func buildPlatformAdapters(platform string, accts []config.ChannelAccount, log *
|
||||
}
|
||||
|
||||
// channelAccountsFromEnv synthesizes the env fallback: the single-account
|
||||
// CHANNEL_<NAME>_* adapters (always present so the channel list stays stable and
|
||||
// an unset token simply stays disconnected) plus any CHANNELS_JSON multi-account
|
||||
// entries, which coexist with the single-account adapters as before. It is the
|
||||
// .env fallback passed to runtimeconfig.LoadChannels when channels.json is absent
|
||||
// or invalid.
|
||||
// CHANNEL_<NAME>_* adapters plus any CHANNELS_JSON multi-account entries. It is
|
||||
// the .env fallback passed to runtimeconfig.LoadChannels when channels.json is
|
||||
// absent or invalid.
|
||||
//
|
||||
// A token-requiring platform (kook/telegram/weixin-ilink) is synthesized only
|
||||
// when its token env var is set; with an unset token there is no account at all,
|
||||
// so an empty-token placeholder never claims the bare platform name. qq-obv11 and
|
||||
// webhook are always present (they do not need a token; the bridge's configured()
|
||||
// gate decides whether to dial) so the channel list stays stable.
|
||||
func channelAccountsFromEnv() map[string][]config.ChannelAccount {
|
||||
out := map[string][]config.ChannelAccount{}
|
||||
out["kook"] = append(out["kook"], config.ChannelAccount{Config: map[string]any{
|
||||
"bot_token": os.Getenv("CHANNEL_KOOK_TOKEN"),
|
||||
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
|
||||
"receive_mode": os.Getenv("CHANNEL_KOOK_RECEIVE_MODE"),
|
||||
"webhook_path": os.Getenv("CHANNEL_KOOK_WEBHOOK_PATH"),
|
||||
"webhook_verify_token": os.Getenv("CHANNEL_KOOK_WEBHOOK_VERIFY_TOKEN"),
|
||||
"webhook_encrypt_key": os.Getenv("CHANNEL_KOOK_WEBHOOK_ENCRYPT_KEY"),
|
||||
}})
|
||||
out["telegram"] = append(out["telegram"], config.ChannelAccount{Config: map[string]any{
|
||||
"bot_token": os.Getenv("CHANNEL_TELEGRAM_BOT_TOKEN"),
|
||||
"webhook_url": os.Getenv("CHANNEL_TELEGRAM_WEBHOOK_URL"),
|
||||
}})
|
||||
if tok := os.Getenv("CHANNEL_KOOK_TOKEN"); tok != "" {
|
||||
out["kook"] = append(out["kook"], config.ChannelAccount{Config: map[string]any{
|
||||
"bot_token": tok,
|
||||
"base_url": os.Getenv("CHANNEL_KOOK_BASE_URL"),
|
||||
"receive_mode": os.Getenv("CHANNEL_KOOK_RECEIVE_MODE"),
|
||||
"webhook_path": os.Getenv("CHANNEL_KOOK_WEBHOOK_PATH"),
|
||||
"webhook_verify_token": os.Getenv("CHANNEL_KOOK_WEBHOOK_VERIFY_TOKEN"),
|
||||
"webhook_encrypt_key": os.Getenv("CHANNEL_KOOK_WEBHOOK_ENCRYPT_KEY"),
|
||||
}})
|
||||
}
|
||||
if tok := os.Getenv("CHANNEL_TELEGRAM_BOT_TOKEN"); tok != "" {
|
||||
out["telegram"] = append(out["telegram"], config.ChannelAccount{Config: map[string]any{
|
||||
"bot_token": tok,
|
||||
"webhook_url": os.Getenv("CHANNEL_TELEGRAM_WEBHOOK_URL"),
|
||||
}})
|
||||
}
|
||||
out["qq-obv11"] = append(out["qq-obv11"], config.ChannelAccount{Config: map[string]any{
|
||||
"mode": envString("CHANNEL_QQ_OBV11_MODE", "server"),
|
||||
"listen_addr": os.Getenv("CHANNEL_QQ_OBV11_LISTEN_ADDR"),
|
||||
@@ -117,13 +126,15 @@ func channelAccountsFromEnv() map[string][]config.ChannelAccount {
|
||||
"secret": os.Getenv("CHANNEL_WEBHOOK_SECRET"),
|
||||
"signatureHeader": os.Getenv("CHANNEL_WEBHOOK_SIGNATURE_HEADER"),
|
||||
}})
|
||||
out["weixin-ilink"] = append(out["weixin-ilink"], config.ChannelAccount{Config: map[string]any{
|
||||
"base_url": os.Getenv("CHANNEL_WEIXIN_ILINK_BASE_URL"),
|
||||
"cdn_base_url": os.Getenv("CHANNEL_WEIXIN_ILINK_CDN_BASE_URL"),
|
||||
"bot_token": os.Getenv("CHANNEL_WEIXIN_ILINK_BOT_TOKEN"),
|
||||
"bot_id": os.Getenv("CHANNEL_WEIXIN_ILINK_BOT_ID"),
|
||||
"owner_id": os.Getenv("CHANNEL_WEIXIN_ILINK_OWNER_ID"),
|
||||
}})
|
||||
if tok := os.Getenv("CHANNEL_WEIXIN_ILINK_BOT_TOKEN"); tok != "" {
|
||||
out["weixin-ilink"] = append(out["weixin-ilink"], config.ChannelAccount{Config: map[string]any{
|
||||
"base_url": os.Getenv("CHANNEL_WEIXIN_ILINK_BASE_URL"),
|
||||
"cdn_base_url": os.Getenv("CHANNEL_WEIXIN_ILINK_CDN_BASE_URL"),
|
||||
"bot_token": tok,
|
||||
"bot_id": os.Getenv("CHANNEL_WEIXIN_ILINK_BOT_ID"),
|
||||
"owner_id": os.Getenv("CHANNEL_WEIXIN_ILINK_OWNER_ID"),
|
||||
}})
|
||||
}
|
||||
if raw := os.Getenv("CHANNELS_JSON"); raw != "" {
|
||||
if accounts, err := config.ParseChannelAccounts(raw); err == nil {
|
||||
for platform, accts := range accounts {
|
||||
@@ -206,12 +217,18 @@ func buildAccountAdapter(platform string, acct config.ChannelAccount, log *slog.
|
||||
if err := adapter.DecodeConfig(acct.Config, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("decode telegram config: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(cfg.BotToken) == "" {
|
||||
return nil, fmt.Errorf("%s: missing bot token", platform)
|
||||
}
|
||||
inner = telegram.New(cfg, telegram.WithLogger(log))
|
||||
case "weixin-ilink":
|
||||
var cfg weixin_ilink.Config
|
||||
if err := adapter.DecodeConfig(acct.Config, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("decode weixin-ilink config: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(cfg.BotToken) == "" {
|
||||
return nil, fmt.Errorf("%s: missing bot token", platform)
|
||||
}
|
||||
w, err := weixin_ilink.New(cfg, nil, logger.WithSource(log, "plugins/channels/weixin_ilink"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build weixin-ilink: %w", err)
|
||||
@@ -228,6 +245,9 @@ func buildAccountAdapter(platform string, acct config.ChannelAccount, log *slog.
|
||||
if err := adapter.DecodeConfig(acct.Config, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("decode kook config: %w", err)
|
||||
}
|
||||
if strings.TrimSpace(cfg.BotToken) == "" {
|
||||
return nil, fmt.Errorf("%s: missing bot token", platform)
|
||||
}
|
||||
inner = kook.New(cfg, kook.WithLogger(log))
|
||||
case "webhook":
|
||||
var cfg webhook.Config
|
||||
|
||||
@@ -51,6 +51,58 @@ func TestBuildChannelAdapters_KookMultiAccount(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildChannelAdapters_SkipsEmptyTokenKookFromFile verifies a stale
|
||||
// channels.json with both a bare empty-token kook and a real token account builds
|
||||
// only the token account, so the bare "kook" never registers (and never wins a
|
||||
// bare-name open).
|
||||
func TestBuildChannelAdapters_SkipsEmptyTokenKookFromFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
raw := `{"kook":[
|
||||
{"id":"","config":{"base_url":"","bot_token":""}},
|
||||
{"id":"3fc8","config":{"bot_token":"real-token"}}
|
||||
]}`
|
||||
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
f := &channelFactory{path: path, fallback: channelAccountsFromEnv(), log: testLog()}
|
||||
adapters := buildChannelAdapters(f)
|
||||
if hasAdapter(adapters, "kook") {
|
||||
t.Fatalf("bare empty-token kook should not be built, got %v", names(adapters))
|
||||
}
|
||||
if !hasAdapter(adapters, "kook:3fc8") {
|
||||
t.Fatalf("expected kook:3fc8, got %v", names(adapters))
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelAccountsFromEnv_NoEmptyTokenPlaceholders verifies the env fallback
|
||||
// does not synthesize an empty-token kook/telegram/weixin account (only a real
|
||||
// token produces the account), so a bare placeholder is never registered.
|
||||
func TestChannelAccountsFromEnv_NoEmptyTokenPlaceholders(t *testing.T) {
|
||||
for _, k := range []string{"CHANNEL_KOOK_TOKEN", "CHANNEL_TELEGRAM_BOT_TOKEN", "CHANNEL_WEIXIN_ILINK_BOT_TOKEN", "CHANNELS_JSON"} {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
env := channelAccountsFromEnv()
|
||||
for _, platform := range []string{"kook", "telegram", "weixin-ilink"} {
|
||||
if _, ok := env[platform]; ok {
|
||||
t.Fatalf("empty-token %s should not be synthesized", platform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelAccountsFromEnv_KookPresentWhenTokenSet verifies a set token yields
|
||||
// the account.
|
||||
func TestChannelAccountsFromEnv_KookPresentWhenTokenSet(t *testing.T) {
|
||||
t.Setenv("CHANNEL_KOOK_TOKEN", "kook-secret")
|
||||
env := channelAccountsFromEnv()
|
||||
list, ok := env["kook"]
|
||||
if !ok || len(list) != 1 {
|
||||
t.Fatalf("expected a synthesized kook account, got %+v", env["kook"])
|
||||
}
|
||||
if list[0].Config["bot_token"] != "kook-secret" {
|
||||
t.Fatalf("bot_token = %v, want kook-secret", list[0].Config["bot_token"])
|
||||
}
|
||||
}
|
||||
|
||||
func hasAdapter(adapters []adapter.PlatformAdapter, name string) bool {
|
||||
for _, a := range adapters {
|
||||
if a.Name() == name {
|
||||
|
||||
@@ -56,6 +56,9 @@ export function ChannelsModule(): React.ReactNode {
|
||||
// Business-policy fold (blacklist/allow + rate limits) is separate from the
|
||||
// connection config — it governs admission/de-noise, not the endpoint.
|
||||
const [polOpen, setPolOpen] = useState(false);
|
||||
// Hint shown next to the Token field when an account already has a secret
|
||||
// (masked): the token input stays empty so a save does NOT overwrite it.
|
||||
const [tokenHint, setTokenHint] = useState("");
|
||||
|
||||
const toggle = (name: string, connect: boolean) => {
|
||||
setBusy(name);
|
||||
@@ -103,7 +106,12 @@ export function ChannelsModule(): React.ReactNode {
|
||||
const acct = (data ?? []).find((c) => platformOf(c.name) === platform)?.accounts?.[0];
|
||||
setCfgId(acct?.id ?? "");
|
||||
const cfg = acct?.config ?? {};
|
||||
setToken(typeof cfg.bot_token === "string" && cfg.bot_token ? "(已配置,仅掩码显示)" : "");
|
||||
// Do NOT put the masked placeholder into `token`: it would be written back as
|
||||
// the real bot_token (overwriting the stored secret with a demo string). Keep
|
||||
// token empty (save omits it → keeps the existing secret) and show a hint.
|
||||
const hasToken = typeof cfg.bot_token === "string" && cfg.bot_token && !cfg.bot_token.includes("(已配置");
|
||||
setToken("");
|
||||
setTokenHint(hasToken ? "已配置(仅掩码显示),留空则不改动;如需更换请重新输入。" : "");
|
||||
setBaseURL(typeof cfg.base_url === "string" ? cfg.base_url : "");
|
||||
const plat: Record<string, string> = {};
|
||||
for (const f of PLATFORM_FIELDS[platform] ?? []) {
|
||||
@@ -318,6 +326,7 @@ export function ChannelsModule(): React.ReactNode {
|
||||
) : null}
|
||||
</span>
|
||||
<input type="password" value={token} onChange={(e) => setToken(e.target.value)} placeholder="机器人 token" className={`${inputCls} border border-[var(--mu-accent)]/25`} />
|
||||
{tokenHint ? <span className="block text-[11px] text-[var(--mu-text-lo)]">{tokenHint}</span> : null}
|
||||
</label>
|
||||
</div>
|
||||
{PLATFORM_GUIDE[cfgPlatform].hint ? (
|
||||
|
||||
@@ -40,11 +40,14 @@ func DefaultChannelsPath(dataDir string) string {
|
||||
// present-and-valid channels.json it is authoritative; otherwise it falls back
|
||||
// to envFallback (the .env/CHANNELS_JSON channel block computed by the caller).
|
||||
// A present-but-invalid file falls back to env and returns a non-nil warning so
|
||||
// the caller can log it. The returned map is non-nil.
|
||||
// the caller can log it. The returned map is non-nil. A file-loaded account list
|
||||
// is cleaned (bare empty-token placeholders dropped) so a stale channels.json
|
||||
// never surfaces an account that cannot connect; the caller's envFallback is
|
||||
// left untouched.
|
||||
func LoadChannels(path string, envFallback map[string][]config.ChannelAccount) (map[string][]config.ChannelAccount, error) {
|
||||
accounts, readErr := loadChannelsFile(path)
|
||||
if readErr == nil {
|
||||
return accounts, nil
|
||||
return cleanAccounts(accounts), nil
|
||||
}
|
||||
var warn error
|
||||
if !os.IsNotExist(readErr) {
|
||||
@@ -80,11 +83,13 @@ func loadChannelsFile(path string) (map[string][]config.ChannelAccount, error) {
|
||||
|
||||
// SaveChannels writes accounts as a pretty-printed JSON map to path with mode
|
||||
// 0600 (the file holds bot tokens). It writes to a temp file then renames so a
|
||||
// concurrent reader never sees a partial file.
|
||||
// concurrent reader never sees a partial file. Bare empty-token placeholders are
|
||||
// dropped before writing, so an unusable account never lands in the file.
|
||||
func SaveChannels(path string, accounts map[string][]config.ChannelAccount) error {
|
||||
if accounts == nil {
|
||||
accounts = map[string][]config.ChannelAccount{}
|
||||
}
|
||||
accounts = cleanAccounts(accounts)
|
||||
dir := filepath.Dir(path)
|
||||
if dir != "" && dir != "." {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
@@ -204,3 +209,56 @@ func maskToken(s string) string {
|
||||
}
|
||||
return "****" + s[len(s)-4:]
|
||||
}
|
||||
|
||||
// tokenRequiringPlatforms are the adapters whose connection needs a bot token
|
||||
// (design/08 §4). An account for one of these with an empty bot_token is a bare
|
||||
// placeholder: it can never connect and must not claim the bare platform name
|
||||
// (design/16 §4.5), so it is dropped from the effective config and from
|
||||
// channels.json. Other platforms (qq-obv11, webhook) are left alone so their
|
||||
// channel list stays stable while the bridge's configured() gate decides whether
|
||||
// to dial.
|
||||
func tokenRequiringPlatforms(platform string) bool {
|
||||
switch platform {
|
||||
case "kook", "telegram", "weixin-ilink":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// cleanAccounts drops unusable bare accounts from an accounts map. For a
|
||||
// token-requiring platform any account with an empty bot_token is dropped (a
|
||||
// bare "kook"/"telegram"/"weixin-ilink" placeholder can never connect). It never
|
||||
// logs or returns a token.
|
||||
func cleanAccounts(accounts map[string][]config.ChannelAccount) map[string][]config.ChannelAccount {
|
||||
if accounts == nil {
|
||||
return accounts
|
||||
}
|
||||
out := make(map[string][]config.ChannelAccount, len(accounts))
|
||||
for platform, list := range accounts {
|
||||
if !tokenRequiringPlatforms(platform) {
|
||||
out[platform] = list
|
||||
continue
|
||||
}
|
||||
kept := make([]config.ChannelAccount, 0, len(list))
|
||||
for _, a := range list {
|
||||
if strings.TrimSpace(accountBotToken(a)) == "" {
|
||||
continue
|
||||
}
|
||||
kept = append(kept, a)
|
||||
}
|
||||
if len(kept) > 0 {
|
||||
out[platform] = kept
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// accountBotToken returns the account's bot_token value (the credential the
|
||||
// token-requiring platforms need). It never logs the token.
|
||||
func accountBotToken(a config.ChannelAccount) string {
|
||||
if s, ok := a.Config["bot_token"].(string); ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -90,6 +90,72 @@ func TestLoadChannelsEmptyMapFallsBackWithWarn(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadChannelsCleansEmptyTokenKook(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
// A stale channels.json holding a bare empty-token kook plus a real account.
|
||||
raw := `{"kook":[
|
||||
{"id":"","config":{"base_url":"","bot_token":""}},
|
||||
{"id":"3fc8ef9434bddfbbe4f4daa8da21c093","config":{"bot_token":"real-token"}}
|
||||
]}`
|
||||
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
accounts, warn := LoadChannels(path, nil)
|
||||
if warn != nil {
|
||||
t.Fatalf("warn = %v, want nil", warn)
|
||||
}
|
||||
kook := accounts["kook"]
|
||||
if len(kook) != 1 {
|
||||
t.Fatalf("kook = %+v, want only the token-bearing account", kook)
|
||||
}
|
||||
if kook[0].ID != "3fc8ef9434bddfbbe4f4daa8da21c093" || kook[0].Config["bot_token"] != "real-token" {
|
||||
t.Fatalf("kook = %+v, want the token-bearing account", kook[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadChannelsKeepsNonTokenPlatforms(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
// qq-obv11/webhook carry no bot token and must be preserved.
|
||||
raw := `{"qq-obv11":[{"id":"","config":{"mode":"server"}}],"webhook":[{"id":"","config":{"endpointPath":""}}]}`
|
||||
if err := os.WriteFile(path, []byte(raw), 0o600); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
accounts, warn := LoadChannels(path, nil)
|
||||
if warn != nil {
|
||||
t.Fatalf("warn = %v", warn)
|
||||
}
|
||||
if len(accounts["qq-obv11"]) != 1 || len(accounts["webhook"]) != 1 {
|
||||
t.Fatalf("non-token platforms should be preserved: %+v", accounts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveChannelsDropsEmptyTokenBareAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
accts := map[string][]config.ChannelAccount{
|
||||
"kook": {
|
||||
{ID: "", Config: map[string]any{"bot_token": ""}},
|
||||
{ID: "a", Config: map[string]any{"bot_token": "tk"}},
|
||||
},
|
||||
"webhook": {{ID: "", Config: map[string]any{"endpointPath": ""}}},
|
||||
}
|
||||
if err := SaveChannels(path, accts); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
got, warn := LoadChannels(path, nil)
|
||||
if warn != nil {
|
||||
t.Fatalf("load warn = %v", warn)
|
||||
}
|
||||
if len(got["kook"]) != 1 || got["kook"][0].ID != "a" {
|
||||
t.Fatalf("kook = %+v, want only the token-bearing account", got["kook"])
|
||||
}
|
||||
if _, ok := got["webhook"]; !ok || len(got["webhook"]) != 1 {
|
||||
t.Fatalf("webhook should be preserved untouched: %+v", got["webhook"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveChannelsRoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
path := filepath.Join(t.TempDir(), "channels.json")
|
||||
|
||||
@@ -283,15 +283,30 @@ func (s *Server) channelName(r *http.Request, w http.ResponseWriter, op string)
|
||||
return req.Name, true
|
||||
}
|
||||
|
||||
// findChannel returns the registered channel adapter named name, if any.
|
||||
// findChannel returns the registered channel adapter named name, if any. An exact
|
||||
// adapter name ("kook:conf0") wins; a bare platform name ("kook") that is not an
|
||||
// exact name still resolves to that platform's first registered account, so the
|
||||
// panel/CLI "connect kook" reaches the token-bearing account even when it is named
|
||||
// "kook:conf0" (the empty-token bare placeholder is never registered).
|
||||
func findChannel(caps CapabilityStore, name string) (sdk.Channel, bool) {
|
||||
if caps == nil {
|
||||
return nil, false
|
||||
}
|
||||
var first sdk.Channel
|
||||
for _, cap := range caps.List(capreg.KindChannel) {
|
||||
if ch, ok := cap.Value.(sdk.Channel); ok && ch.Name() == name {
|
||||
ch, ok := cap.Value.(sdk.Channel)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ch.Name() == name {
|
||||
return ch, true
|
||||
}
|
||||
if first == nil && channelBasePlatform(ch.Name()) == name {
|
||||
first = ch
|
||||
}
|
||||
}
|
||||
if first != nil {
|
||||
return first, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
capreg "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||||
)
|
||||
|
||||
// testChannel is a bare sdk.Channel used to exercise findChannel resolution.
|
||||
type testChannel struct{ name string }
|
||||
|
||||
func (t testChannel) Name() string { return t.name }
|
||||
func (t testChannel) Connect(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
func (t testChannel) Disconnect(context.Context) error { return nil }
|
||||
func (t testChannel) IsConnected() bool { return false }
|
||||
func (t testChannel) HealthCheck(context.Context) error { return nil }
|
||||
|
||||
// TestFindChannelExactMatch prefers an exact adapter name over a bare-name match.
|
||||
func TestFindChannelExactMatch(t *testing.T) {
|
||||
reg := capreg.New()
|
||||
_ = reg.Register(capreg.Capability{Name: "kook:conf0", Kind: capreg.KindChannel, Value: testChannel{name: "kook:conf0"}})
|
||||
_ = reg.Register(capreg.Capability{Name: "telegram:conf0", Kind: capreg.KindChannel, Value: testChannel{name: "telegram:conf0"}})
|
||||
ch, ok := findChannel(reg, "kook:conf0")
|
||||
if !ok || ch.Name() != "kook:conf0" {
|
||||
t.Fatalf("exact kook:conf0 not found, got %v/%v", ch, ok)
|
||||
}
|
||||
ch, ok = findChannel(reg, "telegram:conf0")
|
||||
if !ok || ch.Name() != "telegram:conf0" {
|
||||
t.Fatalf("exact telegram:conf0 not found, got %v/%v", ch, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindChannelBarePlatformResolvesToTokenAccount verifies a bare platform name
|
||||
// ("kook") that is not an exact adapter name resolves to that platform's first
|
||||
// registered account (the token-bearing one, since the empty-token bare account
|
||||
// is never registered).
|
||||
func TestFindChannelBarePlatformResolvesToTokenAccount(t *testing.T) {
|
||||
reg := capreg.New()
|
||||
_ = reg.Register(capreg.Capability{Name: "kook:3fc8", Kind: capreg.KindChannel, Value: testChannel{name: "kook:3fc8"}})
|
||||
ch, ok := findChannel(reg, "kook")
|
||||
if !ok || ch.Name() != "kook:3fc8" {
|
||||
t.Fatalf("bare kook should resolve to kook:3fc8, got %v/%v", ch, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindChannelUnknownOrBlank rejects a name that matches no channel.
|
||||
func TestFindChannelUnknownOrBlank(t *testing.T) {
|
||||
reg := capreg.New()
|
||||
_ = reg.Register(capreg.Capability{Name: "kook:conf0", Kind: capreg.KindChannel, Value: testChannel{name: "kook:conf0"}})
|
||||
if _, ok := findChannel(reg, "unknown"); ok {
|
||||
t.Fatal("unknown name should not be found")
|
||||
}
|
||||
if _, ok := findChannel(reg, ""); ok {
|
||||
t.Fatal("blank name should not be found")
|
||||
}
|
||||
// Skipped: non-channel capabilities must not match.
|
||||
_ = reg.Register(capreg.Capability{Name: "some-tool", Kind: capreg.KindTool, Value: testChannel{name: "some-tool"}})
|
||||
if _, ok := findChannel(reg, "some-tool"); ok {
|
||||
t.Fatal("non-channel capability must not match findChannel")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user