mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-17 16:37:43 +00:00
* fix(sync): ignore startup example tasks during import
* fix(sync): defer startup example tasks until initial sync completes
Switch ExampleTasksService to afterInitialSyncDoneStrict$ so example tasks
are not created before the first remote import lands. On a fresh synced
client the imported tasks are then already in the store and the length===0
guard skips creation entirely — closing the same conflict for file-based
providers (Dropbox/WebDAV), which the op-log marker gate does not cover.
The isExampleTask marker + gate exclusion stay as a safety net for the
narrow case where example tasks are created on a still-empty server and an
import arrives before they are uploaded.
Also dedupe the two markRejected call sites into _discardExampleTaskOps and
document the local-only read (a remote isExampleTask flag can never bypass
the conflict dialog) and the intentional mixed-case behavior.
* test(sync): cover mixed-pending and empty-discard import gate cases
Add a SyncImportConflictGateService test for the mixed case (real pending
work + startup example tasks): the conflict dialog is still shown, but the
example-task id is reported in discardablePendingOpIds and intentionally
left for the caller to keep on USE_LOCAL — locking the documented invariant.
Also assert markRejected is not called on the config-only silent-accept path,
pinning the empty-array guard in _discardExampleTaskOps.
* test(sync): integration coverage for example-task import gate
Run the real OperationLogStoreService + SyncImportConflictGateService against
IndexedDB (no mocks) to cover the seam the unit specs stub:
- example-task creates persisted with their real multi-entity payload are
recognized as non-meaningful and reported as discardable
- after markRejected they are actually excluded from getUnsynced() (never
uploaded), while a pending config op is preserved
- real user work alongside example tasks still triggers the dialog
- a remote-sourced example-task op is never discardable (gate reads local
pending only)
Verified load-bearing: removing the gate exclusion turns the first case red.
* fix(sync): mark onboarding done when example tasks are skipped
EXAMPLE_TASKS_CREATED was only written after creating example tasks, so a
synced client that skipped creation because real tasks already existed never
set the flag — and could recreate onboarding tasks on a later startup when
the task list happened to be empty.
Set the flag whenever tasks already exist at the initial-sync gate, so
onboarding runs at most once per client.
* docs(sync): note known limits of the example-task import gate
Document two accepted, narrow residuals of syncing onboarding example tasks:
- upload→piggyback path: example ops accepted in the same upload round are
already synced and not in the discard list; state stays correct because the
SYNC_IMPORT filter drops them as CONCURRENT on receivers.
- file-based snapshot path: hasMeaningfulStoreData() counts example tasks (no
in-state marker), so a fresh Dropbox/WebDAV client that made example tasks
while sync was disabled can still see the conflict dialog.
* test(sync): cover fresh-client example-task import gate (e2e)
Add a SuperSync e2e proving a fresh client with first-run example tasks accepts
an incoming SYNC_IMPORT without a conflict dialog, plus a backward-compatible
{ allowExampleTasks } opt-in to createSimulatedClient (the harness otherwise
pre-sets SUP_EXAMPLE_TASKS_CREATED).
Uses waitForInitialSync:false + a race between the conflict dialog and sync
completion so the test actually fails when the dialog appears (pre-fix), and
asserts all four onboarding titles are absent. Guards the op-log isExampleTask
marker/gate path (not the afterInitialSyncDoneStrict$ timing, which a fresh
e2e client cannot exercise since sync is disabled at boot).
* docs(sync): link example-task import-gate limitations to #7985
116 lines
4.7 KiB
TypeScript
116 lines
4.7 KiB
TypeScript
import { test, expect } from '../../fixtures/supersync.fixture';
|
|
import {
|
|
createTestUser,
|
|
getSuperSyncConfig,
|
|
createSimulatedClient,
|
|
closeClient,
|
|
waitForTask,
|
|
getTaskTitles,
|
|
type SimulatedE2EClient,
|
|
} from '../../utils/supersync-helpers';
|
|
|
|
/**
|
|
* Regression test for #7976 — a fresh client's first-run example tasks must not trip the
|
|
* SYNC_IMPORT conflict gate when pulling an account that already has data.
|
|
*
|
|
* THE BUG: ExampleTasksService creates onboarding task-create ops on first run. When a
|
|
* fresh client then syncs an account that already has remote data, those pending ops used
|
|
* to make the conflict gate treat the client as having meaningful local work, so it showed
|
|
* a `dialog-sync-import-conflict` instead of silently accepting the import.
|
|
*
|
|
* WHAT THIS GUARDS: the op-log `isExampleTask` marker + gate exclusion. It does NOT
|
|
* exercise the afterInitialSyncDoneStrict$ timing — on a fresh e2e client sync is disabled
|
|
* at boot, so example tasks are created before sync is configured regardless of the strict
|
|
* gate; the marker is what suppresses the dialog here. Covering the strict-wait timing
|
|
* needs a client with sync pre-enabled at boot (a separate test).
|
|
*
|
|
* METHOD: configure sync with `waitForInitialSync: false` so setupSuperSync does NOT
|
|
* auto-resolve the conflict dialog (it otherwise clicks "Use Server Data"), then race
|
|
* "conflict dialog visible" against "sync complete". The fix means sync completes WITHOUT
|
|
* the dialog; pre-fix the dialog wins the race.
|
|
*
|
|
* REPRODUCE-FIRST: expected to FAIL on `bb4b625645^` (race resolves to 'dialog') and PASS
|
|
* on the PR head. Run on both before trusting it.
|
|
*
|
|
* Run: npm run e2e:supersync:file e2e/tests/sync/supersync-example-task-fresh-client.spec.ts -- --retries=0
|
|
*/
|
|
const EXAMPLE_TASK_TITLES = [
|
|
'Create your first project',
|
|
'Set up Sync',
|
|
'Learn the keyboard shortcuts',
|
|
'Go further',
|
|
];
|
|
|
|
test.describe('@supersync Fresh-client example tasks vs incoming import (#7976)', () => {
|
|
test('import is accepted without an example-task conflict dialog', async ({
|
|
browser,
|
|
baseURL,
|
|
testRunId,
|
|
}) => {
|
|
const appUrl = baseURL || 'http://localhost:4242';
|
|
const uniqueId = Date.now();
|
|
let seeder: SimulatedE2EClient | null = null;
|
|
let freshClient: SimulatedE2EClient | null = null;
|
|
|
|
try {
|
|
const user = await createTestUser(testRunId);
|
|
const syncConfig = getSuperSyncConfig(user);
|
|
|
|
// Seed the account with real data (example tasks suppressed for the seeder).
|
|
seeder = await createSimulatedClient(browser, appUrl, 'Seeder', testRunId);
|
|
await seeder.sync.setupSuperSync(syncConfig);
|
|
const realTask = `Real-Task-${uniqueId}`;
|
|
await seeder.workView.addTask(realTask);
|
|
await seeder.sync.syncAndWait();
|
|
|
|
// Fresh client WITH onboarding example tasks (created at boot, before sync config).
|
|
freshClient = await createSimulatedClient(browser, appUrl, 'Fresh', testRunId, {
|
|
allowExampleTasks: true,
|
|
});
|
|
|
|
// Configure sync but do NOT let setup auto-resolve the conflict dialog
|
|
// (waitForInitialSync:true would click "Use Server Data" and hide the bug).
|
|
await freshClient.sync.setupSuperSync({
|
|
...syncConfig,
|
|
waitForInitialSync: false,
|
|
});
|
|
|
|
// Race: does the example-task conflict dialog appear, or does the initial sync
|
|
// complete cleanly? (Pattern from supersync-import-clean-slate.spec.ts.)
|
|
const syncResult = await Promise.race([
|
|
freshClient.sync.syncImportConflictDialog
|
|
.waitFor({ state: 'visible', timeout: 30000 })
|
|
.then(() => 'dialog' as const),
|
|
freshClient.sync.syncCheckIcon
|
|
.waitFor({ state: 'visible', timeout: 30000 })
|
|
.then(() => 'complete' as const),
|
|
]);
|
|
|
|
// CORE REGRESSION: the import is accepted silently — no example-task conflict dialog.
|
|
// Pre-fix this resolves to 'dialog'.
|
|
expect(syncResult).toBe('complete');
|
|
|
|
// The import replaced local state: the real remote task is present and NONE of the
|
|
// onboarding example tasks survive. Example tasks live in the INBOX project.
|
|
// (If the seeder's task lands elsewhere on your setup, adjust this navigation.)
|
|
await freshClient.page.goto('/#/project/INBOX_PROJECT/tasks', {
|
|
waitUntil: 'domcontentloaded',
|
|
timeout: 30000,
|
|
});
|
|
await freshClient.page.waitForLoadState('networkidle');
|
|
await waitForTask(freshClient.page, realTask);
|
|
|
|
const titles = await getTaskTitles(freshClient);
|
|
for (const exampleTitle of EXAMPLE_TASK_TITLES) {
|
|
expect(titles).not.toContain(exampleTitle);
|
|
}
|
|
} finally {
|
|
if (freshClient) {
|
|
await closeClient(freshClient);
|
|
}
|
|
if (seeder) {
|
|
await closeClient(seeder);
|
|
}
|
|
}
|
|
});
|
|
});
|