fix(matrix): avoid device cleanup sync races

This commit is contained in:
Gustavo Madeira Santana
2026-04-26 22:33:39 -04:00
parent 99159f89da
commit 3b74b913e3
4 changed files with 39 additions and 22 deletions

View File

@@ -96,7 +96,7 @@ describe("matrix device actions", () => {
},
],
}));
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
withResolvedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({
listOwnDevices: vi.fn(async () => [
{
@@ -150,5 +150,10 @@ describe("matrix device actions", () => {
current: true,
}),
]);
expect(withResolvedActionClientMock).toHaveBeenCalledWith(
{ accountId: "poe" },
expect.any(Function),
);
expect(withStartedActionClientMock).not.toHaveBeenCalled();
});
});

View File

@@ -1,5 +1,5 @@
import { summarizeMatrixDeviceHealth } from "../device-health.js";
import { withResolvedActionClient, withStartedActionClient } from "./client.js";
import { withResolvedActionClient } from "./client.js";
import type { MatrixActionClientOpts } from "./types.js";
export async function listMatrixOwnDevices(opts: MatrixActionClientOpts = {}) {
@@ -7,7 +7,7 @@ export async function listMatrixOwnDevices(opts: MatrixActionClientOpts = {}) {
}
export async function pruneMatrixStaleGatewayDevices(opts: MatrixActionClientOpts = {}) {
return await withStartedActionClient(opts, async (client) => {
return await withResolvedActionClient(opts, async (client) => {
const devices = await client.listOwnDevices();
const health = summarizeMatrixDeviceHealth(devices);
const staleGatewayDeviceIds = health.staleOpenClawDevices.map((device) => device.deviceId);

View File

@@ -81,6 +81,19 @@ type MatrixQaDestructiveSetup = {
seededEventId: string;
};
async function cleanupMatrixQaTempDevices(
client: MatrixQaE2eeScenarioClient,
deviceIds: Array<string | null | undefined>,
): Promise<void> {
await client.stop().catch(() => undefined);
const uniqueDeviceIds = [
...new Set(deviceIds.filter((deviceId): deviceId is string => !!deviceId)),
];
if (uniqueDeviceIds.length > 0) {
await client.deleteOwnDevices(uniqueDeviceIds).catch(() => undefined);
}
}
function requireMatrixQaE2eeOutputDir(context: MatrixQaScenarioContext) {
if (!context.outputDir) {
throw new Error("Matrix E2EE destructive QA scenarios require an output directory");
@@ -668,8 +681,7 @@ export async function runMatrixQaE2eeStateLossExternalRecoveryKeyScenario(
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}
@@ -748,8 +760,7 @@ export async function runMatrixQaE2eeStateLossStoredRecoveryKeyScenario(
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}
@@ -793,8 +804,7 @@ export async function runMatrixQaE2eeStateLossNoRecoveryKeyScenario(
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}
@@ -863,8 +873,7 @@ export async function runMatrixQaE2eeStaleRecoveryKeyAfterBackupResetScenario(
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}
@@ -1026,8 +1035,7 @@ export async function runMatrixQaE2eeServerBackupDeletedLocalReuploadRestoresSce
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}
@@ -1101,8 +1109,7 @@ export async function runMatrixQaE2eeCorruptCryptoIdbSnapshotScenario(
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}
@@ -1141,6 +1148,7 @@ export async function runMatrixQaE2eeServerDeviceDeletedLocalStateIntactScenario
assertMatrixQaCliBackupRestoreSucceeded(restored.payload, "deleted-device preflight");
await setup.owner.deleteOwnDevices([device.deviceId]);
const ownerDevicesAfterDelete = await setup.owner.listOwnDevices();
await setup.owner.stop().catch(() => undefined);
const defaultStatus = await runMatrixQaCliJson<MatrixQaCliVerificationStatus>({
allowNonZero: true,
args: ["matrix", "verify", "status", "--account", "deleted-device", "--json"],
@@ -1238,6 +1246,7 @@ export async function runMatrixQaE2eeServerDeviceDeletedReloginRecoversScenario(
await setup.owner.deleteOwnDevices([deleted.device.deviceId]);
const ownerDevicesAfterDelete = await setup.owner.listOwnDevices();
await setup.owner.stop().catch(() => undefined);
const defaultStatus = await runMatrixQaCliJson<MatrixQaCliVerificationStatus>({
allowNonZero: true,
args: ["matrix", "verify", "status", "--account", "deleted-device-recovery", "--json"],
@@ -1322,12 +1331,11 @@ export async function runMatrixQaE2eeServerDeviceDeletedReloginRecoversScenario(
};
} finally {
await replacement?.cli.dispose().catch(() => undefined);
if (replacement?.device.deviceId) {
await setup.owner.deleteOwnDevices([replacement.device.deviceId]).catch(() => undefined);
}
await deleted.cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([deleted.device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [
replacement?.device.deviceId,
deleted.device.deviceId,
]);
}
}
@@ -1566,6 +1574,7 @@ export async function runMatrixQaE2eeWrongAccountRecoveryKeyScenario(
};
} finally {
await cli?.dispose().catch(() => undefined);
await observer.stop().catch(() => undefined);
if (device) {
await observer.deleteOwnDevices([device.deviceId]).catch(() => undefined);
}
@@ -1627,7 +1636,6 @@ export async function runMatrixQaE2eeHistoryExistsBackupEmptyScenario(
};
} finally {
await cli.dispose().catch(() => undefined);
await setup.owner.deleteOwnDevices([device.deviceId]).catch(() => undefined);
await setup.owner.stop().catch(() => undefined);
await cleanupMatrixQaTempDevices(setup.owner, [device.deviceId]);
}
}

View File

@@ -1495,6 +1495,7 @@ export async function runMatrixQaE2eeRecoveryKeyLifecycleScenario(
}
}
await recoveryClient.stop();
await client.stop().catch(() => undefined);
await client.deleteOwnDevices([recoveryDevice.deviceId]).catch(() => undefined);
cleanupRecoveryDevice = false;
return {
@@ -1530,6 +1531,7 @@ export async function runMatrixQaE2eeRecoveryKeyLifecycleScenario(
} finally {
if (cleanupRecoveryDevice) {
await recoveryClient.stop().catch(() => undefined);
await client.stop().catch(() => undefined);
await client.deleteOwnDevices([recoveryDevice.deviceId]).catch(() => undefined);
}
}
@@ -1609,6 +1611,7 @@ export async function runMatrixQaE2eeRecoveryOwnerVerificationRequiredScenario(
].join("\n"),
};
} finally {
await client.stop().catch(() => undefined);
await client.deleteOwnDevices([recoveryDevice.deviceId]).catch(() => undefined);
}
},
@@ -3136,6 +3139,7 @@ export async function runMatrixQaE2eeStaleDeviceHygieneScenario(
if (!before.some((device) => device.deviceId === secondary.deviceId)) {
throw new Error("Matrix stale-device list did not include the secondary login");
}
await client.stop().catch(() => undefined);
const deleted = await client.deleteOwnDevices([secondary.deviceId]);
const remainingDeviceIds = deleted.remainingDevices.map((device) => device.deviceId);
if (remainingDeviceIds.includes(secondary.deviceId)) {