fix(license): drop an activation token that no longer stands for this machine (#367) #388

Merged
maximus merged 3 commits from issue-367-stale-activation-token into main 2026-10-05 23:23:02 +00:00
Owner

Fixes #367

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

PR empilée sur #387 (elle-même sur #386) : elle cible issue-370-remove-license-key et ne montre que son propre diff.

Ce qui change pour l'utilisateur

  • Un dossier écrit par la v0.15.0 (clé A valide à côté du jeton d'activation d'une licence B, remboursée donc révoquée) retrouve « Activer » sans manipulation de fichier : dès que la récupération d'un cours est refusée (license_revoked), ou dès l'ouverture de la liste des machines quand la clé A n'a aucune activation de ce poste.
  • Une machine désactivée depuis un autre ordinateur retrouve « Activer » à l'ouverture de la liste des machines.
  • Le message du refus des cours invite à réactiver : « L'activation de cette machine est liée à une licence révoquée — activez de nouveau cette machine dans Paramètres → Utilisateurs » (EN : « … — activate this machine again in Settings → Users »).
  • Chaque retrait de jeton écrit une ligne au Journal, à code fixe, sans jeton, identifiant de licence ni identifiant de machine.
  • La clé enregistrée et son édition ne bougent pas : la route des cours n'écrit toujours pas l'état de la clé (décision de #365).

Technique

Rust

  • balance_commands.rs : fetch_price_in(dir, symbol, date, api_base), sans AppHandle, lit le jeton par read_stored_activation_token_in(dir) (constante ACTIVATION_FILE, plus de nom en dur) et, sur FetchPriceError::LicenseRevoked, appelle remove_activation_token_if_unchanged_in en best effort. fetch_price_with_token, sans appelant, est retiré.
  • license_commands.rs :
    • remove_activation_token_if_unchanged_in(dir, token) : supprime le fichier seulement s'il vaut encore le jeton dont parle la réponse (deux côtés rognés), via remove_activation_token_in de #370. Ok(false) fichier absent ou autre jeton, Storage fichier illisible.
    • machines_from_verify rend ListedMachines { machines, skipped } : l'affichage de la liste reste tolérant (pas d'invalid_response), seule la réconciliation lit le compteur.
    • drop_token_of_unlisted_machine_in (modèle : drop_token_of_deactivated_machine_in), appelée par list_machines_in après une réponse Ok. Gardes : identifiant du système lisible ; liste complète ; jeton qui active ce poste (signature, expiration, identifiant) ; aucune ligne qui nomme ce poste (names_this_machine, formes dérivée et brute) ; fichier inchangé depuis la requête. Toute erreur (license_revoked, 429, réseau, réponse illisible…) sort avant.
    • list_activated_machines rend MachineList { machines, token_dropped: "not_listed" | null }.

Front

  • licenseService.ts : MachineList, TokenDropped, et ACTIVATION_TOKEN_LOG (codes not_listed_for_this_machine, price_refused_revoked, sur le modèle de REVOCATION_LOG).
  • useLicenseMachines.ts : loadMachines relit l'état d'activation après chaque LIST_LOAD_DONE (lecture locale, aucune requête ajoutée au budget de 5/min/IP) et écrit la ligne quand token_dropped vaut not_listed ; logInfo injecté dans les dépendances.
  • balance.service.ts : _doFetchOnce écrit la ligne price_refused_revoked sur le code license_revoked (sans relance, donc une ligne par requête refusée).
  • i18n FR/EN : balance.priceFetching.errors.licenseRevoked, docs.editions.tips.

Docs : docs/architecture.md (lignes licenseService.ts, useLicenseMachines, list_activated_machines, contrat d'erreur, Révocation, Identifiant machine), ADR 0017 (amendement #365, « Les autres sources »), ADR 0020 (règle 4, en-tête), guide (puce « Machines »), CHANGELOG FR/EN sous Fixed/Corrigé.

Écarts au corps de l'issue

  1. Garde « jeton inchangé pendant la requête » aussi côté liste. L'issue ne l'impose qu'au 403 des cours. La même course existe côté liste : le bouton qui ouvre la liste n'est jamais désactivé, donc « Activer » puis ouverture de la liste peut faire arriver une réponse d'avant l'activation après l'écriture du jeton neuf, qui serait supprimé. list_machines_in lit le jeton avant la requête et la réconciliation passe par le même helper conditionnel. L'écart ne fait que réduire les suppressions (test a_token_written_while_the_list_was_in_flight_stays).
  2. Message auth inchangé. Il dit déjà « activez cette machine dans Paramètres → Utilisateurs » (EN : « activate this machine in Settings → Users ») ; un test l'épingle dans les deux langues avec licenseRevoked.
  3. list_activated_machines lit la clé publique embarquée (validation locale du jeton) : embedded_decoding_key()?, comme deactivate_machine_impl.

Tests

Rust : 209 → 223 (+14)

  • balance_commands : a_revoked_license_drops_the_token_it_refused (fichier ACTIVATION_FILE avec retour à la ligne, Bearer rogné, rien d'écrit à la place), a_token_written_while_the_refused_request_was_in_flight_stays (fermeture match_request qui écrit un autre jeton pendant la requête), no_other_answer_drops_the_token (200, 401, 403 premium_required et HTML, 404, 429, 502, hors ligne), without_a_token_nothing_is_sent_and_the_answer_is_auth.
  • license_commands : a_v0_15_0_folder_gets_activate_back_once_the_price_route_refuses_its_token (dossier v0.15.0 reproduit : « Activée » avant, jeton parti, license.check non écrit, « non activée » après, clé A garde son édition), the_conditional_removal_drops_only_the_token_it_is_given, a_complete_list_without_this_machine_drops_its_token, a_row_of_this_machine_keeps_the_token_under_either_identifier, a_raw_token_stays_next_to_its_row_rehashed_as_adr_0020_derives_it (ligne au format dérivé exact de l'ADR 0020, premier vecteur écrit en toutes lettres : hypothèse de maximus-api#82 figée côté client), the_token_stays_unless_the_list_can_speak_for_it (ligne illisible, identifiant illisible, jeton expiré, d'une autre machine, d'une autre clé, illisible, absent), a_list_that_fails_drops_nothing (license_revoked des deux formes, 429, not_found, valid:true sans machines, 503, hors ligne), a_token_written_while_the_list_was_in_flight_stays, the_webview_learns_whether_the_list_dropped_the_token, a_row_that_does_not_read_is_left_out_and_counted.
  • Adaptés au nouveau contrat : the_machine_list_flags_this_machine_under_either_identifier, les trois tests d'enregistrement de révocation qui appellent list_machines_in, verify_answers_map_to_machines_or_codes, verify_posts_product_to_v1_and_lists_machines, all_three_calls_work_against_v1_only_routes.

vitest : 1487 → 1495 (+8)

  • useLicenseMachines.test.ts : relecture de l'état après la liste, ordre des événements et ligne au Journal ; relecture après toute liste, ligne seulement sur token_dropped ; rien après une liste en échec ; câblage du hook (logInfo du service de journal, chemin 409). Adapté : « re-reads the local status… » lit maintenant l'état trois fois (montage, après la liste, après la désactivation).
  • balance.service.test.ts : ligne price_refused_revoked une fois sur license_revoked, aucune sur les autres réponses ; invitation à activer dans licenseRevoked et authFailed, FR et EN.
  • licenseService.test.ts : contrat MachineList ; codes de ACTIVATION_TOKEN_LOG.
  • Les lignes de Journal sont écrites en toutes lettres dans les tests (une comparaison à la constante passerait avec deux lignes échangées).

Mutations (une à la fois, restauration par git checkout -- depuis l'index : toutes rouges)

Mutation Tests rouges
R1 suppression conditionnelle : un autre jeton est supprimé aussi 3 : les deux tests « in flight » (cours, liste), the_conditional_removal_…
R2 comparaison sans rognage 2 : a_revoked_license_drops_the_token_it_refused, the_conditional_removal_…
R3 la route des cours ne supprime jamais 2 : a_revoked_license_…, a_v0_15_0_folder_…
R4 suppression sur toute erreur des cours 1 : no_other_answer_drops_the_token
R5 suppression sur tout 403 1 : no_other_answer_drops_the_token
R6 la route des cours écrit un license.check 2 : a_revoked_license_…, a_v0_15_0_folder_…
R7 jeton lu dans un autre fichier que ACTIVATION_FILE 4 : les trois tests de la route qui posent un jeton, a_v0_15_0_folder_…
R8 garde « liste complète » retirée 1 : the_token_stays_… (« a row that does not read »)
R9 identifiant illisible : jeton vérifié sur sa seule signature 1 : the_token_stays_… (« an unreadable OS id »)
R10 aucune validation locale du jeton 1 : the_token_stays_… (« an expired token of this machine »)
R10b claim machine non vérifié 1 : the_token_stays_… (« a token of another machine »)
R10c signature non vérifiée 1 : the_token_stays_… (« a token signed by another key »)
R11 seule une ligne dérivée nomme ce poste 1 : a_row_of_this_machine_… (jeton brut, ligne brute)
R12 seule une ligne brute nomme ce poste 2 : a_raw_token_stays_next_to_its_row_rehashed_…, a_row_of_this_machine_…
R14 réconciliation aussi sur une liste en échec 1 : a_list_that_fails_drops_nothing
R15 jeton lu après la réponse, plus avant la requête 1 : a_token_written_while_the_list_was_in_flight_stays
R16 not_listed rendu sans suppression 1 : a_token_written_while_the_list_was_in_flight_stays
R17 suppression jamais signalée 1 : a_complete_list_without_this_machine_drops_its_token
R18 machines_from_verify ne compte rien 2 : a_row_that_does_not_read_…, the_token_stays_…
R19 list_machines_inner perd le compteur 1 : the_token_stays_…
R20 valeur du fil notListed 1 : the_webview_learns_…
R21 sans jeton, la requête part quand même 1 : without_a_token_nothing_is_sent_…
F1 pas de relecture de l'état après la liste 4
F2 état relu avant l'appel de la liste 4
F3 ligne écrite à chaque liste 1
F4 aucune ligne quand le jeton est parti 2
F5 le hook câble logWarn au lieu de logInfo 1 (câblage)
F6 état relu aussi après une liste en échec 1
F7 les cours n'écrivent pas leur ligne 1
F8 ligne des cours sur toute erreur 1
F9 ligne de la liste écrite par les cours 1
F10 message FR licenseRevoked sans l'invitation 1
F11 code price_refused_revoked renommé 2
F12 le service perd token_dropped 2

Validation

  • npm run build (tsc + vite) : vert
  • npm test : 1495 verts
  • cargo test : 223 verts ; cargo check sans avertissement ; Cargo.lock inchangé (cargo metadata --locked OK)

Pour reviewer

  • Lire d'abord drop_token_of_unlisted_machine_in et list_machines_in (license_commands.rs), puis fetch_price_in (balance_commands.rs) et remove_activation_token_if_unchanged_in.
  • Non gardé par les tests : les commandes avec AppHandle (fetch_price, list_activated_machines_impl : embedded_decoding_key, local_machine_ids().ok(), unix_now) ; le chargement de la liste depuis toggleList (fire-and-forget) : le câblage du hook est prouvé par le chemin 409 de performActivate, seul chargement qu'un test peut attendre sous renderToStaticMarkup.
  • Fenêtre résiduelle : entre la lecture-comparaison et la suppression du fichier, quelques microsecondes contre l'aller-retour réseau ; c'est la règle de l'issue (« seulement si le fichier vaut encore le jeton présenté »).
  • Côté serveur (hors de ce dépôt) : la réconciliation suppose que maximus-api#82 rehache les lignes brutes avec exactement le dérivé de l'ADR 0020 ; la route des cours ne vérifie toujours pas qu'une machine est encore active (suivi maximus-api à ouvrir, cité dans l'issue) : jusque-là, une machine désactivée ailleurs garde les cours tant que personne n'ouvre la liste sur ce poste.
  • À vérifier à la main : (1) un dossier dans l'état décrit (clé A + jeton d'une licence B révoquée) : récupérer un cours dans le Bilan → message qui invite à réactiver et ligne au Journal ; Paramètres → Utilisateurs affiche « Non activée » et « Activer » ; « Activer » répare. (2) Une machine désactivée depuis un autre poste : ouvrir la liste des machines → « Activer » revient sans recharger, ligne au Journal.

🤖 Generated with Claude Code

Fixes #367 Generated autonomously by /autopilot run of 2026-10-04 > PR empilée sur #387 (elle-même sur #386) : elle cible `issue-370-remove-license-key` et ne montre que son propre diff. ## Ce qui change pour l'utilisateur - Un dossier écrit par la v0.15.0 (clé A valide à côté du jeton d'activation d'une licence B, remboursée donc révoquée) retrouve « Activer » sans manipulation de fichier : dès que la récupération d'un cours est refusée (`license_revoked`), ou dès l'ouverture de la liste des machines quand la clé A n'a aucune activation de ce poste. - Une machine désactivée depuis un autre ordinateur retrouve « Activer » à l'ouverture de la liste des machines. - Le message du refus des cours invite à réactiver : « L'activation de cette machine est liée à une licence révoquée — activez de nouveau cette machine dans Paramètres → Utilisateurs » (EN : « … — activate this machine again in Settings → Users »). - Chaque retrait de jeton écrit une ligne au Journal, à code fixe, sans jeton, identifiant de licence ni identifiant de machine. - La clé enregistrée et son édition ne bougent pas : la route des cours n'écrit toujours pas l'état de la clé (décision de #365). ## Technique **Rust** - `balance_commands.rs` : `fetch_price_in(dir, symbol, date, api_base)`, sans `AppHandle`, lit le jeton par `read_stored_activation_token_in(dir)` (constante `ACTIVATION_FILE`, plus de nom en dur) et, sur `FetchPriceError::LicenseRevoked`, appelle `remove_activation_token_if_unchanged_in` en best effort. `fetch_price_with_token`, sans appelant, est retiré. - `license_commands.rs` : - `remove_activation_token_if_unchanged_in(dir, token)` : supprime le fichier seulement s'il vaut encore le jeton dont parle la réponse (deux côtés rognés), via `remove_activation_token_in` de #370. `Ok(false)` fichier absent ou autre jeton, `Storage` fichier illisible. - `machines_from_verify` rend `ListedMachines { machines, skipped }` : l'affichage de la liste reste tolérant (pas d'`invalid_response`), seule la réconciliation lit le compteur. - `drop_token_of_unlisted_machine_in` (modèle : `drop_token_of_deactivated_machine_in`), appelée par `list_machines_in` après une réponse `Ok`. Gardes : identifiant du système lisible ; liste complète ; jeton qui active ce poste (signature, expiration, identifiant) ; aucune ligne qui nomme ce poste (`names_this_machine`, formes dérivée **et** brute) ; fichier inchangé depuis la requête. Toute erreur (`license_revoked`, 429, réseau, réponse illisible…) sort avant. - `list_activated_machines` rend `MachineList { machines, token_dropped: "not_listed" | null }`. **Front** - `licenseService.ts` : `MachineList`, `TokenDropped`, et `ACTIVATION_TOKEN_LOG` (codes `not_listed_for_this_machine`, `price_refused_revoked`, sur le modèle de `REVOCATION_LOG`). - `useLicenseMachines.ts` : `loadMachines` relit l'état d'activation après chaque `LIST_LOAD_DONE` (lecture locale, aucune requête ajoutée au budget de 5/min/IP) et écrit la ligne quand `token_dropped` vaut `not_listed` ; `logInfo` injecté dans les dépendances. - `balance.service.ts` : `_doFetchOnce` écrit la ligne `price_refused_revoked` sur le code `license_revoked` (sans relance, donc une ligne par requête refusée). - i18n FR/EN : `balance.priceFetching.errors.licenseRevoked`, `docs.editions.tips`. **Docs** : `docs/architecture.md` (lignes `licenseService.ts`, `useLicenseMachines`, `list_activated_machines`, contrat d'erreur, Révocation, Identifiant machine), ADR 0017 (amendement #365, « Les autres sources »), ADR 0020 (règle 4, en-tête), guide (puce « Machines »), CHANGELOG FR/EN sous Fixed/Corrigé. ## Écarts au corps de l'issue 1. **Garde « jeton inchangé pendant la requête » aussi côté liste.** L'issue ne l'impose qu'au 403 des cours. La même course existe côté liste : le bouton qui ouvre la liste n'est jamais désactivé, donc « Activer » puis ouverture de la liste peut faire arriver une réponse d'avant l'activation après l'écriture du jeton neuf, qui serait supprimé. `list_machines_in` lit le jeton avant la requête et la réconciliation passe par le même helper conditionnel. L'écart ne fait que réduire les suppressions (test `a_token_written_while_the_list_was_in_flight_stays`). 2. **Message `auth` inchangé.** Il dit déjà « activez cette machine dans Paramètres → Utilisateurs » (EN : « activate this machine in Settings → Users ») ; un test l'épingle dans les deux langues avec `licenseRevoked`. 3. **`list_activated_machines` lit la clé publique embarquée** (validation locale du jeton) : `embedded_decoding_key()?`, comme `deactivate_machine_impl`. ## Tests **Rust : 209 → 223 (+14)** - `balance_commands` : `a_revoked_license_drops_the_token_it_refused` (fichier `ACTIVATION_FILE` avec retour à la ligne, `Bearer` rogné, rien d'écrit à la place), `a_token_written_while_the_refused_request_was_in_flight_stays` (fermeture `match_request` qui écrit un autre jeton pendant la requête), `no_other_answer_drops_the_token` (200, 401, 403 `premium_required` et HTML, 404, 429, 502, hors ligne), `without_a_token_nothing_is_sent_and_the_answer_is_auth`. - `license_commands` : `a_v0_15_0_folder_gets_activate_back_once_the_price_route_refuses_its_token` (dossier v0.15.0 reproduit : « Activée » avant, jeton parti, `license.check` non écrit, « non activée » après, clé A garde son édition), `the_conditional_removal_drops_only_the_token_it_is_given`, `a_complete_list_without_this_machine_drops_its_token`, `a_row_of_this_machine_keeps_the_token_under_either_identifier`, `a_raw_token_stays_next_to_its_row_rehashed_as_adr_0020_derives_it` (ligne au format dérivé exact de l'ADR 0020, premier vecteur écrit en toutes lettres : hypothèse de maximus-api#82 figée côté client), `the_token_stays_unless_the_list_can_speak_for_it` (ligne illisible, identifiant illisible, jeton expiré, d'une autre machine, d'une autre clé, illisible, absent), `a_list_that_fails_drops_nothing` (`license_revoked` des deux formes, 429, `not_found`, `valid:true` sans `machines`, 503, hors ligne), `a_token_written_while_the_list_was_in_flight_stays`, `the_webview_learns_whether_the_list_dropped_the_token`, `a_row_that_does_not_read_is_left_out_and_counted`. - Adaptés au nouveau contrat : `the_machine_list_flags_this_machine_under_either_identifier`, les trois tests d'enregistrement de révocation qui appellent `list_machines_in`, `verify_answers_map_to_machines_or_codes`, `verify_posts_product_to_v1_and_lists_machines`, `all_three_calls_work_against_v1_only_routes`. **vitest : 1487 → 1495 (+8)** - `useLicenseMachines.test.ts` : relecture de l'état après la liste, ordre des événements et ligne au Journal ; relecture après toute liste, ligne seulement sur `token_dropped` ; rien après une liste en échec ; câblage du hook (`logInfo` du service de journal, chemin 409). Adapté : « re-reads the local status… » lit maintenant l'état trois fois (montage, après la liste, après la désactivation). - `balance.service.test.ts` : ligne `price_refused_revoked` une fois sur `license_revoked`, aucune sur les autres réponses ; invitation à activer dans `licenseRevoked` et `authFailed`, FR et EN. - `licenseService.test.ts` : contrat `MachineList` ; codes de `ACTIVATION_TOKEN_LOG`. - Les lignes de Journal sont écrites en toutes lettres dans les tests (une comparaison à la constante passerait avec deux lignes échangées). ### Mutations (une à la fois, restauration par `git checkout --` depuis l'index : toutes rouges) | Mutation | Tests rouges | |---|---| | R1 suppression conditionnelle : un autre jeton est supprimé aussi | 3 : les deux tests « in flight » (cours, liste), `the_conditional_removal_…` | | R2 comparaison sans rognage | 2 : `a_revoked_license_drops_the_token_it_refused`, `the_conditional_removal_…` | | R3 la route des cours ne supprime jamais | 2 : `a_revoked_license_…`, `a_v0_15_0_folder_…` | | R4 suppression sur toute erreur des cours | 1 : `no_other_answer_drops_the_token` | | R5 suppression sur tout 403 | 1 : `no_other_answer_drops_the_token` | | R6 la route des cours écrit un `license.check` | 2 : `a_revoked_license_…`, `a_v0_15_0_folder_…` | | R7 jeton lu dans un autre fichier que `ACTIVATION_FILE` | 4 : les trois tests de la route qui posent un jeton, `a_v0_15_0_folder_…` | | R8 garde « liste complète » retirée | 1 : `the_token_stays_…` (« a row that does not read ») | | R9 identifiant illisible : jeton vérifié sur sa seule signature | 1 : `the_token_stays_…` (« an unreadable OS id ») | | R10 aucune validation locale du jeton | 1 : `the_token_stays_…` (« an expired token of this machine ») | | R10b claim machine non vérifié | 1 : `the_token_stays_…` (« a token of another machine ») | | R10c signature non vérifiée | 1 : `the_token_stays_…` (« a token signed by another key ») | | R11 seule une ligne dérivée nomme ce poste | 1 : `a_row_of_this_machine_…` (jeton brut, ligne brute) | | R12 seule une ligne brute nomme ce poste | 2 : `a_raw_token_stays_next_to_its_row_rehashed_…`, `a_row_of_this_machine_…` | | R14 réconciliation aussi sur une liste en échec | 1 : `a_list_that_fails_drops_nothing` | | R15 jeton lu après la réponse, plus avant la requête | 1 : `a_token_written_while_the_list_was_in_flight_stays` | | R16 `not_listed` rendu sans suppression | 1 : `a_token_written_while_the_list_was_in_flight_stays` | | R17 suppression jamais signalée | 1 : `a_complete_list_without_this_machine_drops_its_token` | | R18 `machines_from_verify` ne compte rien | 2 : `a_row_that_does_not_read_…`, `the_token_stays_…` | | R19 `list_machines_inner` perd le compteur | 1 : `the_token_stays_…` | | R20 valeur du fil `notListed` | 1 : `the_webview_learns_…` | | R21 sans jeton, la requête part quand même | 1 : `without_a_token_nothing_is_sent_…` | | F1 pas de relecture de l'état après la liste | 4 | | F2 état relu avant l'appel de la liste | 4 | | F3 ligne écrite à chaque liste | 1 | | F4 aucune ligne quand le jeton est parti | 2 | | F5 le hook câble `logWarn` au lieu de `logInfo` | 1 (câblage) | | F6 état relu aussi après une liste en échec | 1 | | F7 les cours n'écrivent pas leur ligne | 1 | | F8 ligne des cours sur toute erreur | 1 | | F9 ligne de la liste écrite par les cours | 1 | | F10 message FR `licenseRevoked` sans l'invitation | 1 | | F11 code `price_refused_revoked` renommé | 2 | | F12 le service perd `token_dropped` | 2 | ## Validation - `npm run build` (tsc + vite) : vert - `npm test` : 1495 verts - `cargo test` : 223 verts ; `cargo check` sans avertissement ; `Cargo.lock` inchangé (`cargo metadata --locked` OK) ## Pour reviewer - **Lire d'abord** `drop_token_of_unlisted_machine_in` et `list_machines_in` (`license_commands.rs`), puis `fetch_price_in` (`balance_commands.rs`) et `remove_activation_token_if_unchanged_in`. - **Non gardé par les tests** : les commandes avec `AppHandle` (`fetch_price`, `list_activated_machines_impl` : `embedded_decoding_key`, `local_machine_ids().ok()`, `unix_now`) ; le chargement de la liste depuis `toggleList` (fire-and-forget) : le câblage du hook est prouvé par le chemin 409 de `performActivate`, seul chargement qu'un test peut attendre sous `renderToStaticMarkup`. - **Fenêtre résiduelle** : entre la lecture-comparaison et la suppression du fichier, quelques microsecondes contre l'aller-retour réseau ; c'est la règle de l'issue (« seulement si le fichier vaut encore le jeton présenté »). - **Côté serveur (hors de ce dépôt)** : la réconciliation suppose que maximus-api#82 rehache les lignes brutes avec exactement le dérivé de l'ADR 0020 ; la route des cours ne vérifie toujours pas qu'une machine est encore active (suivi maximus-api à ouvrir, cité dans l'issue) : jusque-là, une machine désactivée ailleurs garde les cours tant que personne n'ouvre la liste sur ce poste. - **À vérifier à la main** : (1) un dossier dans l'état décrit (clé A + jeton d'une licence B révoquée) : récupérer un cours dans le Bilan → message qui invite à réactiver et ligne au Journal ; Paramètres → Utilisateurs affiche « Non activée » et « Activer » ; « Activer » répare. (2) Une machine désactivée depuis un autre poste : ouvrir la liste des machines → « Activer » revient sans recharger, ligne au Journal. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
maximus added 1 commit 2026-10-04 21:59:39 +00:00
fix(license): drop an activation token that no longer stands for this machine
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m49s
PR Check — Rust / rust (pull_request) Successful in 3m21s
ed35570121
A data directory written by v0.15.0 or earlier can hold a valid key next
to the activation token of another license, typically a refunded hence
revoked one. The License card read "Activated", market prices were
refused, and "Activate" was never offered: the folder could not recover
without a manual file removal.

- Prices: a 403 license_revoked drops the refused token, and only while
  the file still holds it, so an activation that lands while the refused
  request is in flight keeps its new token (CWE-367). The key's revoked
  state is never written from this route. The token is read through
  ACTIVATION_FILE by the directory-based read_stored_activation_token_in,
  in fetch_price_in, testable without AppHandle.
- Machine list: list_activated_machines returns { machines,
  token_dropped }. A complete list that names this machine under neither
  identifier drops a token that activates this machine, when the OS id is
  readable and the token is the one on disk when the list was asked for.
  An error, a revoked key or a rate limit drops nothing.
  machines_from_verify counts the rows it skips, which the list still
  tolerates. This also gives "Activate" back to a machine deactivated from
  another computer.
- Front: the activation status is read again after every list load; a
  fixed log line (ACTIVATION_TOKEN_LOG) is written on token_dropped and on
  a price refused as revoked; the revoked-license price message invites to
  activate the machine again.
- Docs: architecture, ADR 0017 (#365 amendment), ADR 0020 (rule 4), user
  guide and in-app tips, changelogs.

Resolves #367

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus added the
autopilot:pending-human
label 2026-10-04 21:59:43 +00:00
maximus added 1 commit 2026-10-04 22:02:13 +00:00
docs(license): align the English tip and the price test notes with #367
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
PR Check — Rust / rust (pull_request) Successful in 3m22s
fd90de5424
The English in-app tip now says, as the French one does, that the
revoked license is that of a refunded key left next to the current key,
not the key itself. The header of the price tests says that the tests of
the stored token call fetch_price_in with a temporary data directory,
instead of claiming that every test bypasses the token file.

Resolves #367

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

Auto-revue (worker autopilot) : APPROVE

Relecture du diff complet de la PR (base issue-370-remove-license-key, tête fd90de5) sous quatre angles : sécurité, justesse, qualité, données. Ce n'est pas une revue indépendante : l'issue reste en status:review.

Résumé

  • Sécurité : aucune donnée personnelle, clé ni jeton dans le code, les tests ou les textes (balayage des lignes ajoutées). Les deux lignes de Journal sont des constantes, sans jeton, identifiant de licence ni identifiant de machine (ADR 0020, CWE-532). Le 403 tardif et la liste tardive passent par la même suppression conditionnelle (CWE-367). La route des cours n'écrit toujours pas license.check.
  • Justesse : chaque point de « Travail à faire », de la « Surface de test » et des critères d'acceptation est couvert. Chaque garde de la réconciliation a une mutation ciblée qui la prouve rouge (R8 à R12, R15) : identifiant lisible, liste complète, jeton qui active ce poste, aucune ligne de ce poste sous l'une ou l'autre forme, jeton inchangé. Vérifié en lecture seule dans maximus-api (verifyLicense) : la liste ne porte que les activations isActive = true, toujours avec leurs dates, sans limite. Une machine désactivée ailleurs en est donc absente, comme le suppose le deuxième critère.
  • Qualité : la logique vit dans Rust et dans les services, pas dans les composants. i18n FR/EN, aucun reste de débogage, aucun avertissement cargo check. fetch_price_with_token, qui n'avait plus d'appelant, est retiré.
  • Données : aucune migration SQL touchée, Cargo.lock inchangé.

Points bloquants

Aucun.

Points non bloquants

  1. src-tauri/src/commands/license_commands.rs:277 : remove_activation_token_if_unchanged_in lit, compare, puis supprime. Une écriture du jeton qui tomberait entre la comparaison et remove_file serait perdue. La fenêtre dure quelques microsecondes contre un aller-retour réseau, et c'est la règle de l'issue. La fermer demanderait un renommage atomique, hors de proportion.
  2. src/services/licenseService.ts:124 : la ligne price_refused_revoked dit « that token is removed ». C'est vrai aussi quand une activation a remplacé le jeton pendant la requête, puisqu'il n'est plus sur le disque. Ce n'est faux que si la suppression échoue sur une erreur d'E/S (best effort) : la requête suivante présente alors le même jeton, reçoit le même refus et retente la suppression. Le contrat {code} de FetchPriceError reste tel quel, comme le demande l'issue.
  3. src/hooks/useLicenseMachines.ts:433 : le chargement lancé par toggleList part sans être attendu, et ne s'atteint pas sous renderToStaticMarkup. Le câblage du hook est prouvé par le chemin 409, où performActivate attend loadMachines. À vérifier à la main : ouvrir la liste sur un poste désactivé ailleurs.
  4. src-tauri/src/commands/balance_commands.rs:225 et license_commands.rs:1282 : aucun test ne garde les commandes avec AppHandle (fetch_price, list_activated_machines_impl), comme pour les autres commandes du module.

Suggestions (hors périmètre)

  • Côté serveur : /v1/prices ne vérifie pas qu'une machine est encore active. Le suivi maximus-api est à ouvrir, comme le note l'issue. D'ici là, une machine désactivée ailleurs garde les cours tant que personne n'ouvre la liste sur ce poste.
  • maximus-api#82 doit rehacher les lignes brutes avec exactement le dérivé de l'ADR 0020 : le test a_raw_token_stays_next_to_its_row_rehashed_as_adr_0020_derives_it fige cette hypothèse côté client.
## Auto-revue (worker autopilot) : APPROVE Relecture du diff complet de la PR (base `issue-370-remove-license-key`, tête `fd90de5`) sous quatre angles : sécurité, justesse, qualité, données. Ce n'est pas une revue indépendante : l'issue reste en `status:review`. ### Résumé - **Sécurité** : aucune donnée personnelle, clé ni jeton dans le code, les tests ou les textes (balayage des lignes ajoutées). Les deux lignes de Journal sont des constantes, sans jeton, identifiant de licence ni identifiant de machine (ADR 0020, CWE-532). Le 403 tardif et la liste tardive passent par la même suppression conditionnelle (CWE-367). La route des cours n'écrit toujours pas `license.check`. - **Justesse** : chaque point de « Travail à faire », de la « Surface de test » et des critères d'acceptation est couvert. Chaque garde de la réconciliation a une mutation ciblée qui la prouve rouge (R8 à R12, R15) : identifiant lisible, liste complète, jeton qui active ce poste, aucune ligne de ce poste sous l'une ou l'autre forme, jeton inchangé. Vérifié en lecture seule dans `maximus-api` (`verifyLicense`) : la liste ne porte que les activations `isActive = true`, toujours avec leurs dates, sans limite. Une machine désactivée ailleurs en est donc absente, comme le suppose le deuxième critère. - **Qualité** : la logique vit dans Rust et dans les services, pas dans les composants. i18n FR/EN, aucun reste de débogage, aucun avertissement `cargo check`. `fetch_price_with_token`, qui n'avait plus d'appelant, est retiré. - **Données** : aucune migration SQL touchée, `Cargo.lock` inchangé. ### Points bloquants Aucun. ### Points non bloquants 1. `src-tauri/src/commands/license_commands.rs:277` : `remove_activation_token_if_unchanged_in` lit, compare, puis supprime. Une écriture du jeton qui tomberait entre la comparaison et `remove_file` serait perdue. La fenêtre dure quelques microsecondes contre un aller-retour réseau, et c'est la règle de l'issue. La fermer demanderait un renommage atomique, hors de proportion. 2. `src/services/licenseService.ts:124` : la ligne `price_refused_revoked` dit « that token is removed ». C'est vrai aussi quand une activation a remplacé le jeton pendant la requête, puisqu'il n'est plus sur le disque. Ce n'est faux que si la suppression échoue sur une erreur d'E/S (best effort) : la requête suivante présente alors le même jeton, reçoit le même refus et retente la suppression. Le contrat `{code}` de `FetchPriceError` reste tel quel, comme le demande l'issue. 3. `src/hooks/useLicenseMachines.ts:433` : le chargement lancé par `toggleList` part sans être attendu, et ne s'atteint pas sous `renderToStaticMarkup`. Le câblage du hook est prouvé par le chemin 409, où `performActivate` attend `loadMachines`. À vérifier à la main : ouvrir la liste sur un poste désactivé ailleurs. 4. `src-tauri/src/commands/balance_commands.rs:225` et `license_commands.rs:1282` : aucun test ne garde les commandes avec `AppHandle` (`fetch_price`, `list_activated_machines_impl`), comme pour les autres commandes du module. ### Suggestions (hors périmètre) - Côté serveur : `/v1/prices` ne vérifie pas qu'une machine est encore active. Le suivi maximus-api est à ouvrir, comme le note l'issue. D'ici là, une machine désactivée ailleurs garde les cours tant que personne n'ouvre la liste sur ce poste. - maximus-api#82 doit rehacher les lignes brutes avec exactement le dérivé de l'ADR 0020 : le test `a_raw_token_stays_next_to_its_row_rehashed_as_adr_0020_derives_it` fige cette hypothèse côté client.
Author
Owner

Revue adversariale (/pr-review) : APPROVE

Verdict : APPROVE. Les deux retraits du jeton font ce que l'issue demande, sous les gardes qu'elle fixe, et chaque garde centrale que j'ai mutée tombe en rouge. Une épingle de test ne départage pas sa branche (suggestion 1), sans effet sur le comportement livré.

Ce qui a tourné

Copie hors dépôt à fd90de5 (clone dans /tmp, npm ci, dossier target séparé) :

  • cargo test --all-targets : 223 verts ; vitest run : 1495 verts (79 fichiers).
  • CI de la tête lue dans les logs : run 486 (cargo check sans avertissement, garde des advisories à 2 vérifications, 223 tests), run 485 (tsc && vite build, 1495 tests).
  • Tête de pile : main est ancêtre de fd90de5, donc ces suites valent l'état d'après merge de toute la pile #386 → #387 → #388.

Mutations du relecteur, une à la fois, restaurées par git checkout -- dans la copie :

Mutation Rouges
M1 fetch_price_in supprime sans condition (remove_activation_token_in) 1 : a_token_written_while_the_refused_request_was_in_flight_stays
M2 list_machines_in lit le jeton après la réponse 1 : a_token_written_while_the_list_was_in_flight_stays
M3 garde listed.skipped > 0 retirée 1 : the_token_stays_unless_the_list_can_speak_for_it
M4 remove_activation_token_if_unchanged_in : une lecture en échec supprime quand même (Err(_) => {}) 0 sur 223 (suggestion 1)
M5 front : état relu seulement quand token_dropped vaut not_listed 2 (useLicenseMachines.test.ts)
M6 en.json : licenseRevoked sans l'invitation à activer 1 (balance.service.test.ts)

Hypothèses serveur vérifiées dans maximus-api (lecture seule)

  • verifyLicense (main) ne liste que les activations isActive = true, sans limite, avec le machine_id complet ; machine_id, activated_at et last_seen_at sont NOT NULL dans le schéma. skipped vaut donc 0 en pratique, et une machine désactivée ailleurs disparaît de la liste, comme le suppose le deuxième critère d'acceptation.
  • activateLicense rafraîchit le jeton d'une machine déjà active : un jeton retiré à tort se répare par « Activer », sans prendre une seconde place.
  • licenseAuth ne répond 403 license_revoked que pour la licence du sub du jeton : la route des cours condamne le jeton, pas la clé.
  • La branche issue-82-derived-machine-ids (non mergée) dérive par HMAC-SHA256 sous com.simpl.resultat/license-machine-id/v1, avec un rognage aligné sur str::trim, et ses tests portent les deux vecteurs de l'ADR 0020, recalculés ici hors du code. L'hypothèse que fige a_raw_token_stays_next_to_its_row_rehashed_as_adr_0020_derives_it tient contre l'implémentation en cours.

Contrôlé, sans constat

  • activation_token_machine_id rogne le jeton : un fichier à retour à la ligne se valide aussi côté liste, et remove_activation_token_if_unchanged_in rogne les deux côtés.
  • ACTIVATION_STATUS ne touche pas activationError : la relecture ajoutée après la liste n'efface pas le message 409 du chemin D2. loadActivationStatus avale ses erreurs, donc le chargement lancé par toggleList ne crée pas de rejet non géré.
  • Clé changée ou retirée pendant la liste : store_license_in et remove_license_in suppriment le jeton, et la suppression conditionnelle rend Ok(false).
  • Aucun autre consommateur de list_activated_machines ni de fetch_price ; pas d'import circulaire entre balance.service.ts et licenseService.ts.
  • Fermeture : la timeline de #367 porte un seul pull_ref closes, depuis #388 ; corps et commits (Resolves #367) ne ferment que #367, conformément à la décision de planification.
  • Lignes ajoutées scannées : aucun secret ni donnée personnelle (seule adresse : 127.0.0.1:1 des tests hors ligne).

Suggestions (non bloquantes)

  1. license_commands.rs, the_conditional_removal_drops_only_the_token_it_is_given : le cas « un dossier à la place du jeton » ne départage pas la branche Err(_) => return Err(LicenseError::Storage). Sous M4, remove_file échoue aussi sur un dossier et rend Storage quand même, d'où 223 verts. Un jeton non UTF-8 départage : sonde ajoutée dans la copie, verte sur la PR, rouge sous M4 (left: Ok(true), fichier supprimé) :
    dir.write(ACTIVATION_FILE, NOT_UTF8);
    assert_eq!(remove_activation_token_if_unchanged_in(&dir.0, "token-a"), Err(LicenseError::Storage));
    assert!(dir.0.join(ACTIVATION_FILE).exists());
    
    Portée pratique faible : les deux appelants ont déjà lu le jeton avant la requête, et la branche ne joue que si le fichier devient illisible pendant celle-ci. Épingle manquante sur un code correct : en tête de pile avant le merge, ou en suivi.
  2. Vérifications manuelles (1) et (2) du corps de la PR : Fixes #367 ferme l'issue au merge, et ces deux contrôles n'auront plus d'autre trace que ce corps. Les reporter au merge dans la liste des vérifications de release de STATE.md, comme pour #346, #350 et #365.

Actions faites

  • Copie /tmp/pr388-src, dossier /tmp/pr388-target, scripts de mutation et logs CI téléchargés : supprimés en fin de revue. Arbre de travail du dépôt jamais touché.
  • Label de #367 : status:review → status:approved, posé avec ce commentaire.
## Revue adversariale (/pr-review) : APPROVE **Verdict : APPROVE.** Les deux retraits du jeton font ce que l'issue demande, sous les gardes qu'elle fixe, et chaque garde centrale que j'ai mutée tombe en rouge. Une épingle de test ne départage pas sa branche (suggestion 1), sans effet sur le comportement livré. ### Ce qui a tourné Copie hors dépôt à `fd90de5` (clone dans `/tmp`, `npm ci`, dossier `target` séparé) : - `cargo test --all-targets` : **223 verts** ; `vitest run` : **1495 verts** (79 fichiers). - CI de la tête lue dans les logs : run 486 (`cargo check` sans avertissement, garde des advisories à 2 vérifications, 223 tests), run 485 (`tsc && vite build`, 1495 tests). - Tête de pile : `main` est ancêtre de `fd90de5`, donc ces suites valent l'état d'après merge de toute la pile #386 → #387 → #388. Mutations du relecteur, une à la fois, restaurées par `git checkout --` dans la copie : | Mutation | Rouges | |---|---| | M1 `fetch_price_in` supprime sans condition (`remove_activation_token_in`) | 1 : `a_token_written_while_the_refused_request_was_in_flight_stays` | | M2 `list_machines_in` lit le jeton après la réponse | 1 : `a_token_written_while_the_list_was_in_flight_stays` | | M3 garde `listed.skipped > 0` retirée | 1 : `the_token_stays_unless_the_list_can_speak_for_it` | | M4 `remove_activation_token_if_unchanged_in` : une lecture en échec supprime quand même (`Err(_) => {}`) | **0 sur 223** (suggestion 1) | | M5 front : état relu seulement quand `token_dropped` vaut `not_listed` | 2 (`useLicenseMachines.test.ts`) | | M6 `en.json` : `licenseRevoked` sans l'invitation à activer | 1 (`balance.service.test.ts`) | ### Hypothèses serveur vérifiées dans maximus-api (lecture seule) - `verifyLicense` (`main`) ne liste que les activations `isActive = true`, sans limite, avec le `machine_id` complet ; `machine_id`, `activated_at` et `last_seen_at` sont `NOT NULL` dans le schéma. `skipped` vaut donc 0 en pratique, et une machine désactivée ailleurs disparaît de la liste, comme le suppose le deuxième critère d'acceptation. - `activateLicense` rafraîchit le jeton d'une machine déjà active : un jeton retiré à tort se répare par « Activer », sans prendre une seconde place. - `licenseAuth` ne répond 403 `license_revoked` que pour la licence du `sub` du jeton : la route des cours condamne le jeton, pas la clé. - La branche `issue-82-derived-machine-ids` (non mergée) dérive par `HMAC-SHA256` sous `com.simpl.resultat/license-machine-id/v1`, avec un rognage aligné sur `str::trim`, et ses tests portent les deux vecteurs de l'ADR 0020, recalculés ici hors du code. L'hypothèse que fige `a_raw_token_stays_next_to_its_row_rehashed_as_adr_0020_derives_it` tient contre l'implémentation en cours. ### Contrôlé, sans constat - `activation_token_machine_id` rogne le jeton : un fichier à retour à la ligne se valide aussi côté liste, et `remove_activation_token_if_unchanged_in` rogne les deux côtés. - `ACTIVATION_STATUS` ne touche pas `activationError` : la relecture ajoutée après la liste n'efface pas le message 409 du chemin D2. `loadActivationStatus` avale ses erreurs, donc le chargement lancé par `toggleList` ne crée pas de rejet non géré. - Clé changée ou retirée pendant la liste : `store_license_in` et `remove_license_in` suppriment le jeton, et la suppression conditionnelle rend `Ok(false)`. - Aucun autre consommateur de `list_activated_machines` ni de `fetch_price` ; pas d'import circulaire entre `balance.service.ts` et `licenseService.ts`. - Fermeture : la timeline de #367 porte un seul `pull_ref` `closes`, depuis #388 ; corps et commits (`Resolves #367`) ne ferment que #367, conformément à la décision de planification. - Lignes ajoutées scannées : aucun secret ni donnée personnelle (seule adresse : `127.0.0.1:1` des tests hors ligne). ### Suggestions (non bloquantes) 1. **`license_commands.rs`, `the_conditional_removal_drops_only_the_token_it_is_given`** : le cas « un dossier à la place du jeton » ne départage pas la branche `Err(_) => return Err(LicenseError::Storage)`. Sous M4, `remove_file` échoue aussi sur un dossier et rend `Storage` quand même, d'où 223 verts. Un jeton non UTF-8 départage : sonde ajoutée dans la copie, verte sur la PR, rouge sous M4 (`left: Ok(true)`, fichier supprimé) : ```rust dir.write(ACTIVATION_FILE, NOT_UTF8); assert_eq!(remove_activation_token_if_unchanged_in(&dir.0, "token-a"), Err(LicenseError::Storage)); assert!(dir.0.join(ACTIVATION_FILE).exists()); ``` Portée pratique faible : les deux appelants ont déjà lu le jeton avant la requête, et la branche ne joue que si le fichier devient illisible pendant celle-ci. Épingle manquante sur un code correct : en tête de pile avant le merge, ou en suivi. 2. **Vérifications manuelles (1) et (2) du corps de la PR** : `Fixes #367` ferme l'issue au merge, et ces deux contrôles n'auront plus d'autre trace que ce corps. Les reporter au merge dans la liste des vérifications de release de `STATE.md`, comme pour #346, #350 et #365. ### Actions faites - Copie `/tmp/pr388-src`, dossier `/tmp/pr388-target`, scripts de mutation et logs CI téléchargés : supprimés en fin de revue. Arbre de travail du dépôt jamais touché. - Label de #367 : `status:review` → `status:approved`, posé avec ce commentaire.
maximus added 1 commit 2026-10-05 23:16:23 +00:00
fix(license): apply the review follow-ups on the license stack head
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m47s
PR Check — Rust / rust (pull_request) Successful in 3m40s
2cbc886653
Follow-ups of the three /pr-review passes on #386, #387 and #388, applied
on the stack head before the fast-forward merge:

- #386: pin the relay of the revoked state by revocationCheckLauncher
  (a launcher passing false instead of wasRevoked passed the whole suite),
  and say in the rate budget comment that two checks in one minute bring
  the machine-limit path to 6 requests.
- #387 S1: the fake `t` of the removal tests and of the License card tests
  marks what it translates, so a string that skipped `t` fails its test.
- #387 S2: pin that storing another key stops on Storage while the
  previous token cannot be dropped, the previous key staying.
- #387 S3: reload the license context after a failed removal too. Rust
  deletes the key first, so a token that resisted left the key gone while
  the card kept showing it under "the key was not removed".
- #388: pin that an unreadable token stays (the directory case could not
  tell, its deletion fails anyway).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximus changed target branch from issue-370-remove-license-key to main 2026-10-05 23:23:00 +00:00
maximus merged commit 2cbc886653 into main 2026-10-05 23:23:02 +00:00
Author
Owner

Mergée le 2026-10-05 en avance rapide par l'API, avec toute la pile : #386 → #387 → #388, chacune épinglée sur sa tête, main = 2cbc886.

2cbc886 a été ajouté après la revue et n'a pas été relu. C'est un commit unique en tête de pile, qui applique les suggestions non bloquantes des trois /pr-review :

  • #386 : un test épingle le relais de « déjà révoquée » par revocationCheckLauncher. Le commentaire de useLicenseMachines.ts dit aussi que deux contrôles dans la même minute portent le parcours « limite de machines » à 6 requêtes.
  • #387 S1 : le faux t des tests du retrait et de la carte Licence marque sa sortie, t(clé[options]).
  • #387 S2 : un test épingle le Storage de store_license_in quand l'ancien jeton ne peut pas être supprimé ; l'ancienne clé reste.
  • #387 S3 : le contexte est rechargé aussi après un retrait en échec, pour que la carte suive le disque.
  • #388 : un jeton illisible (non UTF-8) reste en place.
Mutation (une à la fois) Tests rouges
le lanceur passe false au lieu de « déjà révoquée » 2
première confirmation du retrait sans t 18
raison de la seconde confirmation sans t 4 (avant : 1)
raison d'échec sur la carte sans t 1 (avant : 0)
pas de rechargement après un échec 10
store_license_in ignore un jeton non supprimable 1 (avant : 0)
jeton illisible supprimé quand même (M4) 1 (avant : 0)

Validation du tip cumulé : worktree jetable, npm ci propre ; une sonde volontairement rouge sort bien en code 1. Résultats : build vert, 1498 vitest, 224 tests Rust, cargo check sans avertissement, cargo metadata --locked accepté. Rien ne change par rapport à main dans .cargo, .forgejo, Cargo.lock, Cargo.toml, package.json ni package-lock.json. La CI de 2cbc886 est verte : run 489 (1498 tests) et run 490 (224 tests, cache partagé restauré en full match).

Suite : les huit autres fichiers de tests dont le faux t rend la clé brute partent dans une issue de suivi. Les vérifications manuelles (1) et (2) de cette PR sont reportées dans STATE.md.

**Mergée le 2026-10-05 en avance rapide par l'API**, avec toute la pile : #386 → #387 → #388, chacune épinglée sur sa tête, `main` = `2cbc886`. **`2cbc886` a été ajouté après la revue et n'a pas été relu.** C'est un commit unique en tête de pile, qui applique les suggestions non bloquantes des trois `/pr-review` : - **#386** : un test épingle le relais de « déjà révoquée » par `revocationCheckLauncher`. Le commentaire de `useLicenseMachines.ts` dit aussi que deux contrôles dans la même minute portent le parcours « limite de machines » à 6 requêtes. - **#387 S1** : le faux `t` des tests du retrait et de la carte Licence marque sa sortie, `t(clé[options])`. - **#387 S2** : un test épingle le `Storage` de `store_license_in` quand l'ancien jeton ne peut pas être supprimé ; l'ancienne clé reste. - **#387 S3** : le contexte est rechargé aussi après un retrait en échec, pour que la carte suive le disque. - **#388** : un jeton illisible (non UTF-8) reste en place. | Mutation (une à la fois) | Tests rouges | |---|---| | le lanceur passe `false` au lieu de « déjà révoquée » | 2 | | première confirmation du retrait sans `t` | 18 | | raison de la seconde confirmation sans `t` | 4 (avant : 1) | | raison d'échec sur la carte sans `t` | 1 (avant : 0) | | pas de rechargement après un échec | 10 | | `store_license_in` ignore un jeton non supprimable | 1 (avant : 0) | | jeton illisible supprimé quand même (M4) | 1 (avant : 0) | **Validation du tip cumulé** : worktree jetable, `npm ci` propre ; une sonde volontairement rouge sort bien en code 1. Résultats : build vert, 1498 vitest, 224 tests Rust, `cargo check` sans avertissement, `cargo metadata --locked` accepté. Rien ne change par rapport à `main` dans `.cargo`, `.forgejo`, `Cargo.lock`, `Cargo.toml`, `package.json` ni `package-lock.json`. La CI de `2cbc886` est verte : run 489 (1498 tests) et run 490 (224 tests, cache partagé restauré en `full match`). **Suite** : les huit autres fichiers de tests dont le faux `t` rend la clé brute partent dans une issue de suivi. Les vérifications manuelles (1) et (2) de cette PR sont reportées dans `STATE.md`.
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#388
No description provided.