Merge f086588599e86f8b5441e41e71adf409aeabb82a into 6b4c24c2e55b5b4013277bd799525086f6a0c40f

This commit is contained in:
Edward Abrams 2026-03-20 21:44:14 -07:00 committed by GitHub
commit 166e7cd446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 183 additions and 0 deletions

View File

@ -81,6 +81,129 @@ describe("createAccountListHelpers", () => {
it("returns sorted ids", () => {
expect(listAccountIds(cfg({ z: {}, a: {}, m: {} }))).toEqual(["a", "m", "z"]);
});
it("includes default when ALL named accounts have their own tokens", () => {
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
appToken: "xapp-base",
accounts: { tank: { botToken: "xoxb-tank", appToken: "xapp-tank" } },
},
},
} as unknown as OpenClawConfig;
expect(listAccountIds(config)).toEqual(["default", "tank"]);
});
it("includes default when normalized IDs differ from raw config keys", () => {
const normalizedHelpers = createAccountListHelpers("testchannel", {
normalizeAccountId: (id: string) => id.toLowerCase().replace(/\s+/g, "-"),
});
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
accounts: {
"Router D": { botToken: "xoxb-router-d" },
},
},
},
} as unknown as OpenClawConfig;
// "Router D" normalizes to "router-d" — should still detect own token
expect(normalizedHelpers.listAccountIds(config)).toEqual(["default", "router-d"]);
});
it("does NOT inject default in mixed configs (some accounts inherit base tokens)", () => {
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
accounts: { teamA: { botToken: "xoxb-own" }, teamB: {} },
},
},
} as unknown as OpenClawConfig;
// teamB inherits base tokens — injecting default would duplicate teamB
expect(listAccountIds(config)).toEqual(["teamA", "teamB"]);
});
it("does NOT inject default when account partially overrides (only appToken)", () => {
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
appToken: "xapp-base",
accounts: { teamA: { appToken: "xapp-own" } },
},
},
} as unknown as OpenClawConfig;
// teamA overrides appToken but inherits botToken — still same bot identity
expect(listAccountIds(config)).toEqual(["teamA"]);
});
it("does NOT inject default when base token is whitespace-only", () => {
const config = {
channels: {
testchannel: {
botToken: " ",
accounts: { teamA: {} },
},
},
} as unknown as OpenClawConfig;
expect(listAccountIds(config)).toEqual(["teamA"]);
});
it("does NOT inject default when named accounts inherit base tokens (avoids duplicates)", () => {
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
appToken: "xapp-base",
accounts: { tank: {} },
},
},
} as unknown as OpenClawConfig;
expect(listAccountIds(config)).toEqual(["tank"]);
});
it("does NOT inject default when Discord named accounts inherit base token", () => {
const config = {
channels: {
testchannel: {
token: "discord-bot-token",
accounts: { teamA: {} },
},
},
} as unknown as OpenClawConfig;
expect(listAccountIds(config)).toEqual(["teamA"]);
});
it("does not duplicate default when already in accounts (case-insensitive)", () => {
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
accounts: { Default: {}, tank: {} },
},
},
} as unknown as OpenClawConfig;
expect(listAccountIds(config)).toEqual(["Default", "tank"]);
});
it("does not duplicate default when already in accounts", () => {
const config = {
channels: {
testchannel: {
botToken: "xoxb-base",
accounts: { default: {}, tank: {} },
},
},
} as unknown as OpenClawConfig;
expect(listAccountIds(config)).toEqual(["default", "tank"]);
});
it("does not include default when base has no tokens", () => {
expect(listAccountIds(cfg({ tank: {} }))).toEqual(["tank"]);
});
});
describe("resolveDefaultAccountId", () => {

View File

@ -43,6 +43,66 @@ export function createAccountListHelpers(
if (ids.length === 0) {
return [DEFAULT_ACCOUNT_ID];
}
// Check whether any existing named account already normalizes to "default".
const normalizedIds = ids.map(normalizeAccountId);
if (normalizedIds.includes(DEFAULT_ACCOUNT_ID)) {
return ids.toSorted((a, b) => a.localeCompare(b));
}
// If the base channel config has its own tokens (botToken/appToken/token),
// only inject a default account when at least one named account carries its
// own per-account auth. When every named account inherits the base tokens
// (i.e. has no per-account botToken/appToken/token override), injecting
// default would start a duplicate provider on the same credentials.
const channel = cfg.channels?.[channelKey];
const base = channel as Record<string, unknown> | undefined;
const isTruthy = (v: unknown): boolean =>
typeof v === "string" ? v.trim().length > 0 : Boolean(v);
// Identify which token fields the base config provides.
const baseTokenFields = (["botToken", "appToken", "token"] as const).filter((f) =>
isTruthy(base?.[f]),
);
if (baseTokenFields.length > 0) {
const accounts = (base?.accounts ?? {}) as Record<string, Record<string, unknown>>;
// Use the caller-provided normalizer for the reverse map so it matches
// the IDs returned by listConfiguredAccountIds().
const normalizeId = options?.normalizeAccountId ?? normalizeAccountId;
// Build reverse map from normalized ID → raw config key so we can look
// up the account object even when normalizeAccountId transforms the keys.
const rawKeys = Object.keys(accounts);
const normalizedToRaw = new Map<string, string>();
for (const key of rawKeys) {
const normalized = normalizeId(key);
if (normalized && !normalizedToRaw.has(normalized)) {
normalizedToRaw.set(normalized, key);
}
}
// Only consider enabled accounts — disabled accounts are skipped at
// startup and must not prevent default injection for enabled ones.
const enabledIds = ids.filter((id) => {
const rawKey = normalizedToRaw.get(id) ?? id;
const acct = accounts[rawKey];
return !acct || acct["enabled"] !== false;
});
const everyAccountHasOwnTokens =
enabledIds.length > 0 &&
enabledIds.every((id) => {
const rawKey = normalizedToRaw.get(id) ?? id;
const acct = accounts[rawKey];
if (!acct) {
return false;
}
// An account is only independent if it overrides *every* base token
// field. Partial overrides still inherit the remaining base tokens.
return baseTokenFields.every((f) => isTruthy(acct[f]));
});
if (everyAccountHasOwnTokens) {
// Every named account has distinct credentials, so default won't
// collide with any of them — safe to inject.
return [DEFAULT_ACCOUNT_ID, ...ids].toSorted((a, b) => a.localeCompare(b));
}
// At least one named account inherits base tokens — injecting default
// would duplicate that account's credentials.
}
return ids.toSorted((a, b) => a.localeCompare(b));
}