CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m41s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 15m54s
When the server keeps rejecting this device's /keys/upload with 400 "One time key ... already exists", the device's encryption store has diverged and the SDK retries forever (matrix-rust-sdk#5200, still open). Synapse saw 474k of these from May 30 to Jul 8 2026. The fix is a fresh login, so: - keyUploadWatch observes /keys/upload responses through the client's fetchFn (the response the SDK gets is untouched; a clone is read). Three conflicts within 15 min = stuck; any successful upload clears it. - KeyUploadBanner (critical strip, same slot as the sync/clock banners): "Encryption on this device is out of sync" with Details and "Fix: sign out…", which opens the normal logout dialog (it already warns when the device isn't verified / backed up). Server side, Grafana now alerts on the same signal (E2EE Key Upload Conflicts) and on delayed-event 5xx (Call Delayed-Event Errors). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
50 lines
2.0 KiB
TypeScript
50 lines
2.0 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { ensureUser, hsReachable, loginUI, uniq } from './localHs';
|
|
|
|
// [Gitea #201 KE-1] When the server keeps rejecting this device's key uploads
|
|
// with "One time key … already exists", the app says so and offers the fix.
|
|
const CONFLICT = {
|
|
errcode: 'M_UNKNOWN',
|
|
error: 'One time key signed_curve25519:AAAAAAAAAA0 already exists. Old key: {}; new key: {}',
|
|
};
|
|
|
|
test.describe('key upload recovery (#201)', () => {
|
|
test.beforeAll(async () => {
|
|
test.skip(!(await hsReachable()), 'local homeserver not running');
|
|
});
|
|
|
|
test('a diverged device sees the banner, and Fix opens the logout dialog', async ({ page }) => {
|
|
test.setTimeout(240_000);
|
|
const user = await ensureUser(uniq('kup'));
|
|
let rejected = 0;
|
|
await page.route(/\/_matrix\/client\/v3\/keys\/upload/, async (route) => {
|
|
rejected += 1;
|
|
await route.fulfill({ status: 400, json: CONFLICT });
|
|
});
|
|
await loginUI(page, user);
|
|
|
|
const banner = page
|
|
.getByRole('alert')
|
|
.filter({ hasText: /Encryption on this device is out of sync/ });
|
|
await expect(banner).toBeVisible({ timeout: 200_000 });
|
|
console.log(`key uploads rejected: ${rejected}`);
|
|
expect(rejected).toBeGreaterThanOrEqual(3);
|
|
|
|
await banner.getByRole('button', { name: 'Details' }).click();
|
|
await expect(banner).toContainText('Settings → Devices');
|
|
await expect(banner).toContainText('already exists');
|
|
|
|
await banner.getByRole('button', { name: /Fix: sign out/ }).click();
|
|
await expect(page.getByRole('dialog').getByText('Logout').first()).toBeVisible();
|
|
});
|
|
|
|
test('a healthy device never sees it', async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const user = await ensureUser(uniq('kok'));
|
|
await loginUI(page, user);
|
|
await page.getByText('Connecting...').first().waitFor({ state: 'detached', timeout: 90_000 });
|
|
await page.waitForTimeout(5000);
|
|
await expect(page.getByText(/Encryption on this device is out of sync/)).toHaveCount(0);
|
|
});
|
|
});
|