fix(matrix): pass deviceId through health probe to prevent storage-meta overwrite (#61317) (#61581)

Merged via squash.

Prepared head SHA: b0495dc6ca
Co-authored-by: MoerAI <26067127+MoerAI@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
ToToKr
2026-04-06 13:42:22 +09:00
committed by GitHub
parent 728aee277f
commit d4c443bc1e
7 changed files with 199 additions and 27 deletions

View File

@@ -49,6 +49,7 @@ describe("matrix account path propagation", () => {
homeserver: "https://matrix.example.org",
userId: "@poe:example.org",
accessToken: "poe-token",
deviceId: "POEDEVICE",
});
});
@@ -66,7 +67,7 @@ describe("matrix account path propagation", () => {
);
});
it("forwards accountId to matrix probes", async () => {
it("forwards accountId and deviceId to matrix probes", async () => {
await matrixPlugin.status!.probeAccount?.({
cfg: {} as never,
timeoutMs: 500,
@@ -83,6 +84,7 @@ describe("matrix account path propagation", () => {
homeserver: "https://matrix.example.org",
accessToken: "poe-token",
userId: "@poe:example.org",
deviceId: "POEDEVICE",
timeoutMs: 500,
accountId: "poe",
});

View File

@@ -498,6 +498,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount, MatrixProbe> =
homeserver: auth.homeserver,
accessToken: auth.accessToken,
userId: auth.userId,
deviceId: auth.deviceId,
timeoutMs,
accountId: account.accountId,
allowPrivateNetwork: auth.allowPrivateNetwork,

View File

@@ -0,0 +1,115 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const ensureMatrixSdkLoggingConfiguredMock = vi.hoisted(() => vi.fn());
const resolveValidatedMatrixHomeserverUrlMock = vi.hoisted(() => vi.fn());
const maybeMigrateLegacyStorageMock = vi.hoisted(() => vi.fn(async () => undefined));
const resolveMatrixStoragePathsMock = vi.hoisted(() => vi.fn());
const writeStorageMetaMock = vi.hoisted(() => vi.fn());
const MatrixClientMock = vi.hoisted(() => vi.fn());
vi.mock("./logging.js", () => ({
ensureMatrixSdkLoggingConfigured: ensureMatrixSdkLoggingConfiguredMock,
}));
vi.mock("./config.js", () => ({
resolveValidatedMatrixHomeserverUrl: resolveValidatedMatrixHomeserverUrlMock,
}));
vi.mock("./storage.js", () => ({
maybeMigrateLegacyStorage: maybeMigrateLegacyStorageMock,
resolveMatrixStoragePaths: resolveMatrixStoragePathsMock,
writeStorageMeta: writeStorageMetaMock,
}));
vi.mock("../sdk.js", () => ({
MatrixClient: MatrixClientMock,
}));
let createMatrixClient: typeof import("./create-client.js").createMatrixClient;
describe("createMatrixClient", () => {
const storagePaths = {
rootDir: "/tmp/openclaw-matrix-create-client-test",
storagePath: "/tmp/openclaw-matrix-create-client-test/storage.json",
recoveryKeyPath: "/tmp/openclaw-matrix-create-client-test/recovery.key",
idbSnapshotPath: "/tmp/openclaw-matrix-create-client-test/idb.snapshot",
metaPath: "/tmp/openclaw-matrix-create-client-test/storage-meta.json",
accountKey: "default",
tokenHash: "token-hash",
};
beforeAll(async () => {
({ createMatrixClient } = await import("./create-client.js"));
});
beforeEach(() => {
vi.clearAllMocks();
ensureMatrixSdkLoggingConfiguredMock.mockReturnValue(undefined);
resolveValidatedMatrixHomeserverUrlMock.mockResolvedValue("https://matrix.example.org");
resolveMatrixStoragePathsMock.mockReturnValue(storagePaths);
MatrixClientMock.mockImplementation(function MockMatrixClient() {
return {
stop: vi.fn(),
};
});
});
it("persists storage metadata by default", async () => {
await createMatrixClient({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "tok",
});
expect(writeStorageMetaMock).toHaveBeenCalledWith({
storagePaths,
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accountId: undefined,
deviceId: undefined,
});
expect(resolveMatrixStoragePathsMock).toHaveBeenCalledTimes(1);
expect(MatrixClientMock).toHaveBeenCalledWith("https://matrix.example.org", "tok", {
userId: "@bot:example.org",
password: undefined,
deviceId: undefined,
encryption: undefined,
localTimeoutMs: undefined,
initialSyncLimit: undefined,
storagePath: storagePaths.storagePath,
recoveryKeyPath: storagePaths.recoveryKeyPath,
idbSnapshotPath: storagePaths.idbSnapshotPath,
cryptoDatabasePrefix: "openclaw-matrix-default-token-hash",
autoBootstrapCrypto: undefined,
ssrfPolicy: undefined,
dispatcherPolicy: undefined,
});
});
it("skips persistent storage wiring when persistence is disabled", async () => {
await createMatrixClient({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "tok",
persistStorage: false,
});
expect(resolveMatrixStoragePathsMock).not.toHaveBeenCalled();
expect(writeStorageMetaMock).not.toHaveBeenCalled();
expect(MatrixClientMock).toHaveBeenCalledWith("https://matrix.example.org", "tok", {
userId: "@bot:example.org",
password: undefined,
deviceId: undefined,
encryption: undefined,
localTimeoutMs: undefined,
initialSyncLimit: undefined,
storagePath: undefined,
recoveryKeyPath: undefined,
idbSnapshotPath: undefined,
cryptoDatabasePrefix: undefined,
autoBootstrapCrypto: undefined,
ssrfPolicy: undefined,
dispatcherPolicy: undefined,
});
});
});

View File

@@ -33,6 +33,7 @@ export async function createMatrixClient(params: {
accessToken: string;
password?: string;
deviceId?: string;
persistStorage?: boolean;
encryption?: boolean;
localTimeoutMs?: number;
initialSyncLimit?: number;
@@ -45,36 +46,41 @@ export async function createMatrixClient(params: {
const { MatrixClient, ensureMatrixSdkLoggingConfigured } =
await loadMatrixCreateClientRuntimeDeps();
ensureMatrixSdkLoggingConfigured();
const env = process.env;
const homeserver = await resolveValidatedMatrixHomeserverUrl(params.homeserver, {
dangerouslyAllowPrivateNetwork: params.allowPrivateNetwork,
});
const userId = params.userId?.trim() || "unknown";
const matrixClientUserId = params.userId?.trim() || undefined;
const persistStorage = params.persistStorage !== false;
const storagePaths = persistStorage
? resolveMatrixStoragePaths({
homeserver,
userId,
accessToken: params.accessToken,
accountId: params.accountId,
deviceId: params.deviceId,
env: process.env,
})
: null;
const storagePaths = resolveMatrixStoragePaths({
homeserver,
userId,
accessToken: params.accessToken,
accountId: params.accountId,
deviceId: params.deviceId,
env,
});
await maybeMigrateLegacyStorage({
storagePaths,
env,
});
fs.mkdirSync(storagePaths.rootDir, { recursive: true });
if (storagePaths) {
await maybeMigrateLegacyStorage({
storagePaths,
env: process.env,
});
fs.mkdirSync(storagePaths.rootDir, { recursive: true });
writeStorageMeta({
storagePaths,
homeserver,
userId,
accountId: params.accountId,
deviceId: params.deviceId,
});
}
writeStorageMeta({
storagePaths,
homeserver,
userId,
accountId: params.accountId,
deviceId: params.deviceId,
});
const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`;
const cryptoDatabasePrefix = storagePaths
? `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`
: undefined;
return new MatrixClient(homeserver, params.accessToken, {
userId: matrixClientUserId,
@@ -83,9 +89,9 @@ export async function createMatrixClient(params: {
encryption: params.encryption,
localTimeoutMs: params.localTimeoutMs,
initialSyncLimit: params.initialSyncLimit,
storagePath: storagePaths.storagePath,
recoveryKeyPath: storagePaths.recoveryKeyPath,
idbSnapshotPath: storagePaths.idbSnapshotPath,
storagePath: storagePaths?.storagePath,
recoveryKeyPath: storagePaths?.recoveryKeyPath,
idbSnapshotPath: storagePaths?.idbSnapshotPath,
cryptoDatabasePrefix,
autoBootstrapCrypto: params.autoBootstrapCrypto,
ssrfPolicy: params.ssrfPolicy,

View File

@@ -34,6 +34,7 @@ describe("probeMatrix", () => {
homeserver: "https://matrix.example.org",
userId: undefined,
accessToken: "tok",
persistStorage: false,
localTimeoutMs: 1234,
});
});
@@ -50,6 +51,7 @@ describe("probeMatrix", () => {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "tok",
persistStorage: false,
localTimeoutMs: 500,
});
});
@@ -67,6 +69,7 @@ describe("probeMatrix", () => {
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "tok",
persistStorage: false,
localTimeoutMs: 500,
accountId: "ops",
});
@@ -87,6 +90,7 @@ describe("probeMatrix", () => {
homeserver: "https://matrix.example.org",
userId: undefined,
accessToken: "tok",
persistStorage: false,
localTimeoutMs: 500,
dispatcherPolicy: {
mode: "explicit-proxy",
@@ -95,6 +99,44 @@ describe("probeMatrix", () => {
});
});
it("passes deviceId through to client creation (#61317)", async () => {
await probeMatrix({
homeserver: "https://matrix.example.org",
accessToken: "tok",
userId: "@bot:example.org",
deviceId: "ABCDEF",
timeoutMs: 500,
accountId: "ops",
});
expect(createMatrixClientMock).toHaveBeenCalledWith({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "tok",
deviceId: "ABCDEF",
persistStorage: false,
localTimeoutMs: 500,
accountId: "ops",
});
});
it("omits deviceId when not provided", async () => {
await probeMatrix({
homeserver: "https://matrix.example.org",
accessToken: "tok",
timeoutMs: 500,
});
expect(createMatrixClientMock).toHaveBeenCalledWith({
homeserver: "https://matrix.example.org",
userId: undefined,
accessToken: "tok",
deviceId: undefined,
persistStorage: false,
localTimeoutMs: 500,
});
});
it("returns client validation errors for insecure public http homeservers", async () => {
createMatrixClientMock.mockRejectedValue(
new Error("Matrix homeserver must use https:// unless it targets a private or loopback host"),

View File

@@ -24,6 +24,7 @@ export async function probeMatrix(params: {
homeserver: string;
accessToken: string;
userId?: string;
deviceId?: string;
timeoutMs: number;
accountId?: string | null;
allowPrivateNetwork?: boolean;
@@ -65,6 +66,8 @@ export async function probeMatrix(params: {
homeserver: params.homeserver,
userId: inputUserId,
accessToken: params.accessToken,
deviceId: params.deviceId,
persistStorage: false,
localTimeoutMs: params.timeoutMs,
accountId: params.accountId,
allowPrivateNetwork: params.allowPrivateNetwork,