fix(matrix): only count ready accounts

This commit is contained in:
Gustavo Madeira Santana
2026-04-01 12:46:40 -04:00
parent 55812b7933
commit cd5a499f0a
3 changed files with 220 additions and 13 deletions

View File

@@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import {
requiresExplicitMatrixDefaultAccount,
resolveConfiguredMatrixAccountIds,
resolveMatrixDefaultOrOnlyAccountId,
} from "./account-selection.js";
import type { CoreConfig } from "./types.js";
describe("Matrix account selection topology", () => {
it("includes a top-level default account when its auth is actually complete", () => {
const cfg = {
channels: {
matrix: {
accessToken: "default-token",
accounts: {
ops: {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
},
},
},
},
} as CoreConfig;
const env = {
MATRIX_HOMESERVER: "https://matrix.example.org",
} as NodeJS.ProcessEnv;
expect(resolveConfiguredMatrixAccountIds(cfg, env)).toEqual(["default", "ops"]);
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, env)).toBe("default");
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(true);
});
it("does not materialize a top-level default account from partial shared auth fields", () => {
const cfg = {
channels: {
matrix: {
accessToken: "shared-token",
accounts: {
ops: {
homeserver: "https://matrix.example.org",
accessToken: "ops-token",
},
},
},
},
} as CoreConfig;
expect(resolveConfiguredMatrixAccountIds(cfg, {} as NodeJS.ProcessEnv)).toEqual(["ops"]);
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, {} as NodeJS.ProcessEnv)).toBe("ops");
expect(requiresExplicitMatrixDefaultAccount(cfg, {} as NodeJS.ProcessEnv)).toBe(false);
});
it("does not materialize a default env account from partial global auth fields", () => {
const cfg = {
channels: {
matrix: {},
},
} as CoreConfig;
const env = {
MATRIX_ACCESS_TOKEN: "shared-token",
MATRIX_OPS_HOMESERVER: "https://matrix.example.org",
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
} as NodeJS.ProcessEnv;
expect(resolveConfiguredMatrixAccountIds(cfg, env)).toEqual(["ops"]);
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, env)).toBe("ops");
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(false);
});
it("counts env-backed named accounts when shared homeserver comes from channel config", () => {
const cfg = {
channels: {
matrix: {
homeserver: "https://matrix.example.org",
},
},
} as CoreConfig;
const env = {
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
} as NodeJS.ProcessEnv;
expect(resolveConfiguredMatrixAccountIds(cfg, env)).toEqual(["ops"]);
expect(resolveMatrixDefaultOrOnlyAccountId(cfg, env)).toBe("ops");
expect(requiresExplicitMatrixDefaultAccount(cfg, env)).toBe(false);
});
});

View File

@@ -11,23 +11,106 @@ import {
} from "openclaw/plugin-sdk/account-id";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { hasConfiguredSecretInput } from "openclaw/plugin-sdk/secret-input";
import { listMatrixEnvAccountIds } from "./env-vars.js";
import {
resolveMatrixAccountStringValues,
type MatrixResolvedStringField,
} from "./auth-precedence.js";
import { getMatrixScopedEnvVarNames, listMatrixEnvAccountIds } from "./env-vars.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function hasConfiguredDefaultMatrixAccountSource(channel: Record<string, unknown> | null): boolean {
if (!channel) {
return false;
type MatrixTopologyStringSources = Partial<Record<MatrixResolvedStringField, string>>;
function readConfiguredMatrixString(value: unknown): string {
return typeof value === "string" ? value.trim() : "";
}
function readConfiguredMatrixSecretSource(value: unknown): string {
return hasConfiguredSecretInput(value) ? "configured" : "";
}
function resolveMatrixChannelStringSources(
entry: Record<string, unknown> | null,
): MatrixTopologyStringSources {
if (!entry) {
return {};
}
// Top-level Matrix config can provide shared defaults for named accounts. Only
// count fields that are actually default-account-specific auth/identity input.
return (
typeof channel.userId === "string" ||
hasConfiguredSecretInput(channel.accessToken) ||
hasConfiguredSecretInput(channel.password)
);
return {
homeserver: readConfiguredMatrixString(entry.homeserver),
userId: readConfiguredMatrixString(entry.userId),
accessToken: readConfiguredMatrixSecretSource(entry.accessToken),
password: readConfiguredMatrixSecretSource(entry.password),
deviceId: readConfiguredMatrixString(entry.deviceId),
deviceName: readConfiguredMatrixString(entry.deviceName),
};
}
function readEnvMatrixString(env: NodeJS.ProcessEnv, key: string): string {
const value = env[key];
return typeof value === "string" ? value.trim() : "";
}
function resolveScopedMatrixEnvStringSources(
accountId: string,
env: NodeJS.ProcessEnv,
): MatrixTopologyStringSources {
const keys = getMatrixScopedEnvVarNames(accountId);
return {
homeserver: readEnvMatrixString(env, keys.homeserver),
userId: readEnvMatrixString(env, keys.userId),
accessToken: readEnvMatrixString(env, keys.accessToken),
password: readEnvMatrixString(env, keys.password),
deviceId: readEnvMatrixString(env, keys.deviceId),
deviceName: readEnvMatrixString(env, keys.deviceName),
};
}
function resolveGlobalMatrixEnvStringSources(env: NodeJS.ProcessEnv): MatrixTopologyStringSources {
return {
homeserver: readEnvMatrixString(env, "MATRIX_HOMESERVER"),
userId: readEnvMatrixString(env, "MATRIX_USER_ID"),
accessToken: readEnvMatrixString(env, "MATRIX_ACCESS_TOKEN"),
password: readEnvMatrixString(env, "MATRIX_PASSWORD"),
deviceId: readEnvMatrixString(env, "MATRIX_DEVICE_ID"),
deviceName: readEnvMatrixString(env, "MATRIX_DEVICE_NAME"),
};
}
function hasReadyResolvedMatrixAuth(values: {
homeserver: string;
userId: string;
accessToken: string;
password: string;
}): boolean {
return Boolean(values.homeserver && (values.accessToken || (values.userId && values.password)));
}
function hasReadyEffectiveMatrixAccountSource(params: {
channel: Record<string, unknown> | null;
accountId: string;
env: NodeJS.ProcessEnv;
}): boolean {
const normalizedAccountId = normalizeAccountId(params.accountId);
const resolved = resolveMatrixAccountStringValues({
accountId: normalizedAccountId,
scopedEnv: resolveScopedMatrixEnvStringSources(normalizedAccountId, params.env),
channel: resolveMatrixChannelStringSources(params.channel),
globalEnv: resolveGlobalMatrixEnvStringSources(params.env),
});
return hasReadyResolvedMatrixAuth(resolved);
}
function hasConfiguredDefaultMatrixAccountSource(params: {
channel: Record<string, unknown> | null;
env: NodeJS.ProcessEnv;
}): boolean {
return hasReadyEffectiveMatrixAccountSource({
channel: params.channel,
accountId: DEFAULT_ACCOUNT_ID,
env: params.env,
});
}
export function resolveMatrixChannelConfig(cfg: OpenClawConfig): Record<string, unknown> | null {
@@ -60,12 +143,15 @@ export function resolveConfiguredMatrixAccountIds(
accounts: channel && isRecord(channel.accounts) ? channel.accounts : undefined,
normalizeAccountId,
});
if (hasConfiguredDefaultMatrixAccountSource(channel)) {
if (hasConfiguredDefaultMatrixAccountSource({ channel, env })) {
configuredAccountIds.push(DEFAULT_ACCOUNT_ID);
}
const readyEnvAccountIds = listMatrixEnvAccountIds(env).filter((accountId) =>
hasReadyEffectiveMatrixAccountSource({ channel, accountId, env }),
);
return listCombinedAccountIds({
configuredAccountIds,
additionalAccountIds: listMatrixEnvAccountIds(env),
additionalAccountIds: readyEnvAccountIds,
fallbackAccountIdWhenEmpty: channel ? DEFAULT_ACCOUNT_ID : undefined,
});
}

View File

@@ -397,6 +397,25 @@ describe("resolveMatrixConfig", () => {
expect(resolveMatrixAuthContext({ cfg, env: {} as NodeJS.ProcessEnv }).accountId).toBe("ops");
});
it("does not materialize a default account from partial top-level auth defaults", () => {
const cfg = {
channels: {
matrix: {
accessToken: "shared-token",
accounts: {
ops: {
homeserver: "https://matrix.ops.example.org",
accessToken: "ops-token",
},
},
},
},
} as CoreConfig;
expect(resolveImplicitMatrixAccountId(cfg, {} as NodeJS.ProcessEnv)).toBe("ops");
expect(resolveMatrixAuthContext({ cfg, env: {} as NodeJS.ProcessEnv }).accountId).toBe("ops");
});
it("honors injected env when implicit Matrix account selection becomes ambiguous", () => {
const cfg = {
channels: {
@@ -416,6 +435,22 @@ describe("resolveMatrixConfig", () => {
);
});
it("does not materialize a default env account from partial global auth fields", () => {
const cfg = {
channels: {
matrix: {},
},
} as CoreConfig;
const env = {
MATRIX_ACCESS_TOKEN: "shared-token",
MATRIX_OPS_HOMESERVER: "https://matrix.example.org",
MATRIX_OPS_ACCESS_TOKEN: "ops-token",
} as NodeJS.ProcessEnv;
expect(resolveImplicitMatrixAccountId(cfg, env)).toBe("ops");
expect(resolveMatrixAuthContext({ cfg, env }).accountId).toBe("ops");
});
it("rejects explicit non-default account ids that are neither configured nor scoped in env", () => {
const cfg = {
channels: {