diff --git a/src/infra/matrix-migration-config.test.ts b/src/infra/matrix-migration-config.test.ts index 3bdae56ed15..624dfd8beec 100644 --- a/src/infra/matrix-migration-config.test.ts +++ b/src/infra/matrix-migration-config.test.ts @@ -11,6 +11,48 @@ function writeFile(filePath: string, value: string) { } describe("resolveMatrixMigrationAccountTarget", () => { + it("reuses stored user identity for token-only configs when the access token matches", async () => { + await withTempHome(async (home) => { + const stateDir = path.join(home, ".openclaw"); + writeFile( + path.join(stateDir, "credentials", "matrix", "credentials-ops.json"), + JSON.stringify( + { + homeserver: "https://matrix.example.org", + userId: "@ops-bot:example.org", + accessToken: "tok-ops", + deviceId: "DEVICE-OPS", + }, + null, + 2, + ), + ); + + const cfg: OpenClawConfig = { + channels: { + matrix: { + accounts: { + ops: { + homeserver: "https://matrix.example.org", + accessToken: "tok-ops", + }, + }, + }, + }, + }; + + const target = resolveMatrixMigrationAccountTarget({ + cfg, + env: process.env, + accountId: "ops", + }); + + expect(target).not.toBeNull(); + expect(target?.userId).toBe("@ops-bot:example.org"); + expect(target?.storedDeviceId).toBe("DEVICE-OPS"); + }); + }); + it("ignores stored device IDs from stale cached Matrix credentials", async () => { await withTempHome(async (home) => { const stateDir = path.join(home, ".openclaw"); @@ -54,4 +96,44 @@ describe("resolveMatrixMigrationAccountTarget", () => { expect(target?.storedDeviceId).toBeNull(); }); }); + + it("does not trust stale stored creds on the same homeserver when the token changes", async () => { + await withTempHome(async (home) => { + const stateDir = path.join(home, ".openclaw"); + writeFile( + path.join(stateDir, "credentials", "matrix", "credentials-ops.json"), + JSON.stringify( + { + homeserver: "https://matrix.example.org", + userId: "@old-bot:example.org", + accessToken: "tok-old", + deviceId: "DEVICE-OLD", + }, + null, + 2, + ), + ); + + const cfg: OpenClawConfig = { + channels: { + matrix: { + accounts: { + ops: { + homeserver: "https://matrix.example.org", + accessToken: "tok-new", + }, + }, + }, + }, + }; + + const target = resolveMatrixMigrationAccountTarget({ + cfg, + env: process.env, + accountId: "ops", + }); + + expect(target).toBeNull(); + }); + }); }); diff --git a/src/infra/matrix-migration-config.ts b/src/infra/matrix-migration-config.ts index 2b434d39f2c..dc2ad7b1da2 100644 --- a/src/infra/matrix-migration-config.ts +++ b/src/infra/matrix-migration-config.ts @@ -165,13 +165,17 @@ export function credentialsMatchResolvedIdentity( identity: { homeserver: string; userId: string; + accessToken: string; }, ): stored is MatrixStoredCredentials { if (!stored || !identity.homeserver) { return false; } if (!identity.userId) { - return stored.homeserver === identity.homeserver; + if (!identity.accessToken) { + return false; + } + return stored.homeserver === identity.homeserver && stored.accessToken === identity.accessToken; } return stored.homeserver === identity.homeserver && stored.userId === identity.userId; } @@ -186,6 +190,7 @@ export function resolveMatrixMigrationAccountTarget(params: { const matchingStored = credentialsMatchResolvedIdentity(stored, { homeserver: resolved.homeserver, userId: resolved.userId, + accessToken: resolved.accessToken, }) ? stored : null;