mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
fix(sync): handle data validation errors and improve client ID management
- Separate JsonParseError and SyncDataCorruptedError handlers in sync wrapper - Handle corrupted remote data gracefully instead of throwing - Add getOrGenerateClientId() as unified entry point, eliminating dual injection of ClientIdService + CLIENT_ID_PROVIDER in snapshot-upload, file-based-encryption, and sync-hydration services - Use crypto.getRandomValues() instead of Math.random() for client ID generation - Warn user when stored client ID is invalid and must be regenerated - Fix flaky e2e supersync tests (premature waitForURL match on daily-summary URL)
This commit is contained in:
parent
81788143b6
commit
0e9218bd68
87 changed files with 1875 additions and 3372 deletions
|
|
@ -47,18 +47,9 @@ export const test = base.extend<SuperSyncFixtures>({
|
|||
* Also checks server health and skips test if server unavailable.
|
||||
*/
|
||||
testRunId: async ({}, use, testInfo) => {
|
||||
// Check server health once per worker.
|
||||
// Prefer the result pre-computed in globalSetup (stored as an env var before workers
|
||||
// were forked) to avoid a stampede of concurrent HTTP requests from all workers
|
||||
// simultaneously — which can overload the server and produce false negatives.
|
||||
// Check server health once per worker
|
||||
if (serverHealthyCache === null) {
|
||||
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
|
||||
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
|
||||
} else {
|
||||
// Fallback for when tests are run directly without going through globalSetup
|
||||
// (e.g., `npx playwright test --config ... e2e/tests/sync/foo.spec.ts`)
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
}
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
if (!serverHealthyCache) {
|
||||
console.warn(
|
||||
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
|
||||
|
|
@ -78,13 +69,9 @@ export const test = base.extend<SuperSyncFixtures>({
|
|||
* Tests are automatically skipped if the server is not healthy.
|
||||
*/
|
||||
serverHealthy: async ({}, use, testInfo) => {
|
||||
// Only check once per worker — use globalSetup env var if available (see testRunId above)
|
||||
// Only check once per worker
|
||||
if (serverHealthyCache === null) {
|
||||
if (process.env.SUPERSYNC_SERVER_HEALTHY !== undefined) {
|
||||
serverHealthyCache = process.env.SUPERSYNC_SERVER_HEALTHY === 'true';
|
||||
} else {
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
}
|
||||
serverHealthyCache = await isServerHealthy();
|
||||
if (!serverHealthyCache) {
|
||||
console.warn(
|
||||
'SuperSync server not healthy at http://localhost:1901 - skipping tests',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { FullConfig } from '@playwright/test';
|
|||
import { execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { isServerHealthy } from './utils/supersync-helpers';
|
||||
|
||||
/**
|
||||
* Warm up the dev server by fetching the app once before any tests start.
|
||||
|
|
@ -70,20 +69,6 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
|
|||
config.projects[0]?.use?.baseURL ||
|
||||
'http://localhost:4242';
|
||||
await warmUpDevServer(baseURL);
|
||||
|
||||
// Check SuperSync server health ONCE here, before workers start.
|
||||
// Without this, each worker checks independently on startup — with many workers
|
||||
// running simultaneously, the concurrent health-check requests can overload the
|
||||
// supersync server and cause false negatives, making workers skip all their tests.
|
||||
// By storing the result in an env var set before workers are forked, every worker
|
||||
// reads the cached result instantly instead of making HTTP requests.
|
||||
const healthy = await isServerHealthy().catch(() => false);
|
||||
process.env.SUPERSYNC_SERVER_HEALTHY = healthy ? 'true' : 'false';
|
||||
if (healthy) {
|
||||
console.log('SuperSync server healthy — supersync tests will run');
|
||||
} else {
|
||||
console.log('SuperSync server not available — supersync tests will be skipped');
|
||||
}
|
||||
};
|
||||
|
||||
export default globalSetup;
|
||||
|
|
|
|||
|
|
@ -191,14 +191,8 @@ export class SuperSyncPage extends BasePage {
|
|||
// piggybacked ops in the response, causing data to sync between clients
|
||||
// outside of explicit syncAndWait(). Set here (not in createSimulatedClient)
|
||||
// so tests using enableWebSocket:true still get immediate uploads.
|
||||
// 3. Block WsTriggeredDownloadService: Even when routeWebSocket() closes the
|
||||
// connection, a WS notification can slip through the moment the connection
|
||||
// opens (before it's closed). Setting __SP_E2E_BLOCK_WS_DOWNLOAD ensures
|
||||
// WsTriggeredDownloadService ignores any such notifications, preventing
|
||||
// uncontrolled background syncs that race with explicit syncAndWait() calls.
|
||||
await this.page.evaluate(() => {
|
||||
(globalThis as any).__SP_E2E_BLOCK_IMMEDIATE_UPLOAD = true;
|
||||
(globalThis as any).__SP_E2E_BLOCK_WS_DOWNLOAD = true;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import {
|
|||
renameTask,
|
||||
archiveDoneTasks,
|
||||
expectTaskInWorklog,
|
||||
hasTaskInWorklog,
|
||||
navigateToWorkView,
|
||||
type SimulatedE2EClient,
|
||||
} from '../../utils/supersync-helpers';
|
||||
|
|
@ -268,30 +267,10 @@ test.describe('@supersync Archive Conflict Resolution', () => {
|
|||
console.log('[BugB] Client B: task not visible in active list');
|
||||
|
||||
// ============ PHASE 7: Verify task IN worklog ============
|
||||
// After archive-wins conflict + sync rounds, the archived task may appear under
|
||||
// either the original title (if archive applied first) or the renamed title (if
|
||||
// the rename op was also synced). Accept either to avoid brittleness.
|
||||
const clientAFound =
|
||||
(await hasTaskInWorklog(clientA, taskName)) ||
|
||||
(await hasTaskInWorklog(clientA, taskRenamed));
|
||||
if (!clientAFound) {
|
||||
throw new Error(
|
||||
`[BugB] Client A: Expected task to be in worklog under "${taskName}" or "${taskRenamed}"`,
|
||||
);
|
||||
}
|
||||
await expectTaskInWorklog(clientA, taskName);
|
||||
console.log('[BugB] Client A: task found in worklog');
|
||||
|
||||
// Client B applied the rename locally before archive was received, so the task
|
||||
// is archived under the renamed title. But also accept the original in case the
|
||||
// archive was applied before the rename reached Client B.
|
||||
const clientBFound =
|
||||
(await hasTaskInWorklog(clientB, taskRenamed)) ||
|
||||
(await hasTaskInWorklog(clientB, taskName));
|
||||
if (!clientBFound) {
|
||||
throw new Error(
|
||||
`[BugB] Client B: Expected task to be in worklog under "${taskRenamed}" or "${taskName}"`,
|
||||
);
|
||||
}
|
||||
await expectTaskInWorklog(clientB, taskName);
|
||||
console.log('[BugB] Client B: task found in worklog');
|
||||
|
||||
console.log('[BugB] ✓ Test B passed: LWW Update did not resurrect archived task');
|
||||
|
|
|
|||
|
|
@ -672,11 +672,8 @@ test.describe('@supersync SuperSync E2E', () => {
|
|||
await saveAndGoHomeBtn.click();
|
||||
console.log('[Archive Test] Client B clicked Save and go home (archiving)');
|
||||
|
||||
// Wait for navigation back to work view.
|
||||
// Use negative lookahead to avoid matching /tag/TODAY/daily-summary prematurely.
|
||||
await clientB.page.waitForURL(/(tag\/TODAY(?!\/daily-summary))/, {
|
||||
timeout: 10000,
|
||||
});
|
||||
// Wait for navigation back to work view
|
||||
await clientB.page.waitForURL(/tag\/TODAY/, { timeout: 10000 });
|
||||
await clientB.page.waitForLoadState('networkidle');
|
||||
console.log('[Archive Test] Client B back on work view after archiving');
|
||||
|
||||
|
|
|
|||
|
|
@ -239,34 +239,8 @@ export const createSimulatedClient = async (
|
|||
}
|
||||
});
|
||||
|
||||
// Navigate to app with retry for transient ERR_CONNECTION_REFUSED.
|
||||
// Under parallel load (many workers × 2-3 browser contexts each), the Angular
|
||||
// dev server can temporarily refuse connections. Retrying recovers from this
|
||||
// without failing the test outright.
|
||||
let lastGotoError: Error | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
try {
|
||||
await page.goto('/');
|
||||
lastGotoError = null;
|
||||
break;
|
||||
} catch (e) {
|
||||
lastGotoError = e as Error;
|
||||
const isConnectionRefused = lastGotoError.message.includes(
|
||||
'ERR_CONNECTION_REFUSED',
|
||||
);
|
||||
if (attempt < 2 && isConnectionRefused) {
|
||||
const delay = 1000 * (attempt + 1);
|
||||
console.log(
|
||||
`[Client ${clientName}] page.goto('/') failed (attempt ${attempt + 1}/3): ERR_CONNECTION_REFUSED — retrying in ${delay}ms`,
|
||||
);
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
} else {
|
||||
break; // Non-connection error or last attempt — let it throw below
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastGotoError) throw lastGotoError;
|
||||
|
||||
// Navigate to app and wait for ready
|
||||
await page.goto('/');
|
||||
await waitForAppReady(page);
|
||||
|
||||
const workView = new WorkViewPage(page, `${clientName}-${testPrefix}`);
|
||||
|
|
@ -527,10 +501,6 @@ export const markTaskDone = async (
|
|||
const task = getTaskElement(client, taskName);
|
||||
await task.hover();
|
||||
await task.locator('done-toggle').click();
|
||||
// Wait for the 200ms animation delay in toggleDoneWithAnimation to complete.
|
||||
// During CDK drag animation the task may temporarily resolve to 2 elements;
|
||||
// use .first() to avoid strict-mode violations.
|
||||
await expect(task.first()).toHaveClass(/isDone/, { timeout: UI_VISIBLE_TIMEOUT });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -547,8 +517,6 @@ export const markSubtaskDone = async (
|
|||
const subtask = getSubtaskElement(client, subtaskName);
|
||||
await subtask.hover();
|
||||
await subtask.locator('done-toggle').click();
|
||||
// Wait for the 200ms animation delay in toggleDoneWithAnimation to complete
|
||||
await expect(subtask).toHaveClass(/isDone/, { timeout: UI_VISIBLE_TIMEOUT });
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue