mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 06:11:24 +00:00
fix(matrix): only count ready accounts
This commit is contained in:
86
extensions/matrix/src/account-selection.test.ts
Normal file
86
extensions/matrix/src/account-selection.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user