fix(import): count the trash when deduplicating by uuid
The map was built from state.entries only, so a container entry whose uuid existed just in the trash looked fresh and inserted a second row sharing that uuid - the identity the sync engine relies on. Same blind spot as the resurrection bug in applyRemoteSnapshot. Also documents F9 and warns that emptying the trash is the wrong way to clean duplicate uuids: the tombstone would kill the live twin on the other device.
This commit is contained in:
@@ -233,6 +233,15 @@ plus bas.
|
|||||||
- **ACTION** : importer un CSV sans colonnes favorite/pinned.
|
- **ACTION** : importer un CSV sans colonnes favorite/pinned.
|
||||||
- **ATTENDU** : entrées importées ni épinglées ni favorites (défaut 0), pas d'erreur.
|
- **ATTENDU** : entrées importées ni épinglées ni favorites (défaut 0), pas d'erreur.
|
||||||
|
|
||||||
|
### F9 · la corbeille voyage dans les sauvegardes ✅
|
||||||
|
- **ACTION** : vault avec par exemple 4 entrées vivantes et 2 en corbeille → *Export vault* → *Clear vault* → réimporter.
|
||||||
|
- **ATTENDU** : état **identique** — 4 dans la liste, 2 en corbeille. Le compteur *Trash* est juste sans avoir à ouvrir la vue.
|
||||||
|
- **`deleted_at` est préservé**, pas réinitialisé : sinon chaque entrée restaurée repartirait pour un délai d'auto-purge complet, et une entrée supprimée il y a 29 jours recommencerait à zéro. D'où son ajout à l'écho de `GET /entries`.
|
||||||
|
- **Deuxième test, celui qui a trouvé le bug** : mettre une entrée à la corbeille → exporter → réimporter **sans rien vider**. L'entrée doit être comptée dans les « déjà présentes » (*Overwrite* / *Skip*), **jamais** ajoutée.
|
||||||
|
La déduplication de l'import ne construisait sa table que depuis `state.entries` : une entrée dont l'`uuid` n'existait **que** dans la corbeille passait pour nouvelle et créait une **seconde ligne partageant son uuid** — l'identité sur laquelle repose toute la sync. Même angle mort que F8.
|
||||||
|
- **Ne PAS vider la corbeille pour nettoyer des doublons d'uuid** : *Empty trash* et *Delete forever* écrivent un tombstone portant cet `uuid`, et l'autre appareil supprimerait alors le jumeau vivant. Retirer les lignes en trop directement en base.
|
||||||
|
- **Le sync, lui, n'emporte pas la corbeille** — voir F8 et `buildSyncSnapshot`.
|
||||||
|
|
||||||
### F8 · mettre à la corbeille puis synchroniser ne duplique pas ✅
|
### F8 · mettre à la corbeille puis synchroniser ne duplique pas ✅
|
||||||
- **ACTION** : synchroniser une première fois (l'instantané distant contient les entrées) → mettre **plusieurs** entrées à la corbeille → **Sync now**.
|
- **ACTION** : synchroniser une première fois (l'instantané distant contient les entrées) → mettre **plusieurs** entrées à la corbeille → **Sync now**.
|
||||||
- **ATTENDU** : les entrées restent en corbeille, et **rien n'est recréé** dans la liste vivante. Le résumé ne doit pas annoncer `pulled N new`.
|
- **ATTENDU** : les entrées restent en corbeille, et **rien n'est recréé** dans la liste vivante. Le résumé ne doit pas annoncer `pulled N new`.
|
||||||
|
|||||||
@@ -680,8 +680,15 @@ async function doImport() {
|
|||||||
// exists — the user is either re-importing a backup or rolling
|
// exists — the user is either re-importing a backup or rolling
|
||||||
// back to an earlier version). Ask what to do with overlapping
|
// back to an earlier version). Ask what to do with overlapping
|
||||||
// entries so a restore isn't silently blocked by the dedup.
|
// entries so a restore isn't silently blocked by the dedup.
|
||||||
|
// The TRASH counts as "already in the vault". Its rows are absent from
|
||||||
|
// state.entries, so without this a container entry whose uuid is only
|
||||||
|
// in the trash was treated as fresh and inserted a SECOND row with the
|
||||||
|
// same uuid — the identity the whole sync engine relies on. Same blind
|
||||||
|
// spot as the resurrection bug in applyRemoteSnapshot (F8).
|
||||||
|
await loadTrash();
|
||||||
const localByUuid = new Map();
|
const localByUuid = new Map();
|
||||||
for (const e of state.entries) if (e && e.uuid) localByUuid.set(e.uuid, e);
|
for (const e of state.entries) if (e && e.uuid) localByUuid.set(e.uuid, e);
|
||||||
|
for (const e of (state.trashed || [])) if (e && e.uuid) localByUuid.set(e.uuid, e);
|
||||||
const fresh = [];
|
const fresh = [];
|
||||||
const overlaps = [];
|
const overlaps = [];
|
||||||
for (const e of parsed.entries) {
|
for (const e of parsed.entries) {
|
||||||
|
|||||||
Reference in New Issue
Block a user