From 821896496bb1d5cf23abd256f1df9cde7c42caf8 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Sun, 29 Mar 2026 17:20:06 +0200 Subject: [PATCH] test(e2e): add day-change sync convergence test (#6992) and fix lint Add supersync E2E test for day-change overdue sync race condition. Fix prettier/lint issues in repeat task first occurrence spec. --- ...eat-task-first-occurrence-bug-5594.spec.ts | 12 +- .../supersync-day-change-overdue-6992.spec.ts | 128 ++++++++++++++++++ 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 e2e/tests/sync/supersync-day-change-overdue-6992.spec.ts diff --git a/e2e/tests/recurring/repeat-task-first-occurrence-bug-5594.spec.ts b/e2e/tests/recurring/repeat-task-first-occurrence-bug-5594.spec.ts index 2d27230197..fa1d5846a6 100644 --- a/e2e/tests/recurring/repeat-task-first-occurrence-bug-5594.spec.ts +++ b/e2e/tests/recurring/repeat-task-first-occurrence-bug-5594.spec.ts @@ -96,9 +96,7 @@ test.describe('Issue #5594: First repeat occurrence should not always be today', // 9. ASSERTION: The task should NOT be in today's undone task list // because Saturday is not in the Mon/Wed/Fri pattern. // The task should have been rescheduled to Monday (2026-06-15). - const undoneTasksWithTitle = taskPage - .getUndoneTasks() - .filter({ hasText: taskTitle }); + const undoneTasksWithTitle = taskPage.getUndoneTasks().filter({ hasText: taskTitle }); // The task should disappear from Today view await expect(undoneTasksWithTitle).toHaveCount(0, { timeout: 15000 }); @@ -114,7 +112,9 @@ test.describe('Issue #5594: First repeat occurrence should not always be today', // The task should appear in the planner under Monday (June 15) // Planner renders tasks in day columns — look for the task text anywhere on the page - const plannerTask = page.getByText(taskTitle.replace(testPrefix + '-', testPrefix + '-')); + const plannerTask = page.getByText( + taskTitle.replace(testPrefix + '-', testPrefix + '-'), + ); await expect(plannerTask.first()).toBeVisible({ timeout: 15000 }); console.log('[Bug #5594] Task correctly appears in planner for future day'); @@ -158,9 +158,7 @@ test.describe('Issue #5594: First repeat occurrence should not always be today', await page.keyboard.press('Escape'); // 5. ASSERTION: Task SHOULD remain in Today for DAILY repeat - const undoneTasksWithTitle = taskPage - .getUndoneTasks() - .filter({ hasText: taskTitle }); + const undoneTasksWithTitle = taskPage.getUndoneTasks().filter({ hasText: taskTitle }); await expect(undoneTasksWithTitle.first()).toBeVisible({ timeout: 10000 }); console.log('[Bug #5594] Daily repeat task correctly stays in Today'); diff --git a/e2e/tests/sync/supersync-day-change-overdue-6992.spec.ts b/e2e/tests/sync/supersync-day-change-overdue-6992.spec.ts new file mode 100644 index 0000000000..ac644d2eb5 --- /dev/null +++ b/e2e/tests/sync/supersync-day-change-overdue-6992.spec.ts @@ -0,0 +1,128 @@ +import { test, expect } from '../../fixtures/supersync.fixture'; +import { + createTestUser, + getSuperSyncConfig, + createSimulatedClient, + closeClient, + waitForTask, + getTaskTitles, + type SimulatedE2EClient, +} from '../../utils/supersync-helpers'; + +/** + * Issue #6992: Sync differences after new-day routines. + * + * Reproduces the scenario where day-change effects on one device create + * operations that conflict with user actions on another device. + * + * Simplified approach (avoids complex recurring task setup): + * 1. Both clients start on day X at 23:55 + * 2. Client A creates a task (gets added to TODAY_TAG for day X) + * 3. Both sync + * 4. Advance both clocks past midnight to day X+1 + * 5. Day-change effects fire on both, creating new repeat instances + * 6. Client A moves a task to today, Client B's day-change removes overdue + * 7. Both sync, verify convergence + * + * Key assertion: after sync, both clients show the same tasks in their + * today list. If they diverge, the LWW conflict resolution failed. + */ +test.describe('@supersync Day-change overdue sync (#6992)', () => { + test('tasks should converge after day change on both clients', async ({ + browser, + baseURL, + testRunId, + }) => { + const appUrl = baseURL || 'http://localhost:4242'; + let clientA: SimulatedE2EClient | null = null; + let clientB: SimulatedE2EClient | null = null; + + try { + const user = await createTestUser(testRunId); + const syncConfig = getSuperSyncConfig(user); + + const dayXTime = new Date('2026-07-20T23:55:00'); + const dayX1Morning = new Date('2026-07-21T09:00:00'); + const dayX1Later = new Date('2026-07-21T09:30:00'); + + // ============ PHASE 1: Client A creates tasks on day X ============ + clientA = await createSimulatedClient(browser, appUrl, 'A', testRunId); + await clientA.page.clock.setFixedTime(dayXTime); + await clientA.page.reload(); + await clientA.workView.waitForTaskList(); + await clientA.sync.setupSuperSync(syncConfig); + + const task1 = `Task1-${testRunId}`; + const task2 = `Task2-${testRunId}`; + await clientA.workView.addTask(task1); + await clientA.workView.addTask(task2); + await clientA.sync.syncAndWait(); + console.log('[#6992] Client A created 2 tasks and synced'); + + // ============ PHASE 2: Client B downloads ============ + clientB = await createSimulatedClient(browser, appUrl, 'B', testRunId); + await clientB.page.clock.setFixedTime(dayXTime); + await clientB.page.reload(); + await clientB.workView.waitForTaskList(); + await clientB.sync.setupSuperSync(syncConfig); + await clientB.sync.syncAndWait(); + await waitForTask(clientB.page, task1); + await waitForTask(clientB.page, task2); + console.log('[#6992] Client B synced, has both tasks'); + + // ============ PHASE 3: Day change on Client A (morning) ============ + // Advance Client A clock to day X+1 morning + await clientA.page.clock.setFixedTime(dayX1Morning); + // Wait for day-change effects to process + await clientA.page.waitForTimeout(5000); + console.log('[#6992] Client A advanced to day X+1 morning'); + + // Get Client A's task list + const titlesABefore = await getTaskTitles(clientA); + console.log('[#6992] Client A tasks after day change:', titlesABefore); + + // Sync Client A + await clientA.sync.syncAndWait(); + console.log('[#6992] Client A synced'); + + // ============ PHASE 4: Day change on Client B (later — THE RACE) ============ + // Client B's clock advances AFTER Client A has already synced. + // Any operations created by Client B's day-change will have a NEWER timestamp. + await clientB.page.clock.setFixedTime(dayX1Later); + // Wait for day-change effects on Client B + await clientB.page.waitForTimeout(5000); + console.log('[#6992] Client B advanced to day X+1 (later)'); + + // ============ PHASE 5: Sync convergence ============ + await clientB.sync.syncAndWait(); + await clientA.sync.syncAndWait(); + await clientB.sync.syncAndWait(); + console.log('[#6992] All sync rounds complete'); + + // Allow UI to settle + await clientA.page.waitForTimeout(1000); + await clientB.page.waitForTimeout(1000); + + // ============ PHASE 6: Verify convergence ============ + const titlesA = await getTaskTitles(clientA); + const titlesB = await getTaskTitles(clientB); + console.log('[#6992] Client A final tasks:', titlesA); + console.log('[#6992] Client B final tasks:', titlesB); + + // Both clients should have the SAME tasks in their today list. + // Order may differ, so compare as sorted arrays. + const sortedA = [...titlesA].sort(); + const sortedB = [...titlesB].sort(); + + // This is the core convergence check. + // If this fails, the day-change operations created a conflict that + // was resolved differently on each device. + expect(sortedA).toEqual(sortedB); + + console.log('[#6992] PASS: Both clients converged'); + } finally { + if (clientA) await closeClient(clientA); + if (clientB) await closeClient(clientB); + } + }); +});