test: corpus de fixtures CSV et contrat de detection #333

Closed
maximus wants to merge 0 commits from issue-326-corpus-fixtures-csv into main
Owner

Premier maillon de la pile import-CSV (#326 -> #323 -> #324 -> #325 -> #327 -> ... -> #332). Base main, aucune dependance.

Ce que fige cette PR

autoDetectConfig, detectAmountMode, detectSingleAmount, preprocessQuotedCSV et parseFrenchAmount n'avaient aucun test sur les 871 du projet, alors qu'ils portent chaque montant importe. Cette PR pose la reference contre laquelle la refonte se mesurera, avant qu'elle ne bouge.

Aucun fichier de production n'est modifie. csvAutoDetect.ts et amountParser.ts sont identiques a main.

Corpus — src/__fixtures__/csv/ (11 fichiers)

Synthetiques, aucune donnee reelle. Ils couvrent des formes qu'un vrai releve ne contient jamais toutes a la fois, ce qui est l'interet :

Fixture Etat fige
signed-amount.csv OK
debit-credit.csv OK
debit-credit-reversed.csv DEFAILLANT
unused-column-zero.csv DEFAILLANT
preamble.csv OK (skipLines: 3)
header-with-number.csv OK
header-numeric-label.csv DEFAILLANT
no-header.csv OK
all-positive.csv DEFAILLANT
desjardins-quoted.csv OK
absolute-indicator.csv DEFAILLANT

Les cas defaillants, marques explicitement

Chaque describe porte KNOWN DEFECT + l'issue qui doit le corriger ; chaque assertion porte // DEFECT — should be X. Un grep "KNOWN DEFECT" enumere l'ensemble.

  1. Ordre debit/credit devine par position (#328) — detectAmountMode affecte la colonne la plus a gauche a debitAmount, sans jamais lire les libelles. Sur Date;Description;Credit;Debit, tous les signes sont inverses — et le total se contente de changer de signe, donc aucun controle agrege ne l'attrape.
  2. Colonne inutilisee a 0,00 (#325) — la detection du mode est correcte ; c'est le parsing qui casse. La regle branche sur isNaN(credit) et "0,00" rend 0, pas NaN : chaque debit s'importe a 0, seuls les credits survivent.
  3. En-tete commencant par des chiffres (#328/#325) — parseFloat rend le prefixe numerique, hasNumber bascule, hasHeader devient false et la ligne d'en-tete part en ligne de donnees.
  4. Montant absolu + indicateur D/C (#328) — la colonne d'indicateur est ignoree, positive_expense est deduit, chaque credit s'importe en depense. Un test montre que ce fichier est aujourd'hui indiscernable de all-positive — c'est le discriminant que #328 doit poser.

Points a savoir pour la revue

Un 11e fixture a ete ajoute. Le corps de l'issue nomme « en-tete contenant un nombre (« Solde 2024 ») ». Mesure : ce cas n'est pas defaillantparseFrenchAmount("Solde 2024") rend NaN, parseFloat s'arretant sur le S initial, donc detectHeader repond correctement. Le defaut exige une cellule qui commence par des chiffres. header-with-number.csv est donc conserve tel que l'issue le nomme et fige comme fonctionnel (avec le commentaire expliquant pourquoi il survit), et header-numeric-label.csv est ajoute pour le cas qui echoue vraiment. Sans lui, le vrai defaut serait « silencieusement absent », ce que le critere d'acceptation interdit.

Les tests bout-en-bout rejouent un MIROIR de la regle du wizard. mapRow n'existe pas encore (c'est #325 qui l'extrait), la regle vit dans un useCallback de useImportWizard.parseFilesInternal (:489-542) et le depot n'a pas de jsdom. La copie est donc locale au fichier de test — et gardee : un test statique relit useImportWizard.ts et verifie que les 5 expressions de production y sont toujours litteralement presentes (meme pattern que __integration__/transactions-transfer-icon.test.ts). Quand #325 les changera, le garde tombera bruyamment, et son commentaire dit au worker suivant de supprimer le miroir et d'importer le vrai mapRow.

Trouvaille hors perimetre nomme. La revision /review-spec demandait d'etendre les assertions aux sites d'appel holdings. Ce faisant : une colonne de prix portant son code de devise ("150,25 CAD") enregistre la position a 15025 dans l'import de titres #245 — meme erreur de facteur 100, silencieuse, et isNaN la laisse passer. Figee dans amountParser.test.ts, a corriger par #325 (durcissement global, decision tranchee du plan).

Methode. Chaque valeur attendue vient de l'execution du code, pas de sa lecture — un probe jetable a vide autoDetectConfig + la regle rejouee sur chaque fixture, puis les assertions ont ete ecrites depuis cette sortie. Deux attentes auraient ete fausses en devinant (header-with-number et le fait que unused-column-zero detecte bien son mode).

Verification

  • npm test924 passed (55 fichiers), contre 871 avant. +53 tests.
  • npm run build — tsc + vite propres.
  • Aucune migration DB, aucune cle i18n, aucun changement user-facing → pas d'entree CHANGELOG (centralisee dans le dernier maillon, #332).

Resolves #326


Generated autonomously by /autopilot run of 2026-08-13

Premier maillon de la pile import-CSV (#326 -> #323 -> #324 -> #325 -> #327 -> ... -> #332). Base `main`, aucune dependance. ## Ce que fige cette PR `autoDetectConfig`, `detectAmountMode`, `detectSingleAmount`, `preprocessQuotedCSV` et `parseFrenchAmount` n'avaient **aucun test** sur les 871 du projet, alors qu'ils portent chaque montant importe. Cette PR pose la reference contre laquelle la refonte se mesurera, **avant** qu'elle ne bouge. **Aucun fichier de production n'est modifie.** `csvAutoDetect.ts` et `amountParser.ts` sont identiques a `main`. ## Corpus — `src/__fixtures__/csv/` (11 fichiers) Synthetiques, aucune donnee reelle. Ils couvrent des formes qu'un vrai releve ne contient jamais toutes a la fois, ce qui est l'interet : | Fixture | Etat fige | |---|---| | `signed-amount.csv` | OK | | `debit-credit.csv` | OK | | `debit-credit-reversed.csv` | **DEFAILLANT** | | `unused-column-zero.csv` | **DEFAILLANT** | | `preamble.csv` | OK (`skipLines: 3`) | | `header-with-number.csv` | OK | | `header-numeric-label.csv` | **DEFAILLANT** | | `no-header.csv` | OK | | `all-positive.csv` | **DEFAILLANT** | | `desjardins-quoted.csv` | OK | | `absolute-indicator.csv` | **DEFAILLANT** | ## Les cas defaillants, marques explicitement Chaque `describe` porte `KNOWN DEFECT` + l'issue qui doit le corriger ; chaque assertion porte `// DEFECT — should be X`. Un `grep "KNOWN DEFECT"` enumere l'ensemble. 1. **Ordre debit/credit devine par position** (#328) — `detectAmountMode` affecte la colonne la plus a gauche a `debitAmount`, sans jamais lire les libelles. Sur `Date;Description;Credit;Debit`, **tous les signes sont inverses** — et le total se contente de changer de signe, donc aucun controle agrege ne l'attrape. 2. **Colonne inutilisee a `0,00`** (#325) — la detection du mode est correcte ; c'est le parsing qui casse. La regle branche sur `isNaN(credit)` et `"0,00"` rend 0, pas NaN : **chaque debit s'importe a 0**, seuls les credits survivent. 3. **En-tete commencant par des chiffres** (#328/#325) — `parseFloat` rend le prefixe numerique, `hasNumber` bascule, `hasHeader` devient `false` et la ligne d'en-tete part en ligne de donnees. 4. **Montant absolu + indicateur D/C** (#328) — la colonne d'indicateur est ignoree, `positive_expense` est deduit, **chaque credit s'importe en depense**. Un test montre que ce fichier est aujourd'hui *indiscernable* de `all-positive` — c'est le discriminant que #328 doit poser. ## Points a savoir pour la revue **Un 11e fixture a ete ajoute.** Le corps de l'issue nomme « en-tete contenant un nombre (« Solde 2024 ») ». Mesure : **ce cas n'est pas defaillant** — `parseFrenchAmount("Solde 2024")` rend NaN, `parseFloat` s'arretant sur le `S` initial, donc `detectHeader` repond correctement. Le defaut exige une cellule qui *commence* par des chiffres. `header-with-number.csv` est donc conserve tel que l'issue le nomme et fige comme **fonctionnel** (avec le commentaire expliquant pourquoi il survit), et `header-numeric-label.csv` est ajoute pour le cas qui echoue vraiment. Sans lui, le vrai defaut serait « silencieusement absent », ce que le critere d'acceptation interdit. **Les tests bout-en-bout rejouent un MIROIR de la regle du wizard.** `mapRow` n'existe pas encore (c'est #325 qui l'extrait), la regle vit dans un `useCallback` de `useImportWizard.parseFilesInternal` (`:489-542`) et le depot n'a pas de jsdom. La copie est donc locale au fichier de test — et **gardee** : un test statique relit `useImportWizard.ts` et verifie que les 5 expressions de production y sont toujours litteralement presentes (meme pattern que `__integration__/transactions-transfer-icon.test.ts`). Quand #325 les changera, le garde tombera bruyamment, et son commentaire dit au worker suivant de supprimer le miroir et d'importer le vrai `mapRow`. **Trouvaille hors perimetre nomme.** La revision `/review-spec` demandait d'etendre les assertions aux sites d'appel holdings. Ce faisant : une colonne de prix portant son code de devise (`"150,25 CAD"`) enregistre la position a **15025** dans l'import de titres #245 — meme erreur de facteur 100, silencieuse, et `isNaN` la laisse passer. Figee dans `amountParser.test.ts`, a corriger par #325 (durcissement global, decision tranchee du plan). **Methode.** Chaque valeur attendue vient de l'execution du code, pas de sa lecture — un probe jetable a vide `autoDetectConfig` + la regle rejouee sur chaque fixture, puis les assertions ont ete ecrites depuis cette sortie. Deux attentes auraient ete fausses en devinant (`header-with-number` et le fait que `unused-column-zero` detecte bien son mode). ## Verification - `npm test` — **924 passed** (55 fichiers), contre 871 avant. +53 tests. - `npm run build` — tsc + vite propres. - Aucune migration DB, aucune cle i18n, aucun changement user-facing → pas d'entree CHANGELOG (centralisee dans le dernier maillon, #332). Resolves #326 --- Generated autonomously by /autopilot run of 2026-08-13
maximus added 1 commit 2026-08-13 16:34:39 +00:00
test: freeze CSV detection and amount parsing behaviour on a fixture corpus
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m42s
6c95e61d7f
First link of the import-format stack. `autoDetectConfig`, `detectAmountMode`,
`detectSingleAmount`, `preprocessQuotedCSV` and `parseFrenchAmount` had no test
at all on a suite of 871, while carrying every imported amount. This adds the
reference the rewrite (#323-#332) measures itself against, before any of it
moves.

Corpus — 11 synthetic files under `src/__fixtures__/csv/`, no real statement
data, covering shapes a single real statement never contains at once: signed
amount, debit/credit, debit/credit in reversed column order, unused column
filled with `0,00`, preamble before the header, header carrying a number,
header cell starting with digits, no header row, all-positive amounts,
whole-line-quoted (Desjardins style), absolute amount + D/C indicator.

Every expectation was derived by running the code, not by reading it. Four
cases are frozen as DEFECTIVE, each named `KNOWN DEFECT` with the right answer
in a comment and the issue that owes the fix:

- reversed debit/credit — the pair is assigned by column position, never by
  label, so every sign is inverted while the total merely negates (#328)
- unused column at `0,00` — the rule branches on `isNaN(credit)` and `"0,00"`
  parses to 0, so every debit imports as zero (#325)
- header cell starting with digits — `parseFloat` returns the numeric prefix,
  `detectHeader` reads the header as data (#328/#325)
- absolute amount + D/C indicator — the indicator column is ignored entirely
  and every credit imports as an expense (#328)

`parseFrenchAmount` is pinned form by form, including the prefix-scan defect
the spec flagged: `"100,00 CAD"` yields 10000 and passes `isNaN`. Per the
/review-spec revision, the assertions reach the holdings call sites too, which
surfaced the same x100 leak in the #245 holdings import — a price cell of
`"150,25 CAD"` stores the position at 15025.

The end-to-end tests replay the wizard's row-mapping rule from a mirror, since
it still lives inside a `useCallback` and the repo has no jsdom. A guard test
asserts the production expressions are still literally present, so the mirror
cannot drift; #325 extracts `mapRow` and must then delete it.

No production file is modified. 924 vitest green (was 871), build clean.

Resolves #326
maximus added the
autopilot:pending-human
label 2026-08-13 16:34:49 +00:00
maximus force-pushed issue-326-corpus-fixtures-csv from 6c95e61d7f to c88ebd862e 2026-08-13 16:36:28 +00:00 Compare
Author
Owner

/pr-review — APPROVE

Resume. PR test-only exemplaire : elle fige par caracterisation cinq fonctions qui portaient chaque montant importe sans avoir une seule assertion, et elle marque explicitement les comportements geles comme fautifs plutot que de les blanchir. Chaque affirmation a ete re-verifiee contre le code de production — toutes tiennent, y compris les deux que la revue aurait pu contester.

Verifications faites (pas seulement lues)

Controle Resultat
csvAutoDetect.ts / amountParser.ts intacts Confirme — les 14 fichiers du diff sont fixtures + .test.ts
Miroir vs useImportWizard.ts:489-542 Fidele, y compris Papa.parse(preprocessed, { delimiter, skipEmptyLines: true })
Les 5 sous-chaines du garde statique Toutes presentes sur une seule ligne dans la source
Ordre debit/credit positionnel (#328) detectAmountMode boucle a < b, retourne debitCol: colA sans lire aucun libelle
positive_expense par absence de negatifs (#329) negCount / total > 0.5, aucun autre signal
detectHeader aveugle Bascule sur !isNaN(parseFrenchAmount(trimmed)) seul
Facteur x100 Trace a la main : "100,00 CAD" -> "10000CAD" -> parseFloat = 10000
CI (log du run 357, pas le chrono) tsc && vite build execute ligne 486 ; 55 fichiers / 924 tests passes — colle au corps de la PR (871 + 53)
.skip / .only Aucun

Deux points ou la PR a raison contre l'intuition

C'est ce qui la credibilise : header-with-number (« Solde 2024 ») n'est pas defaillant — parseFloat s'arrete sur le S — et le 11e fixture ajoute pour le vrai cas (header-numeric-label) est la bonne reponse plutot que de faire passer l'issue pour satisfaite. Idem pour unused-column-zero, ou la detection du mode est correcte et seul le parsing casse : la PR ne confond pas les deux etages.

Suggestions non bloquantes

Aucune ne justifie de retenir le merge.

  1. src/__fixtures__/csv/index.ts:113fs / path / import.meta.dirname ne type-checkent que parce que @types/node@25.2.1 est installe transitivement ; il n'est pas declare en devDependencies. Le precedent transactions-transfer-icon.test.ts:38 fait deja pareil, donc l'exposition n'est pas neuve — mais la declarer explicitement coute une ligne et evite un « Cannot find module 'fs' » incomprehensible le jour ou une dependance la lache.
  2. csvAutoDetect.test.ts:563 — le garde statique casse sur un simple reflow Prettier, avec un message qui ne dit rien du formatage. Le commentaire l'explique deja ; a garder en tete pour #325.
  3. mapCorpusRow omet le try/catch que la production enroule autour du mapping. Sans effet sur le corpus actuel, mais si #325 rend mapRow capable de lever, le miroir plantera le test la ou la production emettrait une ligne d'erreur.
  4. csvAutoDetect.test.ts:837 — « A semicolon file can never match it » est une sur-affirmation : un fichier ; dont un champ contient ,"" matcherait. Contrive, mais un futur lecteur fera confiance a l'absolu.

Review adversariale via /pr-review.

## /pr-review — APPROVE **Resume.** PR test-only exemplaire : elle fige par caracterisation cinq fonctions qui portaient chaque montant importe sans avoir une seule assertion, et elle marque explicitement les comportements geles comme fautifs plutot que de les blanchir. Chaque affirmation a ete re-verifiee contre le code de production — toutes tiennent, y compris les deux que la revue aurait pu contester. ### Verifications faites (pas seulement lues) | Controle | Resultat | |---|---| | `csvAutoDetect.ts` / `amountParser.ts` intacts | Confirme — les 14 fichiers du diff sont fixtures + `.test.ts` | | Miroir vs `useImportWizard.ts:489-542` | Fidele, y compris `Papa.parse(preprocessed, { delimiter, skipEmptyLines: true })` | | Les 5 sous-chaines du garde statique | Toutes presentes sur une seule ligne dans la source | | Ordre debit/credit positionnel (#328) | `detectAmountMode` boucle `a < b`, retourne `debitCol: colA` sans lire aucun libelle | | `positive_expense` par absence de negatifs (#329) | `negCount / total > 0.5`, aucun autre signal | | `detectHeader` aveugle | Bascule sur `!isNaN(parseFrenchAmount(trimmed))` seul | | Facteur x100 | Trace a la main : `"100,00 CAD"` -> `"10000CAD"` -> `parseFloat` = 10000 | | CI (log du run 357, pas le chrono) | `tsc && vite build` execute ligne 486 ; **55 fichiers / 924 tests passes** — colle au corps de la PR (871 + 53) | | `.skip` / `.only` | Aucun | ### Deux points ou la PR a raison contre l'intuition C'est ce qui la credibilise : `header-with-number` (« Solde 2024 ») n'est **pas** defaillant — `parseFloat` s'arrete sur le `S` — et le 11e fixture ajoute pour le vrai cas (`header-numeric-label`) est la bonne reponse plutot que de faire passer l'issue pour satisfaite. Idem pour `unused-column-zero`, ou la detection du mode est correcte et seul le parsing casse : la PR ne confond pas les deux etages. ### Suggestions non bloquantes Aucune ne justifie de retenir le merge. 1. `src/__fixtures__/csv/index.ts:113` — `fs` / `path` / `import.meta.dirname` ne type-checkent que parce que `@types/node@25.2.1` est installe **transitivement** ; il n'est pas declare en `devDependencies`. Le precedent `transactions-transfer-icon.test.ts:38` fait deja pareil, donc l'exposition n'est pas neuve — mais la declarer explicitement coute une ligne et evite un « Cannot find module 'fs' » incomprehensible le jour ou une dependance la lache. 2. `csvAutoDetect.test.ts:563` — le garde statique casse sur un simple reflow Prettier, avec un message qui ne dit rien du formatage. Le commentaire l'explique deja ; a garder en tete pour #325. 3. `mapCorpusRow` omet le `try/catch` que la production enroule autour du mapping. Sans effet sur le corpus actuel, mais si #325 rend `mapRow` capable de lever, le miroir plantera le test la ou la production emettrait une ligne d'erreur. 4. `csvAutoDetect.test.ts:837` — « A semicolon file can never match it » est une sur-affirmation : un fichier `;` dont un champ contient `,""` matcherait. Contrive, mais un futur lecteur fera confiance a l'absolu. --- *Review adversariale via `/pr-review`.*
Author
Owner

Mergée dans main en fast-forward avec le reste de la pile (tip 37b832e).

Forgejo ne détecte pas un merge local comme merged — la PR est donc fermée à la main, et l'issue liée s'est fermée automatiquement via son Resolves #N.

Tip cumulé validé avant push : 1181 vitest, 111 tests Rust, build tsc + vite. La CI ne tourne pas sur push main, cette validation locale était donc le seul filet.

Mergée dans `main` en fast-forward avec le reste de la pile (tip `37b832e`). Forgejo ne détecte pas un merge local comme *merged* — la PR est donc fermée à la main, et l'issue liée s'est fermée automatiquement via son `Resolves #N`. Tip cumulé validé avant push : **1181 vitest**, **111 tests Rust**, build tsc + vite. La CI ne tourne pas sur push `main`, cette validation locale était donc le seul filet.
maximus closed this pull request 2026-08-14 16:13:56 +00:00
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m43s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/Simpl-Resultat#333
No description provided.