test(i18n): one marked fake t for the suite, and a guard (#389) #435

Merged
maximus merged 4 commits from issue-389-i18n-mock into main 2026-10-14 00:28:40 +00:00
Owner

Generated autonomously by /autopilot run of 2026-10-12

Issue #389. Stacked on PR #434 (#409), itself on PR #433 (#408). Base: issue-409-hook-harness.

Summary

One fake t for the whole test suite: markedT in src/test/i18nMock.ts renders t(key) or t(key[a=1,b=2]), and the factory i18nMock({ language?, fixture? }) builds the react-i18next module for vi.mock. All 27 test files that mocked react-i18next use it, and a source guard refuses any local fake t. No CHANGELOG entry (tests only), no app code changed.

Commits (review order)

  1. test(i18n): add the shared marked fake t... : module, its self-test, echo of useLicenseMachines.test.ts replaced by markedT.
  2. test(i18n): move the raw fake t files... : the 15 raw files, assertions on displayed texts rewritten in marked form, render assertions added where a component had none.
  3. test(i18n): replace the local marked fake t copies... : the 12 marked files, copies deleted, assertions unchanged.
  4. test(i18n): guard the test sources... : src/__integration__/i18n-mock-guard.test.ts + the convention in docs/architecture.md (## Tests (vitest sans jsdom)).

Inventory drift (re-verified by script on 79ba1ed)

27 files, not 24: the 12 raw and 12 marked of the issue are exact, plus 3 raw files added by #423 after the 2026-10-10 inventory (AddKeywordDialog.render, CategoryDetailPanel.render, CategoryForm.render). They are migrated with the raw files (the criterion "one fake t" and the guard need it).

Files

  • New: src/test/i18nMock.ts, src/test/i18nMock.test.ts, src/__integration__/i18n-mock-guard.test.ts
  • src/hooks/useLicenseMachines.test.ts
  • Raw (15): PriceFetchControl, AddKeywordDialog.render, CategoryDetailPanel.render, CategoryForm.render, OnboardingChecklist, ImportEmptyState, SourceList, WizardNavigation, FilterPanel, DocsSubsections, LicenseKeyForm.render, PriceFetchConsentToggle, UpsellCtas.render, TransactionTable.render, DashboardPage.render
  • Marked (12): BudgetTable.render, BudgetTable.wiring, DashboardEmptyState.render, AppShell.render, OnboardingBanner.render, StepCard.render, LicenseCard.render, BudgetPage.render, BudgetPage.wiring, ImportPage.render, UsersSettingsPage, TransactionsPage.render
  • docs/architecture.md

Decisions

  • [MEDIUM] Drift of 3 files included (above).
  • [MEDIUM] Guard keywords anchored on a word boundary: the literal t: ( matches useLicenseContext: () =>, get: (target, select: (q, isParent: (r, useEffect: (fn in 9 files with no fake t. A third check: a vi.mock("react-i18next", ...) must go through i18nMock.
  • [MEDIUM] Assertions added where a component displayed nothing under test: PriceFetchControl (never rendered before), PriceFetchConsentToggle (renders null while loading under SSR, so rendered through the #409 harness with its loading slot preset), CategoryDetailPanel, SourceList + SourceCard, the region aria-label of OnboardingChecklist. UpsellCtas.render covers four components: one mutation each.
  • [HIGH] DocsSubsections: FIXTURE in vi.hoisted (the factory reads it at load); language as a getter function for LicenseCard and UpsellCtas; PriceFetchControl's own key({json}) format replaced by the mark, its fixture i18nKey data left bare.

Proof: one t removed from each component (exit code of npx vitest run <test file>)

Component Mutation Exit Red test
balance/PriceFetchControl.tsx {t("balance.priceFetching.button")} → {"balance.priceFetching.button"} 1 PriceFetchControl — displayed texts > names the fetch button and the stock best-effort notice through t
categories/AddKeywordDialog.tsx placeholder={t("transactions.table.noCategory")} → bare key 1 AddKeywordDialog — category field (#423) > starts empty, with the 'no category' placeholder, until the list is in
categories/CategoryDetailPanel.tsx {t("categories.editCategory")} → bare key 1 CategoryDetailPanel — displayed texts (#389) > titles the edit view and the create view through t
categories/CategoryForm.tsx ariaLabel={t("categories.parent")} → bare key 1 CategoryForm — parent field (#423) > renders the parent as a combobox labelled by the field name, with no parent select left
dashboard/OnboardingChecklist.tsx h3 {t("onboarding.checklist.title")} → bare key 1 OnboardingChecklist > renders the title, the counter 1 / 6 and the hide button
dashboard/OnboardingChecklist.tsx aria-label={t("onboarding.checklist.title")} → bare key 1 same test (assertion added)
import/ImportEmptyState.tsx {t("import.empty.guideLink")} → bare key 1 ImportEmptyState — no folder (#356) > offers one primary 'choose a folder' button wired to onBrowse, and the guide link
import/SourceList.tsx {t("import.sources.title")} → bare key 1 SourceList and SourceCard — displayed texts (#389) > titles the list and counts each source's files through t
import/SourceCard.tsx {t("import.sources.new")} → bare key 1 SourceList and SourceCard — displayed texts (#389) > marks the files not yet imported through t
import/WizardNavigation.tsx {t("common.cancel")} → bare key 1 WizardNavigation — getting-started target (#398) > highlights the next button, and neither back nor cancel
reports/FilterPanel.tsx t("reports.filters.accounts.label") → bare key 1 FilterPanel — render > uses i18n keys for the accounts filter label and hint
settings/DocsSubsections.tsx t(`docs.${sectionKey}.subsections`, { returnObjects: true }) → the bare template 1 DocsSubsections > renders one block per entry with its id and registers each element
settings/LicenseKeyForm.tsx {t(error.i18nKey, error.params)} → {error.i18nKey} 1 LicenseKeyForm > names a refused key in the app language
settings/PriceFetchConsentToggle.tsx {t("settings.privacy.title")} → bare key 1 PriceFetchConsentToggle — displayed texts > names the section, the consent row and the not-premium tooltip through t
shared/UpsellGate.tsx {t("upsell.revoked")} → bare key 1 UpsellGate with a revoked key (#365) > names the revocation between the feature description and the CTAs
shared/PurchaseButton.tsx tier: t("license.editions.base") → bare key 1 UpsellGate with a revoked key (#365) > names the revocation... (4 red)
shared/HaveKeyButton.tsx {t("upsell.ctaHaveKey")} → bare key 1 UpsellGate with a revoked key (#365) > names the revocation... (3 red)
shared/DownloadLatestButton.tsx {t("settings.updates.downloadLatest")} → bare key 1 UpdateCard in the free edition > offers Get Base, I already have a key and Download the latest version
transactions/TransactionTable.tsx <p>{t("transactions.noTransactions")}</p> → bare key 1 TransactionTable — getting-started target (#399) > highlights nothing without a row: the card stays, without a ring
pages/DashboardPage.tsx {t("dashboard.title")} → bare key 1 DashboardPage on the shared path (#352, #396, #397) > renders only its header while the path first loads

The first OnboardingChecklist mutation survived (exit 0): it hit the region aria-label, which nothing asserted. Assertion added, both mutations now red. Every mutation run once, file restored by git checkout.

Guard proven red (exit 1) twice: a raw local mock put back in WizardNavigation.test.tsx (2 findings: the useTranslation: line, and the mock without i18nMock), and t: markedT replaced by t: (k: string) => k in useLicenseMachines.test.ts (1 finding). The guard also has its own unit test on samples (local fake found, useLicenseContext: () => / get: (target not found).

Checks

  • npm run build green; npm test 118 files / 2026 tests (base 116 / 2009).
  • Diff: tests and docs only, no SQL migration, no UI text, no CHANGELOG (tests only, per the issue).

For the reviewer

  • Out of scope finding: PriceFetchControl.tsx renders a hard-coded best-effort badge, without t (i18n rule). Not fixed here (tests-only PR).
  • PriceFetchConsentToggle.test.tsx now mocks react with the harness for the whole file; its older tests do not render and stay green (some of them assert local literals only, untouched).
  • Four former marked files (BudgetTable.*, BudgetPage.wiring, UsersSettingsPage) ignored options; markedT now prints them. No assertion needed a change.

🤖 Generated with Claude Code

Generated autonomously by /autopilot run of 2026-10-12 Issue #389. Stacked on PR #434 (#409), itself on PR #433 (#408). Base: `issue-409-hook-harness`. ## Summary One fake `t` for the whole test suite: `markedT` in `src/test/i18nMock.ts` renders `t(key)` or `t(key[a=1,b=2])`, and the factory `i18nMock({ language?, fixture? })` builds the `react-i18next` module for `vi.mock`. All 27 test files that mocked `react-i18next` use it, and a source guard refuses any local fake `t`. No CHANGELOG entry (tests only), no app code changed. ## Commits (review order) 1. `test(i18n): add the shared marked fake t...` : module, its self-test, `echo` of `useLicenseMachines.test.ts` replaced by `markedT`. 2. `test(i18n): move the raw fake t files...` : the 15 raw files, assertions on displayed texts rewritten in marked form, render assertions added where a component had none. 3. `test(i18n): replace the local marked fake t copies...` : the 12 marked files, copies deleted, assertions unchanged. 4. `test(i18n): guard the test sources...` : `src/__integration__/i18n-mock-guard.test.ts` + the convention in `docs/architecture.md` (`## Tests (vitest sans jsdom)`). ## Inventory drift (re-verified by script on `79ba1ed`) 27 files, not 24: the 12 raw and 12 marked of the issue are exact, plus 3 raw files added by #423 after the 2026-10-10 inventory (`AddKeywordDialog.render`, `CategoryDetailPanel.render`, `CategoryForm.render`). They are migrated with the raw files (the criterion "one fake t" and the guard need it). ## Files - New: `src/test/i18nMock.ts`, `src/test/i18nMock.test.ts`, `src/__integration__/i18n-mock-guard.test.ts` - `src/hooks/useLicenseMachines.test.ts` - Raw (15): `PriceFetchControl`, `AddKeywordDialog.render`, `CategoryDetailPanel.render`, `CategoryForm.render`, `OnboardingChecklist`, `ImportEmptyState`, `SourceList`, `WizardNavigation`, `FilterPanel`, `DocsSubsections`, `LicenseKeyForm.render`, `PriceFetchConsentToggle`, `UpsellCtas.render`, `TransactionTable.render`, `DashboardPage.render` - Marked (12): `BudgetTable.render`, `BudgetTable.wiring`, `DashboardEmptyState.render`, `AppShell.render`, `OnboardingBanner.render`, `StepCard.render`, `LicenseCard.render`, `BudgetPage.render`, `BudgetPage.wiring`, `ImportPage.render`, `UsersSettingsPage`, `TransactionsPage.render` - `docs/architecture.md` ## Decisions - [MEDIUM] Drift of 3 files included (above). - [MEDIUM] Guard keywords anchored on a word boundary: the literal `t: (` matches `useLicenseContext: () =>`, `get: (target`, `select: (q`, `isParent: (r`, `useEffect: (fn` in 9 files with no fake `t`. A third check: a `vi.mock("react-i18next", ...)` must go through `i18nMock`. - [MEDIUM] Assertions added where a component displayed nothing under test: `PriceFetchControl` (never rendered before), `PriceFetchConsentToggle` (renders null while loading under SSR, so rendered through the #409 harness with its `loading` slot preset), `CategoryDetailPanel`, `SourceList` + `SourceCard`, the region `aria-label` of `OnboardingChecklist`. `UpsellCtas.render` covers four components: one mutation each. - [HIGH] `DocsSubsections`: `FIXTURE` in `vi.hoisted` (the factory reads it at load); `language` as a getter function for `LicenseCard` and `UpsellCtas`; `PriceFetchControl`'s own `key({json})` format replaced by the mark, its fixture `i18nKey` data left bare. ## Proof: one `t` removed from each component (exit code of `npx vitest run <test file>`) | Component | Mutation | Exit | Red test | |---|---|---|---| | `balance/PriceFetchControl.tsx` | `{t("balance.priceFetching.button")}` → `{"balance.priceFetching.button"}` | 1 | PriceFetchControl — displayed texts > names the fetch button and the stock best-effort notice through t | | `categories/AddKeywordDialog.tsx` | `placeholder={t("transactions.table.noCategory")}` → bare key | 1 | AddKeywordDialog — category field (#423) > starts empty, with the 'no category' placeholder, until the list is in | | `categories/CategoryDetailPanel.tsx` | `{t("categories.editCategory")}` → bare key | 1 | CategoryDetailPanel — displayed texts (#389) > titles the edit view and the create view through t | | `categories/CategoryForm.tsx` | `ariaLabel={t("categories.parent")}` → bare key | 1 | CategoryForm — parent field (#423) > renders the parent as a combobox labelled by the field name, with no parent select left | | `dashboard/OnboardingChecklist.tsx` | h3 `{t("onboarding.checklist.title")}` → bare key | 1 | OnboardingChecklist > renders the title, the counter 1 / 6 and the hide button | | `dashboard/OnboardingChecklist.tsx` | `aria-label={t("onboarding.checklist.title")}` → bare key | 1 | same test (assertion added) | | `import/ImportEmptyState.tsx` | `{t("import.empty.guideLink")}` → bare key | 1 | ImportEmptyState — no folder (#356) > offers one primary 'choose a folder' button wired to onBrowse, and the guide link | | `import/SourceList.tsx` | `{t("import.sources.title")}` → bare key | 1 | SourceList and SourceCard — displayed texts (#389) > titles the list and counts each source's files through t | | `import/SourceCard.tsx` | `{t("import.sources.new")}` → bare key | 1 | SourceList and SourceCard — displayed texts (#389) > marks the files not yet imported through t | | `import/WizardNavigation.tsx` | `{t("common.cancel")}` → bare key | 1 | WizardNavigation — getting-started target (#398) > highlights the next button, and neither back nor cancel | | `reports/FilterPanel.tsx` | `t("reports.filters.accounts.label")` → bare key | 1 | FilterPanel — render > uses i18n keys for the accounts filter label and hint | | `settings/DocsSubsections.tsx` | `` t(`docs.${sectionKey}.subsections`, { returnObjects: true }) `` → the bare template | 1 | DocsSubsections > renders one block per entry with its id and registers each element | | `settings/LicenseKeyForm.tsx` | `{t(error.i18nKey, error.params)}` → `{error.i18nKey}` | 1 | LicenseKeyForm > names a refused key in the app language | | `settings/PriceFetchConsentToggle.tsx` | `{t("settings.privacy.title")}` → bare key | 1 | PriceFetchConsentToggle — displayed texts > names the section, the consent row and the not-premium tooltip through t | | `shared/UpsellGate.tsx` | `{t("upsell.revoked")}` → bare key | 1 | UpsellGate with a revoked key (#365) > names the revocation between the feature description and the CTAs | | `shared/PurchaseButton.tsx` | `tier: t("license.editions.base")` → bare key | 1 | UpsellGate with a revoked key (#365) > names the revocation... (4 red) | | `shared/HaveKeyButton.tsx` | `{t("upsell.ctaHaveKey")}` → bare key | 1 | UpsellGate with a revoked key (#365) > names the revocation... (3 red) | | `shared/DownloadLatestButton.tsx` | `{t("settings.updates.downloadLatest")}` → bare key | 1 | UpdateCard in the free edition > offers Get Base, I already have a key and Download the latest version | | `transactions/TransactionTable.tsx` | `<p>{t("transactions.noTransactions")}</p>` → bare key | 1 | TransactionTable — getting-started target (#399) > highlights nothing without a row: the card stays, without a ring | | `pages/DashboardPage.tsx` | `{t("dashboard.title")}` → bare key | 1 | DashboardPage on the shared path (#352, #396, #397) > renders only its header while the path first loads | The first `OnboardingChecklist` mutation survived (exit 0): it hit the region `aria-label`, which nothing asserted. Assertion added, both mutations now red. Every mutation run once, file restored by `git checkout`. Guard proven red (exit 1) twice: a raw local mock put back in `WizardNavigation.test.tsx` (2 findings: the `useTranslation:` line, and the mock without `i18nMock`), and `t: markedT` replaced by `t: (k: string) => k` in `useLicenseMachines.test.ts` (1 finding). The guard also has its own unit test on samples (local fake found, `useLicenseContext: () =>` / `get: (target` not found). ## Checks - `npm run build` green; `npm test` 118 files / 2026 tests (base 116 / 2009). - Diff: tests and docs only, no SQL migration, no UI text, no CHANGELOG (tests only, per the issue). ## For the reviewer - Out of scope finding: `PriceFetchControl.tsx` renders a hard-coded `best-effort` badge, without `t` (i18n rule). Not fixed here (tests-only PR). - `PriceFetchConsentToggle.test.tsx` now mocks `react` with the harness for the whole file; its older tests do not render and stay green (some of them assert local literals only, untouched). - Four former marked files (`BudgetTable.*`, `BudgetPage.wiring`, `UsersSettingsPage`) ignored options; `markedT` now prints them. No assertion needed a change. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 4 commits 2026-10-12 23:59:24 +00:00
src/test/i18nMock.ts holds the one fake t of the suite: markedT renders
t(key) or t(key[a=1,b=2]), and i18nMock({ language?, fixture? }) builds
the react-i18next module for vi.mock. The local echo of
useLicenseMachines.test.ts is replaced by markedT.

Refs #389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 15 test files whose fake t returned the bare key (the 12 of the
issue, plus the three category render tests added by #423) now mock
react-i18next with i18nMock. Assertions naming a displayed text are
rewritten in the marked form, t(key[options]), so a text that skipped
t fails them. Components without such an assertion get one:
PriceFetchControl, PriceFetchConsentToggle (rendered through the hook
harness, its loading slot preset), CategoryDetailPanel, SourceList and
SourceCard, and the region label of OnboardingChecklist.

Each component was proved by a mutation removing one t call: every one
turns at least one test red (exit 1).

Refs #389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 12 test files that already marked their output each carried a copy
of the same fake t. They now use the shared i18nMock; no assertion
changes. LicenseCard keeps its per-test language through a getter.

Refs #389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test(i18n): guard the test sources against a local fake t
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 2m5s
2bd347b4a0
src/__integration__/i18n-mock-guard.test.ts reads every test file of src
and fails on a useTranslation: or t: ( property (word-bounded) outside
src/test/i18nMock.ts, or on a react-i18next mock that does not go
through i18nMock. docs/architecture.md gets the convention: never a raw
t, always i18nMock.

Resolves #389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-10-12 23:59:33 +00:00
Author
Owner

Self-review (autopilot worker, not an independent review) — verdict: APPROVE

Diff read against issue-409-hook-harness (1385 lines, tests and docs/architecture.md only).

Acceptance criteria of #389

  • No react-i18next mock returns a bare key: the 27 files go through i18nMock; the only fake t is markedT (src/test/i18nMock.ts).
  • Each component of the raw files: one t removed, exit 1 (table in the PR body, 20 mutations over 19 components, incl. the 3 #423 files and the 4 components of UpsellCtas.render). One survivor on the first pass (OnboardingChecklist region aria-label), assertion added, red since.
  • Guard red on a local mock (two variants, exit 1), green on the tree.
  • npm test green: 118 files / 2026 tests (base 116 / 2009); npm run build green.
  • Planning criteria: 24-file list re-verified by script (exact, plus 3 drift files reported); one PR, commits in the requested order; docs section appended under ## Tests (vitest sans jsdom).

Checks

  • Security: none touched (tests, docs). No secret, no personal data in the diff.
  • Regressions: no app source changed. not.toContain assertions were rewritten in marked form too, so they stay meaningful (a bare-key not.toContain would have become vacuous). Data values that are not t output (i18nKey of price fixtures, i18n_key of a category node) kept bare.
  • i18n / CHANGELOG / migrations: no UI text, no CHANGELOG (tests only, per the issue), no SQL.

Points for the human reviewer

  • Guard keywords are word-bounded (\bt\s*:\s*\(): the literal substring of the issue flags 9 clean files (useLicenseContext: () =>, get: (target...). A third check refuses a react-i18next mock that bypasses i18nMock.
  • PriceFetchConsentToggle.test.tsx now mocks react with the #409 harness file-wide (render test needs the loading slot preset).
  • Out of scope: PriceFetchControl.tsx hard-codes a best-effort badge without t.

Generated autonomously by /autopilot run of 2026-10-12

## Self-review (autopilot worker, not an independent review) — verdict: APPROVE Diff read against `issue-409-hook-harness` (1385 lines, tests and `docs/architecture.md` only). **Acceptance criteria of #389** - [x] No `react-i18next` mock returns a bare key: the 27 files go through `i18nMock`; the only fake `t` is `markedT` (`src/test/i18nMock.ts`). - [x] Each component of the raw files: one `t` removed, exit 1 (table in the PR body, 20 mutations over 19 components, incl. the 3 #423 files and the 4 components of `UpsellCtas.render`). One survivor on the first pass (`OnboardingChecklist` region `aria-label`), assertion added, red since. - [x] Guard red on a local mock (two variants, exit 1), green on the tree. - [x] `npm test` green: 118 files / 2026 tests (base 116 / 2009); `npm run build` green. - [x] Planning criteria: 24-file list re-verified by script (exact, plus 3 drift files reported); one PR, commits in the requested order; docs section appended under `## Tests (vitest sans jsdom)`. **Checks** - Security: none touched (tests, docs). No secret, no personal data in the diff. - Regressions: no app source changed. `not.toContain` assertions were rewritten in marked form too, so they stay meaningful (a bare-key `not.toContain` would have become vacuous). Data values that are not `t` output (`i18nKey` of price fixtures, `i18n_key` of a category node) kept bare. - i18n / CHANGELOG / migrations: no UI text, no CHANGELOG (tests only, per the issue), no SQL. **Points for the human reviewer** - Guard keywords are word-bounded (`\bt\s*:\s*\(`): the literal substring of the issue flags 9 clean files (`useLicenseContext: () =>`, `get: (target`...). A third check refuses a `react-i18next` mock that bypasses `i18nMock`. - `PriceFetchConsentToggle.test.tsx` now mocks `react` with the #409 harness file-wide (render test needs the `loading` slot preset). - Out of scope: `PriceFetchControl.tsx` hard-codes a `best-effort` badge without `t`. Generated autonomously by /autopilot run of 2026-10-12
Author
Owner

CI lue par l’orchestrateur de l’autopilot (2026-10-12) sur la tête 2bd347b : PR Check — Frontend run 531 vert.

✓ built in 11.92s

Correction du rapport du worker : PriceFetchControl.tsx n’affiche aucun badge best-effort codé en dur. Les trois occurrences sont des commentaires (L2, L9, L173), et l’avis passe par t("balance.priceFetching.bestEffortNotice"). Le commentaire d’en-tête L9 parle encore d’un « badge » qui n’existe plus : commentaire périmé, pas une entorse à l’i18n.

CI lue par l’orchestrateur de l’autopilot (2026-10-12) sur la tête `2bd347b` : `PR Check — Frontend` run 531 vert. ``` ✓ built in 11.92s ``` Correction du rapport du worker : `PriceFetchControl.tsx` n’affiche aucun badge `best-effort` codé en dur. Les trois occurrences sont des commentaires (L2, L9, L173), et l’avis passe par `t("balance.priceFetching.bestEffortNotice")`. Le commentaire d’en-tête L9 parle encore d’un « badge » qui n’existe plus : commentaire périmé, pas une entorse à l’i18n.
Author
Owner

Complément de la lecture de la CI (run 531) : Test Files 118 passed (118), Tests 2026 passed (2026).

Complément de la lecture de la CI (run 531) : `Test Files 118 passed (118)`, `Tests 2026 passed (2026)`.
Author
Owner

Revue /pr-review — PR #435 (#389), tête 2bd347b, contre issue-409-hook-harness (79ba1ed)

Verdict : APPROVE

Le module src/test/i18nMock.ts fait ce que l'issue demande : un seul markedT, une fabrique avec language (valeur ou fonction) et fixture, adoptée par les 27 fichiers qui moquaient react-i18next (inventaire revérifié : 27 fichiers portent i18nMock, aucun vi.mock("react-i18next" sans lui). La dérive 24 → 27 (trois rendus de #423) est justifiée. L'ordre des commits suit la planification, et la garde ne passe au vert qu'au dernier commit.

Ce que j'ai exécuté (worktree détaché hors dépôt, npm ci propre)

  • npm run build vert ; npx vitest run : 118 fichiers, 2026 tests verts. CI de la tête : success.
  • Mutations du module partagé (suite complète) :
    • markedT sans les options → 47 rouges ;
    • markedT qui rend la clé nue → 128 rouges ;
    • fixture ignorée → 3 rouges ;
    • language figé à "fr" (fonction ignorée) → 4 rouges.
  • 14 mutations « un t retiré » de mon choix, autres que celles du tableau de la PR : 7 rouges (FilterPanel hint, ImportEmptyState titres noFolder et noSources, PriceFetchControl bestEffortNotice, PriceFetchConsentToggle label, OnboardingChecklist hide et resume). 7 survivent (TransactionTable transferIcon.ariaLabel et notes.placeholder, LicenseKeyForm activate et keyPlaceholder, PriceFetchControl consent.accept, DashboardPage kpiSectionTitle ; common.next n'existe pas dans WizardNavigation). Ça reste conforme au critère, qui demande au moins un rouge par composant, et non par appel à t. Je le note pour qu'on ne lise pas la garde comme « tout t oublié rougit ».
  • Sondes de la garde :
    • un mock useTranslation() { … } en méthode courte, sans i18nMock → rouge (3ᵉ contrôle) ;
    • le même avec un commentaire qui nomme i18nMock → vert ;
    • const t = (k: string) => k → vert ;
    • { t: vi.fn((k) => k) } → vert.
  • Chaque fichier a été restauré par git checkout, et git status est vide après chaque passe.

Suggestions (non bloquantes)

  1. Un second faux t reste dans le dépôt : src/utils/renderCategoryLabel.test.ts:13, const t = (key, options) => known[key] ?? options?.defaultValue ?? key, passé en paramètre à renderCategoryLabel. Il ne cache pas de t oublié aujourd'hui, puisque les assertions lisent des textes de known. Mais le critère « un seul faux t dans le dépôt » et la phrase de docs/architecture.md (« Le seul faux t du dépôt est markedT ») sont donc inexacts, et la garde ne le voit pas, faute de motif t = (. Deux sorties : migrer ce fichier vers markedT + fixture (le repli defaultValue demande un petit habillage), ou restreindre la phrase de la doc aux mocks de react-i18next.
  2. Garde : \bt\s*=\s*\( et \bt\s*:\s*vi\.fn\( passent tous deux. Le 3ᵉ contrôle (source.includes("i18nMock")) est satisfait par n'importe quelle mention, un commentaire compris : un vi.mock("react-i18next" qui ne contient pas l'appel i18nMock( serait plus strict. Le cas courant (un mock local copié-collé) est bien pris, d'où le caractère non bloquant.
  3. PriceFetchConsentToggle — displayed texts remplit les cases du harnais par ordre d'appel (slots[0..2]) : un useState ajouté en tête du composant décale tout en silence. Le commentaire de markup() le dit, c'est la limite connue du harnais de #409.
  4. Hors périmètre, déjà signalé dans le corps de la PR : le badge best-effort codé en dur dans PriceFetchControl.tsx mérite son issue.

Fermeture

Le corps de la PR ne porte aucun mot-clé de fermeture, mais le commit de tête contient Resolves #389. Un merge ff de la pile (/merge-stack) ferme donc #389. Pour l'instant, la timeline n'affiche aucun closes (ref_action: none).

Actions faites

  • Revue postée, label de #389 passé de status:review à status:approved.
  • Worktree de revue supprimé (git worktree remove) en fin de revue.
## Revue `/pr-review` — PR #435 (#389), tête `2bd347b`, contre `issue-409-hook-harness` (`79ba1ed`) **Verdict : APPROVE** Le module `src/test/i18nMock.ts` fait ce que l'issue demande : un seul `markedT`, une fabrique avec `language` (valeur ou fonction) et `fixture`, adoptée par les 27 fichiers qui moquaient `react-i18next` (inventaire revérifié : 27 fichiers portent `i18nMock`, aucun `vi.mock("react-i18next"` sans lui). La dérive 24 → 27 (trois rendus de #423) est justifiée. L'ordre des commits suit la planification, et la garde ne passe au vert qu'au dernier commit. ### Ce que j'ai exécuté (worktree détaché hors dépôt, `npm ci` propre) - `npm run build` vert ; `npx vitest run` : **118 fichiers, 2026 tests verts**. CI de la tête : `success`. - **Mutations du module partagé** (suite complète) : - `markedT` sans les options → **47 rouges** ; - `markedT` qui rend la clé nue → **128 rouges** ; - `fixture` ignorée → **3 rouges** ; - `language` figé à `"fr"` (fonction ignorée) → **4 rouges**. - **14 mutations « un `t` retiré » de mon choix**, autres que celles du tableau de la PR : 7 rouges (`FilterPanel` hint, `ImportEmptyState` titres noFolder et noSources, `PriceFetchControl` bestEffortNotice, `PriceFetchConsentToggle` label, `OnboardingChecklist` hide et resume). 7 survivent (`TransactionTable` transferIcon.ariaLabel et notes.placeholder, `LicenseKeyForm` activate et keyPlaceholder, `PriceFetchControl` consent.accept, `DashboardPage` kpiSectionTitle ; `common.next` n'existe pas dans `WizardNavigation`). Ça reste conforme au critère, qui demande au moins un rouge par composant, et non par appel à `t`. Je le note pour qu'on ne lise pas la garde comme « tout `t` oublié rougit ». - **Sondes de la garde** : - un mock `useTranslation() { … }` en méthode courte, sans `i18nMock` → rouge (3ᵉ contrôle) ; - le même avec un commentaire qui nomme `i18nMock` → vert ; - `const t = (k: string) => k` → vert ; - `{ t: vi.fn((k) => k) }` → vert. - Chaque fichier a été restauré par `git checkout`, et `git status` est vide après chaque passe. ### Suggestions (non bloquantes) 1. **Un second faux `t` reste dans le dépôt** : `src/utils/renderCategoryLabel.test.ts:13`, `const t = (key, options) => known[key] ?? options?.defaultValue ?? key`, passé en paramètre à `renderCategoryLabel`. Il ne cache pas de `t` oublié aujourd'hui, puisque les assertions lisent des textes de `known`. Mais le critère « un seul faux `t` dans le dépôt » et la phrase de `docs/architecture.md` (« Le seul faux `t` du dépôt est `markedT` ») sont donc inexacts, et la garde ne le voit pas, faute de motif `t = (`. Deux sorties : migrer ce fichier vers `markedT` + `fixture` (le repli `defaultValue` demande un petit habillage), ou restreindre la phrase de la doc aux mocks de `react-i18next`. 2. **Garde** : `\bt\s*=\s*\(` et `\bt\s*:\s*vi\.fn\(` passent tous deux. Le 3ᵉ contrôle (`source.includes("i18nMock")`) est satisfait par n'importe quelle mention, un commentaire compris : un `vi.mock("react-i18next"` qui ne contient pas l'appel `i18nMock(` serait plus strict. Le cas courant (un mock local copié-collé) est bien pris, d'où le caractère non bloquant. 3. `PriceFetchConsentToggle — displayed texts` remplit les cases du harnais par ordre d'appel (`slots[0..2]`) : un `useState` ajouté en tête du composant décale tout en silence. Le commentaire de `markup()` le dit, c'est la limite connue du harnais de #409. 4. Hors périmètre, déjà signalé dans le corps de la PR : le badge `best-effort` codé en dur dans `PriceFetchControl.tsx` mérite son issue. ### Fermeture Le corps de la PR ne porte aucun mot-clé de fermeture, mais le commit de tête contient `Resolves #389`. Un merge ff de la pile (`/merge-stack`) ferme donc #389. Pour l'instant, la timeline n'affiche aucun `closes` (`ref_action: none`). ### Actions faites - Revue postée, label de #389 passé de `status:review` à `status:approved`. - Worktree de revue supprimé (`git worktree remove`) en fin de revue.
Author
Owner

Suivis de cette revue appliqués en tête de pile, PR #436, commit ac3a981 (détail dans le commentaire 23535 de la PR #436, non relu).

Suivis de cette revue appliqués en tête de pile, PR #436, commit `ac3a981` (détail dans le commentaire 23535 de la PR #436, non relu).
Author
Owner

Correction de l’orchestrateur. Mon commentaire 23450 affirmait que PriceFetchControl.tsx n’avait pas de badge best-effort codé en dur. C’était faux : le badge existe aux lignes 201-206 (<span> dont le texte est le littéral best-effort, sans passer par t). Mon grep était tronqué par un head -3. Le worker et la suggestion 4 de la revue avaient raison, et le commentaire d’en-tête L9 n’est pas périmé.

Corrigé en tête de pile, sur la PR #436, par le commit c17bf5d :

  • nouvelle clé balance.priceFetching.bestEffortBadge (« non garanti » en FR, « best-effort » en EN) ;
  • le test « displayed texts » vérifie la forme marquée et l’absence du littéral ; la mutation qui remet le littéral le fait rougir (code 1) ;
  • entrée Fixed / Corrigé dans les deux CHANGELOG.
**Correction de l’orchestrateur.** Mon commentaire 23450 affirmait que `PriceFetchControl.tsx` n’avait pas de badge `best-effort` codé en dur. C’était faux : le badge existe aux lignes 201-206 (`<span>` dont le texte est le littéral `best-effort`, sans passer par `t`). Mon `grep` était tronqué par un `head -3`. Le worker et la suggestion 4 de la revue avaient raison, et le commentaire d’en-tête L9 n’est pas périmé. Corrigé en tête de pile, sur la PR #436, par le commit `c17bf5d` : - nouvelle clé `balance.priceFetching.bestEffortBadge` (« non garanti » en FR, « best-effort » en EN) ; - le test « displayed texts » vérifie la forme marquée et l’absence du littéral ; la mutation qui remet le littéral le fait rougir (code 1) ; - entrée Fixed / Corrigé dans les deux CHANGELOG.
maximus changed target branch from issue-409-hook-harness to main 2026-10-14 00:28:31 +00:00
maximus merged commit 2bd347b4a0 into main 2026-10-14 00:28:40 +00:00
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#435
No description provided.