test: add frontend unit suite + fix mixed local/UTC timestamps

Two CODE_AUDIT items in one session.

§3.2 — Frontend regression net (js/tests/, 35 tests, node:test, zero deps):
- harness.js loads app.js (monofile, no exports) into a node:vm with browser
  globals stubbed, surfacing internals via an export epilogue.
- crypto: deriveKeyAndVerifier (AES key == raw PBKDF2, cross-checked vs Node
  pbkdf2Sync), legacy-vs-v2 verifier decoupling, encrypt/decrypt round-trip,
  IV uniqueness, AEAD tamper/wrong-key.
- csv: parseCSV tokenizer, findColumn heuristics, Bitwarden/KeePass mapping.
- merge: applyRemoteSnapshot add/update/skip (LWW), tombstone delete,
  resurrection arbitration (both NaN branches), local-tombstone veto,
  additive folder merge. Only api() is stubbed; loadEntries/encryptImportEntry
  run for real.
- Wired as a build gate in BuildAssets.ps1 (after node --check, bypass
  PM_SKIP_TESTS=1).

§2.2 — Unify timestamps on UTC:
- Entry created_at/updated_at were written via Delphi FormatDateTime(Now)
  = LOCAL, while deleted_at/tombstones use SQLite CURRENT_TIMESTAMP = UTC.
  The tombstone-resurrection arbitration compared the two zones, skewing by
  the machine's UTC offset even single-device.
- Add NowUTC/NowUTCStr to PM.Database, swap in at every entry/attachment
  write site (Entries create/update/bulk, Attachments POST echo).
- No JS change needed: arbitration now compares same-zone values.
- Existing rows self-heal on next edit (no destructive migration).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-04 19:17:33 +01:00
parent 73e4e37f19
commit d9397881dc
12 changed files with 818 additions and 26 deletions
+17
View File
@@ -28,6 +28,17 @@ lieu d'embarquer un bundle mort qui ne se révèle qu'après un rebuild
Delphi complet). Node optionnel — absent = warning, on continue. Toujours
`node --check js/app.js` après un gros edit JS.
Ensuite (même gate, node requis) il lance la **suite de tests frontend**
(`node --test js/tests/**/*.test.js`, ~1.7 s, zéro dépendance) : un
invariant crypto/merge cassé avorte le build comme une erreur de syntaxe.
`PM_SKIP_TESTS=1` pour bypasser en itération rapide. Lancer manuellement
via `npm test`. Voir [js/tests/README.md](js/tests/README.md) — le harness
charge `app.js` (monofichier sans exports) dans un `node:vm` avec les
globals navigateur stubbés, puis expose les internals via un épilogue
d'export. Couvre : round-trip crypto + dérivation verifier (legacy vs -v2),
parsing CSV d'import, et l'arbitrage merge/tombstone de sync
(`applyRemoteSnapshot`, seule `api()` est stubbée).
## Carte des fichiers
| Rôle | Path |
@@ -800,6 +811,12 @@ le vide ET vide `FPendingURL` → écran noir permanent sur cold-start lent.
nécessite `AttachThreadInput` pour cross-process.
- **`updated_at`** est bumpé à chaque PUT entry → sort par défaut =
`name` asc pour que modifier une entry ne change pas sa position.
- **Timestamps = UTC partout** : tout `created_at`/`updated_at`/`deleted_at`
écrit côté Delphi passe par `NowUTC`/`NowUTCStr` (`PM.Database`) — JAMAIS
`FormatDateTime(..., Now)` (heure locale). SQLite `CURRENT_TIMESTAMP` /
`datetime('now')` sont déjà UTC. Mélanger les deux décalait l'arbitrage
sync (résurrection tombstone) de l'offset UTC de la machine, même en solo.
Côté JS, générer les timestamps via `toISOString()` (UTC) uniquement.
- **TMS render-time vs DOM-attachment** : pattern courant — fonctions
qui rendent un widget (`soTagsField`, `soTotpField`) appellent un
helper (`renderSoChips`, `startTotpTick`) qui fait `$('#id')` sur un