diff --git a/src/infra/matrix-account-selection.test.ts b/src/infra/matrix-account-selection.test.ts new file mode 100644 index 00000000000..f649e7fde7d --- /dev/null +++ b/src/infra/matrix-account-selection.test.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { + findMatrixAccountEntry, + resolveConfiguredMatrixAccountIds, + resolveMatrixDefaultOrOnlyAccountId, +} from "./matrix-account-selection.js"; + +describe("matrix account selection", () => { + it("resolves configured account ids from non-canonical account keys", () => { + const cfg: OpenClawConfig = { + channels: { + matrix: { + accounts: { + "Team Ops": { homeserver: "https://matrix.example.org" }, + }, + }, + }, + }; + + expect(resolveConfiguredMatrixAccountIds(cfg)).toEqual(["team-ops"]); + expect(resolveMatrixDefaultOrOnlyAccountId(cfg)).toBe("team-ops"); + }); + + it("matches the default account against normalized Matrix account keys", () => { + const cfg: OpenClawConfig = { + channels: { + matrix: { + defaultAccount: "Team Ops", + accounts: { + "Ops Bot": { homeserver: "https://matrix.example.org" }, + "Team Ops": { homeserver: "https://matrix.example.org" }, + }, + }, + }, + }; + + expect(resolveMatrixDefaultOrOnlyAccountId(cfg)).toBe("team-ops"); + }); + + it("finds the raw Matrix account entry by normalized account id", () => { + const cfg: OpenClawConfig = { + channels: { + matrix: { + accounts: { + "Team Ops": { + homeserver: "https://matrix.example.org", + userId: "@ops:example.org", + }, + }, + }, + }, + }; + + expect(findMatrixAccountEntry(cfg, "team-ops")).toEqual({ + homeserver: "https://matrix.example.org", + userId: "@ops:example.org", + }); + }); +}); diff --git a/src/infra/matrix-account-selection.ts b/src/infra/matrix-account-selection.ts index 548eb6edeaa..827b500c72f 100644 --- a/src/infra/matrix-account-selection.ts +++ b/src/infra/matrix-account-selection.ts @@ -13,6 +13,30 @@ export function resolveMatrixChannelConfig(cfg: OpenClawConfig): Record | null { + const channel = resolveMatrixChannelConfig(cfg); + if (!channel) { + return null; + } + + const accounts = isRecord(channel.accounts) ? channel.accounts : null; + if (!accounts) { + return null; + } + + const normalizedAccountId = normalizeAccountId(accountId); + for (const [rawAccountId, value] of Object.entries(accounts)) { + if (normalizeAccountId(rawAccountId) === normalizedAccountId && isRecord(value)) { + return value; + } + } + + return null; +} + export function resolveConfiguredMatrixAccountIds(cfg: OpenClawConfig): string[] { const channel = resolveMatrixChannelConfig(cfg); if (!channel) { @@ -24,9 +48,9 @@ export function resolveConfiguredMatrixAccountIds(cfg: OpenClawConfig): string[] return [DEFAULT_ACCOUNT_ID]; } - const ids = Object.keys(accounts) - .map((accountId) => normalizeAccountId(accountId)) - .filter((accountId) => accountId.length > 0 && isRecord(accounts[accountId])); + const ids = Object.entries(accounts) + .filter(([, value]) => isRecord(value)) + .map(([accountId]) => normalizeAccountId(accountId)); return Array.from(new Set(ids.length > 0 ? ids : [DEFAULT_ACCOUNT_ID])).toSorted((a, b) => a.localeCompare(b), @@ -39,18 +63,17 @@ export function resolveMatrixDefaultOrOnlyAccountId(cfg: OpenClawConfig): string return DEFAULT_ACCOUNT_ID; } - const accounts = isRecord(channel.accounts) ? channel.accounts : null; const configuredDefault = normalizeOptionalAccountId( typeof channel.defaultAccount === "string" ? channel.defaultAccount : undefined, ); - if (configuredDefault && accounts && isRecord(accounts[configuredDefault])) { + const configuredAccountIds = resolveConfiguredMatrixAccountIds(cfg); + if (configuredDefault && configuredAccountIds.includes(configuredDefault)) { return configuredDefault; } - if (accounts && isRecord(accounts[DEFAULT_ACCOUNT_ID])) { + if (configuredAccountIds.includes(DEFAULT_ACCOUNT_ID)) { return DEFAULT_ACCOUNT_ID; } - const configuredAccountIds = resolveConfiguredMatrixAccountIds(cfg); if (configuredAccountIds.length === 1) { return configuredAccountIds[0] ?? DEFAULT_ACCOUNT_ID; } diff --git a/src/infra/matrix-migration-config.ts b/src/infra/matrix-migration-config.ts index f5f3d469987..a95a6e65295 100644 --- a/src/infra/matrix-migration-config.ts +++ b/src/infra/matrix-migration-config.ts @@ -3,7 +3,7 @@ import os from "node:os"; import type { OpenClawConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import { normalizeAccountId } from "../routing/session-key.js"; -import { resolveMatrixChannelConfig } from "./matrix-account-selection.js"; +import { findMatrixAccountEntry, resolveMatrixChannelConfig } from "./matrix-account-selection.js"; import { resolveMatrixCredentialsPath } from "./matrix-storage-paths.js"; export type MatrixStoredCredentials = { @@ -13,10 +13,6 @@ export type MatrixStoredCredentials = { deviceId?: string; }; -function isRecord(value: unknown): value is Record { - return Boolean(value) && typeof value === "object" && !Array.isArray(value); -} - function clean(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } @@ -60,12 +56,7 @@ function resolveMatrixAccountConfigEntry( cfg: OpenClawConfig, accountId: string, ): Record | null { - const channel = resolveMatrixChannelConfig(cfg); - if (!channel) { - return null; - } - const accounts = isRecord(channel.accounts) ? channel.accounts : null; - return accounts && isRecord(accounts[accountId]) ? accounts[accountId] : null; + return findMatrixAccountEntry(cfg, accountId); } export function resolveMatrixMigrationConfigFields(params: {