feat(profiles): name the copy of a stopped catch-up, one copy per profile, startup error cleared (#395) #436
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/Simpl-Resultat#436
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-395-catch-up-followups"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Generated autonomously by /autopilot run of 2026-10-12 (milestone
planned-2026-10-10-fiabilite-tests, worker 4/4, last link of the stack #433 → #434 → #435 → this PR).Suivis de la revue de la PR #394 (#390) consignés dans #395. Base :
issue-389-i18n-mock(PR #435).Ce qui change
1. Copie nommée sur un rattrapage arrêté
ProfilePreparation.catch_up_backup: Option<String>(Rust et TS), rempli dès que la copie existe, même si le rattrapage s'arrête ensuite.catch_updevient une enveloppe de test decatch_up_into(conn, migrations, backup, &mut CatchUp), queprepare_atappelle : le rapport partiel survit à l'erreur.; copy of the previous state kept as <nom>.withoutFolders(db.ts) : dans toute ligne d'erreur dedescribePreparationet dans le message de repli deconnectToProfile, un chemin est réduit à son dernier segment, tout autre séparateur devient-(« disk I/O error » → « disk I-O error »). Aucune ligne d'erreur ne contient/ni\(testé, y compris<profile folder>/migration-backups/…laissé parscrubetC:\Users\L'Heureux\…).history_write_failed(déclencheur qui refuse l'historique),pragma_failed(point de défaillance#[cfg(test)], voir décisions),apply_pending(v18 qui échoue).2. Épingles RV4, RV7, RT2 : le test du profil sans catégorie 31 porte une violation préexistante (transaction vers une catégorie absente, clé
SET NULL) : signalée dansfk_violations, jamais touchée. Test TS de la lignepoint to a missing row.3. Délai de démarrage
Bug corrigé : la logique de démarrage d'
App.tsxpasse danssrc/utils/startupConnection.ts(startProfileConnection), qui faitsetStartupError(null)au succès ; 5 tests aux faux minuteurs.Test
#[ignore]a_large_profile_catch_up_duration: plancher 6 (schéma v0.3.0 + seed), 100 001 transactions (17 Mo, 10 001 drapeaux texte), v7 → v17 avec copie,foreign_key_checket réparation.Machine : ThinkPad T490, Intel Core i5-8365U @ 1.60GHz (max 4,1 GHz), 8 CPU (4 cœurs × 2 fils,
nproc8), 16 Go, SSD NVMe, Linux 6.17.Règle appliquée : 306 ms < 3 s →
STARTUP_TIMEOUT_MSreste 10 000. Mesure, machine, règle et risque résiduel dans les points de vigilance de l'ADR 0021.4. Rétention des copies
backup_torefait la copie à chaque rattrapage :.tmp, synchronisé, renommé par-dessus la copie de même nom (jamais supprimée d'abord).remove_backups(dir, stem, Some(copie))supprime les autres<profil>-before-v*.db/.db.tmpdu profil (au mieux). Rien n'est supprimé sur erreur, sur copie échouée, ni quand rien n'est à appliquer.delete_profile_dbpasseNone.docs/guide-utilisateur.md) etdocs.profiles.tipsFR/EN : une seule copie par profil, la plus récente, refaite à chaque mise à jour, supprimée avec le profil.5. CHANGELOG (EN + FR, sous [Unreleased], entrées existantes gardées) : Changed / Modifié (une copie par profil, copie nommée au Journal), Fixed / Corrigé (page d'erreur du démarrage).
Fichiers
src-tauri/src/database/profile_db.rs,src-tauri/src/commands/profile_commands.rssrc/services/db.ts,src/services/db.test.tssrc/App.tsx,src/utils/startupConnection.ts(nouveau),src/utils/startupConnection.test.ts(nouveau)docs/adr/0021-migrations-profils-secondaires.md,docs/architecture.md,docs/guide-utilisateur.md,src/i18n/locales/fr.json,src/i18n/locales/en.jsonCHANGELOG.md,CHANGELOG.fr.mdDécisions
catch_up_into+catch_upen enveloppe#[cfg(test)]: signature d'erreurStringet tests existants inchangés.pragma_failed: aucune façon de faire échouerPRAGMA foreign_keys = ONsur un vrai SQLite (pas de featurehooks/limitsde rusqlite). Point de défaillancefail_point("pragma")lu dans unthread_localcompilé sous#[cfg(test)]seulement, toujoursOken production. Écartés : activer une feature rusqlite pour un test ; ne pas tester ce chemin./ni\» est appliqué à toutes les lignes d'erreur de la préparation, pas seulement au nom de la copie :scrub(Rust) laisse<profile folder>/migration-backups/x.tmp. Écartés : changer le placeholder Rust (épinglé par #390) ; tronquer le message.let _ =) : une copie restée ne nuit pas, et un échec d'élagage ne doit ni bloquer l'ouverture ni écrire « database update stopped ».src/utils/startupConnection.ts, drapeau d'annulation local à chaque exécution de l'effet (au lieu d'unuseRefpartagé entre exécutions).Preuves
npm run buildvert.cargo test262 verts + 1 ignoré (base 255) ;cargo checksans avertissement.Mutations (une à la fois, restauration par
git checkout, atterrissage vérifié)fk_violationstoujours videa_profile_without_category_31_still_catches_up)beforevide dans la reprisecatch_up_backupperdu sur erreurkeepignoré dansremove_backupssetStartupError(null)retiré au succèswithoutFoldersidentité14/14 rouges, aucune erreur de compilation.
À regarder
profile_db.rs(FAIL_STEP,fail_point).withoutFolders(LOOKS_LIKE_PATH) : un chemin relatif à un seul séparateur devienta-b.dbplutôt queb.db.🤖 Generated with Claude Code
a46a94a314tofbecfffa6eAuto-revue (worker autopilot, non indépendante) — PR #436 (#395)
Verdict : APPROVE
Diff relu en entier contre
issue-389-i18n-mock, têtefbecfff.Critères de #395
catch_up_backup(Rust + TS), rempli dès la copie, même sur erreur ; ligne d'erreur du Journal qui le nomme ; un test Rust par chemin (history_write_failed,pragma_failed,apply_pending) ; nom seul, jamais de chemin ; aucune ligne d'erreur avec/ni\(test TS), message de repli compris.setStartupError(null)au succès, épinglé ; test#[ignore]lancé (306 ms release, 422 ms debug, machine dans le corps) ; règle appliquée (< 3 s → 10 000 inchangé) ; mesure, machine, règle et risque résiduel dans l'ADR 0021..tmp, sync, rename par-dessus) ; élagage seulement après succès et renommage ; rien sur erreur ni sur copie échouée ; mutation « élaguer avant la copie » rouge (6 tests) ; guide FR + tips FR/EN.Contrôles
FAIL_STEPest compilé sous#[cfg(test)]seulement,fail_pointvautOk(())en production.architecture.md, guide.cargo test262 + 1 ignoré,cargo checksans avertissement.Restes, non bloquants
renamelaisserait tous les tests verts (seul un plantage entre les deux le montrerait). Le code ne le fait pas ; le commentaire debackup_tole dit.withoutFolders: un chemin relatif à un seul séparateur devienta-bplutôt queb(pas de dossier utilisateur dans ce cas).CI lue par l’orchestrateur de l’autopilot (2026-10-12) sur la tête
fbecfff:PR Check — Frontend, run 536, vert :Test Files 119 passed (119),Tests 2039 passed (2039).PR Check — Rust, run 537, vert : cache restauré enfull match: true,windows-sys pinning: 9 checks, exit 0,test result: ok. 262 passed; 0 failed; 1 ignored. Le test ignoré est la mesurea_large_profile_catch_up_duration.Point cosmétique pour la revue : dans
CHANGELOG.mdcomme dansCHANGELOG.fr.md, l’entrée Fixed / Corrigé de #395 est suivie d’une ligne vide avant celle de #320. Les deux puces de la section sont donc séparées par un blanc, à retirer.Revue indépendante (/pr-review) — PR #436 (#395)
Verdict : APPROVE
Le diff a été relu en entier contre
issue-389-i18n-mock, têtefbecfff. Le code fait ce que le corps annonce : la copie est nommée sur les trois arrêts qui suivent la copie, une seule copie est gardée par profil, l'élagage n'a lieu qu'après un succès et après le renommage, et la page d'erreur du démarrage se retire au succès. Aucun bloquant.Exécuté par le relecteur (copie hors dépôt par
git archivedefbecfff)npm cipuis vitest : 119 fichiers / 2039 tests verts.cargo test --lib profile_db: 36 verts, 1 ignoré./commits/fbecfff/statusagrégé ensuccess(Frontend et Rust).cmp/diff -q. Toutes rougissent :setStartupError(null)retiré au succèsclearTimeoutretiré au succès (le délai réécrit l'erreur après l'ouverture)withoutFoldersappliqué au seul nom de copie, pas à la ligneif final_path.exists() { return Ok(name) }match, donc aussi sur erreurcatch_up_backuprempli au seul succès.db.tmpLectures vérifiées
remove_backupsfiltre sur le préfixe<stem>-before-v, et les ids sont hexadécimaux (is_profile_db_filename). Un profil dont l'id est le préfixe d'un autre ne peut donc pas élaguer les copies de l'autre.thread_localFAIL_STEPest compilé sous#[cfg(test)]seulement, etprepare_atne lance aucun fil : l'injection reste propre au test qui la pose.App.tsxtestestartupErroravantdbReady. Remettre l'erreur ànullsuffit donc à afficher l'application.prepare_atpassent déjà parscrubcôté Rust. Côté TypeScript,withoutFolderssert de défense en profondeur.Suggestions (non bloquantes)
withoutFolderslaisse passer le premier mot d'un dossier qui contient une espace. Sonde sur la tête :C:\Users\Jean Dupont\AppData\Roaming\com.simpl.resultat\profile_x.dbdonneJean profile_x.db;/home/marie claire/.config/x.dbdonnemarie x.db.Le critère « aucun
/ni\» tient, mais un morceau du nom d'utilisateur reste dans la ligne (Loi 25). Ce n'est pas une régression : avant la PR, le message de repli n'était pas nettoyé du tout, et Rust remplace déjà le dossier des bases par<profile folder>. En revanche, la phrase de l'ADR 0021 §7, « ne garde que le dernier segment de tout chemin », est inexacte dans ce cas. Le dire dans l'ADR et ajouter une épingle, ou traiter le cas en suivi (par exemple, remplacer d'abord les préfixes connusC:\Users\<…>\et/home/<…>/jusqu'au séparateur suivant).Détail d'affichage :
'C:\Users\Bob\x.db'devientx.db'. L'apostrophe ouvrante disparaît avec le segment, la fermante reste.Fermeture de #395 : le corps de la PR ne porte aucun mot-clé de fermeture, et la timeline de #395 montre
ref_action: nonepour #436. Les deux commits portent en revancheResolves #395, donc l'issue se fermera quand ces commits arriveront surmain(merge ff de la pile). C'est cohérent avec le reste de la pile, et le signaler suffit.Actions faites
status:reviewremplacé parstatus:approvedsur #395.scratchpad/pr436, cible Cargoscratchpad/ctarget) supprimée en fin de revue.Suivis des quatre revues, appliqués en tête de pile
Quatre commits ajoutés sur
issue-395-catch-up-followupsaprèsfbecfff, un par PR de la pile. Les têtes revues des PR #433 (b161d54), #434 (79ba1ed) et #435 (2bd347b) n'ont pas bougé. Ces commits ont été ajoutés après les revues et ne sont pas relus.74fae14nulldelockHolder. S3 :close()sur un proxy degetDb()passe par une section « connection close » et vérifie sa génération ; un ancien proxy rejette avecDbConnectionClosedErrorau lieu de fermer le profil courant. S4 :closeUnlockedne rejette plus. L'état est vidé et la génération incrémentée avant leclose(), et un échec part au Journal (logWarn).connectToProfileouvre quand même le profil suivant, etcloseDb()se résout sans connexion :switchProfileappellecloseDb()avantconnectToProfile, donc un rejet aurait laissé l'app sans connexion. S5 : l'alerte cite legetDb()dans un rappel comme « possible cause ». S2 : le contrôle de génération du handle de transaction est commenté comme une défense, inatteignable aujourd'hui.docs/architecture.mdest mis à jour.a1c31a1LOAD_START). Le commentaire « one subscription only » renvoie à ce test. S2 (M8) : un tableau de dépendances qui raccourcit, ou s'allonge d'unundefined, recalcule le memo. S3 : ledispatchd'unuseReducergarde le reducer du premier rendu, ajouté à « NOT SIMULATED » danshookHarness.tset dansdocs/architecture.md.ac3a981renderCategoryLabel.test.tspassemarkedT, et ses assertions sont sous forme marquée. S2 : la garde reconnaît aussit = (,t = vi.fn(ett: vi.fn(, et exige un appeli18nMock(, commentaires retirés avant la recherche. Avant la migration, la règle de liaison a trouvérenderCategoryLabel.test.ts:13sur l'arbre réel. La phrase « seul fauxt» dedocs/architecture.mdest restreinte aux fichiers de test, et la description de la garde est mise à jour.86eff9bwithoutFoldersréduit un chemin dont les dossiers contiennent des espaces à son dernier segment :C:\Users\Jean Dupont\…\profile_x.dbdonneprofile_x.db,/home/marie claire/.config/x.dbdonnex.db. Un chemin part d'un début reconnu (lecteur,/,~/,./,\\,<profile folder>) et court jusqu'à son dernier séparateur. Il s'arrête à un saut de ligne, à", à une apostrophe inverse, à;, à|, à,ou:suivis d'un blanc, à une)non suivie d'un séparateur, ou à un blanc suivi d'un autre chemin. Règle prudente : quand rien n'arrête le chemin, le texte est retiré jusqu'au dernier séparateur de la ligne (/a/x.db then I/OdonneO), plutôt que de laisser passer un dossier. S2 :'C:\Users\Bob\x.db'donne'x.db'. Limite : un chemin relatif sans début reconnu garde l'ancien repli, qui remplace les séparateurs par-. Aucune ligne d'erreur ne contient/ni\, comme avant. La phrase de l'ADR 0021 §7 est réécrite. Dans les deux CHANGELOG, la ligne vide entre les deux puces Fixed / Corrigé est retirée.Mutations (une à la fois, fichier restauré et vérifié après chaque passe)
lockHolderjamais remis ànullclose()d'un proxy degetDb()sans contrôle de générationcloseUnlockedrelance l'échec duclose()close()réussipathnameajouté aux dépendances du suiveurdepsChangedretiréerenderCategoryLabelsautetrenderCategoryLabelne passe plusdefaultValuet: (seulet: vi.fn(non reconnut = (non reconnui18nMocksuffit (ancienne règle)fbecfff),ou:))arrête (Program Files (x86))"retiré des arrêtsI/O)Une mutation a survécu : retirer le traitement de l'apostrophe fermante. L'apostrophe ouvrante ne fait plus partie du chemin, donc ce code était redondant, et je l'ai retiré plutôt que de garder une ligne qu'aucun test n'épingle. Le cas
'C:\Users\Bob\x.db'→'x.db'reste testé. Le\rdes arrêts n'a pas de test propre.Validation sur
86eff9b(worktree,npm cipropre)npm run build: vert.npm test: 119 fichiers, 2065 tests verts (2039 surfbecfff).cargo testni decargo check.Non traité, comme demandé : le badge
best-effortdePriceFetchControl.tsx(point 4 de la revue de la PR #435).The small badge next to "Fetch price" on a stock rendered the literal "best-effort" without going through t, so French users saw English. It now reads balance.priceFetching.bestEffortBadge ("non garanti" in French). Raised by the review of PR #435 (suggestion 4). Refs #389 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Un cinquième commit s’ajoute aux suivis du commentaire 23535 :
c17bf5d, « fix(balance): translate the best-effort badge of the price fetch button » (Refs #389). C’est la suggestion 4 de la revue de #435, que j’avais écartée à tort (voir la correction sur #435).Le badge passe par
t("balance.priceFetching.bestEffortBadge"), avec une entrée Fixed / Corrigé dans les deux CHANGELOG. La mutation qui remet le littéral fait rougir 1 test. Le worktree donne build vert et vitest119 passed (119),2065 passed (2065).Tête de pile :
c17bf5d. Comme les quatre autres, ce commit n’est pas relu.Pile mergée le 2026-10-13 en avance rapide par l’API, du bas vers le haut, chaque enfant reciblé sur
mainjuste avant son merge épinglé : #433 (b161d54) → #434 (79ba1ed) → #435 (2bd347b) → #436 (c17bf5d).Tip validé avant le merge dans un worktree détaché, après
npm cipropre : la sonde rouge sort en rc=1,tscetnpm run buildpassent, vitest donne119 passed (119)et2065 passed (2065),cargo testdonne 262 réussis et 1 ignoré. CI de la tête : runs 541 et 542.Les commits
74fae14,a1c31a1,ac3a981,86eff9betc17bf5dont été ajoutés après les revues et ne sont pas relus. Leur relecture est le point 1 de #437.