[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
|
|
|
|
|
"""Un SCSS personnalisé est une copie figée, comme une vue COW.
|
|
|
|
|
|
|
|
|
|
Personnaliser un site écrit du SCSS dans `ir_attachment`. Cette copie est
|
|
|
|
|
figée le jour où elle est écrite et continue d'employer les variables de
|
|
|
|
|
CETTE version. Un module peut les renommer d'un palier à l'autre : mesuré,
|
|
|
|
|
`website/.../primary_variables.scss` déclarait `$o-theme-font-number` en 12.0
|
|
|
|
|
et a remplacé tout le mécanisme en 13.0. Une personnalisation de 2020
|
|
|
|
|
demandait encore l'ancien nom, et le bundle s'arrêtait dessus.
|
|
|
|
|
|
|
|
|
|
Ce qui rend un tel détecteur utilisable n'est pas de trouver — c'est de ne
|
|
|
|
|
pas crier à tort. Sur le seul fichier mesuré, la première version rapportait
|
|
|
|
|
dix noms dont six n'étaient pas des manques : paramètres de mixin, variables
|
|
|
|
|
de boucle, arguments nommés d'`@include`. Ces tests portent surtout là-dessus.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
sys.path.insert(0, os.path.join(REPO, "script", "odoo", "migration"))
|
|
|
|
|
|
|
|
|
|
import check_stale_scss as scss # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWhatCountsAsAUse(unittest.TestCase):
|
|
|
|
|
def test_a_plain_dereference_is_a_use(self):
|
|
|
|
|
self.assertIn("color", scss.used_names("a { b: $color; }"))
|
|
|
|
|
|
|
|
|
|
def test_a_declaration_is_not_a_use(self):
|
|
|
|
|
self.assertNotIn("color", scss.used_names("$color: red;"))
|
|
|
|
|
|
|
|
|
|
def test_a_named_include_argument_is_not_a_use(self):
|
|
|
|
|
# LE faux positif mesuré : « @include o-position-absolute(
|
|
|
|
|
# $right: 50%) » se lisait comme l'usage d'un $right inexistant.
|
|
|
|
|
source = "@include o-position-absolute($right: 50%, $left: 50%);"
|
|
|
|
|
self.assertEqual(scss.used_names(source), set())
|
|
|
|
|
|
|
|
|
|
def test_a_default_value_is_not_a_use(self):
|
|
|
|
|
self.assertNotIn("size", scss.used_names("$size: 3 !default;"))
|
|
|
|
|
|
|
|
|
|
def test_the_name_is_not_truncated_to_dodge_the_colon(self):
|
|
|
|
|
# Une négation en tête de motif fait rétrograder le nom : le premier
|
|
|
|
|
# essai rapportait « $botto » au lieu d'écarter « $bottom ».
|
|
|
|
|
names = scss.used_names("a { b: $bottom-margin; }")
|
|
|
|
|
self.assertEqual(names, {"bottom-margin"})
|
|
|
|
|
|
|
|
|
|
def test_a_use_after_a_declaration_still_counts(self):
|
|
|
|
|
self.assertEqual(scss.used_names("$a: $b;"), {"b"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestWhatTheFileBindsItself(unittest.TestCase):
|
|
|
|
|
def test_mixin_parameters_are_bound(self):
|
|
|
|
|
source = "@mixin thing($on, $off: 2) { a: $on; b: $off; }"
|
|
|
|
|
self.assertEqual(scss.bound_names(source), {"on", "off"})
|
|
|
|
|
|
|
|
|
|
def test_function_parameters_are_bound(self):
|
|
|
|
|
source = "@function f($value) { @return $value; }"
|
|
|
|
|
self.assertIn("value", scss.bound_names(source))
|
|
|
|
|
|
|
|
|
|
def test_each_loop_variables_are_bound(self):
|
|
|
|
|
source = "@each $key, $val in $map { a: $key; }"
|
|
|
|
|
bound = scss.bound_names(source)
|
|
|
|
|
self.assertIn("key", bound)
|
|
|
|
|
self.assertIn("val", bound)
|
|
|
|
|
|
|
|
|
|
def test_for_loop_variables_are_bound(self):
|
|
|
|
|
source = "@for $counter from 1 through 3 { a: $counter; }"
|
|
|
|
|
self.assertIn("counter", scss.bound_names(source))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheUrlIsSplitForTheFix(unittest.TestCase):
|
|
|
|
|
"""reset_asset veut le fichier ET le bundle ; l'URL porte les deux."""
|
|
|
|
|
|
|
|
|
|
def test_a_frontend_customization(self):
|
|
|
|
|
base, bundle = scss.split_custom_url(
|
|
|
|
|
"/website/static/src/scss/website.custom.web.assets_frontend.scss"
|
|
|
|
|
)
|
|
|
|
|
self.assertEqual(base, "/website/static/src/scss/website.scss")
|
|
|
|
|
self.assertEqual(bundle, "web.assets_frontend")
|
|
|
|
|
|
|
|
|
|
def test_a_common_customization(self):
|
|
|
|
|
base, bundle = scss.split_custom_url(
|
|
|
|
|
"/website/static/src/scss/options/colors/"
|
|
|
|
|
"user_theme_color_palette.custom.web.assets_common.scss"
|
|
|
|
|
)
|
|
|
|
|
self.assertTrue(base.endswith("user_theme_color_palette.scss"))
|
|
|
|
|
self.assertEqual(bundle, "web.assets_common")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheReport(unittest.TestCase):
|
|
|
|
|
def setUp(self):
|
|
|
|
|
from script.todo import todo_i18n
|
|
|
|
|
|
|
|
|
|
self.addCleanup(
|
|
|
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
|
|
|
)
|
|
|
|
|
todo_i18n._current_lang = "en"
|
|
|
|
|
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
def finding(self, **override):
|
|
|
|
|
data = {
|
|
|
|
|
"id": 1110,
|
|
|
|
|
"url": "/a/b.custom.web.assets_frontend.scss",
|
|
|
|
|
"missing": ["o-theme-font-number"],
|
|
|
|
|
"custom": "a { b: $o-theme-font-number; }",
|
|
|
|
|
"base_url": "/a/b.scss",
|
|
|
|
|
"bundle": "web.assets_frontend",
|
|
|
|
|
"module_path": "odoo13.0/a/b.scss",
|
|
|
|
|
"module_content": "a { b: 1; }",
|
|
|
|
|
"version_dir": "odoo13.0",
|
|
|
|
|
"database": "db",
|
|
|
|
|
}
|
|
|
|
|
data.update(override)
|
|
|
|
|
return data
|
|
|
|
|
|
[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
def test_nothing_at_risk_says_so(self):
|
|
|
|
|
text = scss.render([], "db", "odoo13.0")
|
|
|
|
|
self.assertIn("✅", text)
|
|
|
|
|
|
|
|
|
|
def test_a_finding_names_the_attachment_and_the_variable(self):
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
text = scss.render([self.finding()], "db", "odoo13.0")
|
[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
self.assertIn("1110", text)
|
|
|
|
|
self.assertIn("$o-theme-font-number", text)
|
|
|
|
|
|
|
|
|
|
def test_it_prints_a_command_that_can_be_pasted(self):
|
|
|
|
|
# Un diagnostic sans le geste qui répare oblige à rechercher les deux
|
|
|
|
|
# arguments de reset_asset au pire moment.
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
text = scss.render([self.finding()], "mydb", "odoo13.0")
|
[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
self.assertIn("reset_asset('/a/b.scss', 'web.assets_frontend')", text)
|
|
|
|
|
self.assertIn("-d mydb", text)
|
|
|
|
|
|
|
|
|
|
def test_it_warns_before_dropping(self):
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
text = scss.render([self.finding()], "db", "odoo13.0")
|
[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
self.assertIn("real customization", text)
|
|
|
|
|
|
|
|
|
|
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
class TestTheDiff(unittest.TestCase):
|
|
|
|
|
"""Ce que la copie a changé : la seule chose que réinitialiser perd."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
from script.todo import todo_i18n
|
|
|
|
|
|
|
|
|
|
self.addCleanup(
|
|
|
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
|
|
|
)
|
|
|
|
|
todo_i18n._current_lang = "en"
|
|
|
|
|
|
|
|
|
|
def finding(self, **override):
|
|
|
|
|
data = {
|
|
|
|
|
"id": 1110,
|
|
|
|
|
"url": "/a/b.custom.web.assets_frontend.scss",
|
|
|
|
|
"missing": ["x"],
|
|
|
|
|
"custom": "a { b: 2; }",
|
|
|
|
|
"base_url": "/a/b.scss",
|
|
|
|
|
"bundle": "web.assets_frontend",
|
|
|
|
|
"module_path": "odoo13.0/a/b.scss",
|
|
|
|
|
"module_content": "a { b: 1; }",
|
|
|
|
|
"version_dir": "odoo13.0",
|
|
|
|
|
"database": "db",
|
|
|
|
|
}
|
|
|
|
|
data.update(override)
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
def test_it_counts_what_would_be_lost(self):
|
|
|
|
|
text = scss.render_diff(self.finding())
|
|
|
|
|
self.assertIn("+1/-1", text)
|
|
|
|
|
|
|
|
|
|
def test_an_identical_copy_says_it_loses_nothing(self):
|
|
|
|
|
text = scss.render_diff(self.finding(custom="a { b: 1; }"))
|
|
|
|
|
self.assertIn("identical", text)
|
|
|
|
|
|
|
|
|
|
def test_a_missing_module_file_is_said_not_guessed(self):
|
|
|
|
|
# Si la cible ne livre plus le fichier, il n'y a rien sur quoi
|
|
|
|
|
# retomber : le taire ferait accepter une réinitialisation vide.
|
|
|
|
|
text = scss.render_diff(self.finding(module_path=None))
|
|
|
|
|
self.assertIn("no longer ships", text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestThePrompt(unittest.TestCase):
|
|
|
|
|
"""Regarder ne doit pas répondre à la question."""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
from script.todo import todo_i18n
|
|
|
|
|
|
|
|
|
|
self.addCleanup(
|
|
|
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
|
|
|
)
|
|
|
|
|
todo_i18n._current_lang = "en"
|
|
|
|
|
|
|
|
|
|
def finding(self):
|
|
|
|
|
return {
|
|
|
|
|
"id": 1,
|
|
|
|
|
"url": "/a/b.custom.web.assets_frontend.scss",
|
|
|
|
|
"missing": ["x"],
|
|
|
|
|
"custom": "a { b: 2; }",
|
|
|
|
|
"base_url": "/a/b.scss",
|
|
|
|
|
"bundle": "web.assets_frontend",
|
|
|
|
|
"module_path": "odoo13.0/a/b.scss",
|
|
|
|
|
"module_content": "a { b: 1; }",
|
|
|
|
|
"version_dir": "odoo13.0",
|
|
|
|
|
"database": "db",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def run_prompt(self, answers):
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
|
|
seq = iter(answers)
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(out):
|
|
|
|
|
wrote = scss.prompt(
|
|
|
|
|
[self.finding()], "db", ask=lambda prompt: next(seq)
|
|
|
|
|
)
|
|
|
|
|
return wrote, out.getvalue()
|
|
|
|
|
|
|
|
|
|
def test_enter_writes_nothing(self):
|
|
|
|
|
wrote, text = self.run_prompt([""])
|
|
|
|
|
self.assertFalse(wrote)
|
|
|
|
|
self.assertEqual(text, "")
|
|
|
|
|
|
|
|
|
|
def test_v_shows_the_diff_and_asks_again(self):
|
|
|
|
|
# Sans le « asks again », montrer vaudrait réponse : on aurait vu le
|
|
|
|
|
# diff et perdu la main sur la décision.
|
|
|
|
|
wrote, text = self.run_prompt(["v", ""])
|
|
|
|
|
self.assertFalse(wrote)
|
|
|
|
|
self.assertIn("+1/-1", text)
|
|
|
|
|
|
|
|
|
|
def test_apply_saves_a_copy_before_writing(self):
|
|
|
|
|
# reset_asset SUPPRIME la pièce jointe : sans sauvegarde préalable,
|
|
|
|
|
# les lignes personnalisées ne seraient plus nulle part.
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
previous = os.getcwd()
|
|
|
|
|
self.addCleanup(os.chdir, previous)
|
|
|
|
|
os.chdir(tempfile.mkdtemp())
|
|
|
|
|
seen = {}
|
|
|
|
|
original = scss.apply_reset
|
|
|
|
|
scss.apply_reset = lambda lst, db, cfg="./config.conf": (
|
|
|
|
|
seen.update(called=True),
|
|
|
|
|
(0, "ok"),
|
|
|
|
|
)[1]
|
|
|
|
|
self.addCleanup(setattr, scss, "apply_reset", original)
|
|
|
|
|
wrote, text = self.run_prompt(["a"])
|
|
|
|
|
self.assertTrue(wrote)
|
|
|
|
|
self.assertTrue(seen.get("called"))
|
|
|
|
|
import glob
|
|
|
|
|
|
|
|
|
|
saved = glob.glob("private/odoo/migration/db/scss_backup/*")
|
|
|
|
|
self.assertEqual(len(saved), 1)
|
|
|
|
|
with open(saved[0]) as handle:
|
|
|
|
|
self.assertEqual(handle.read(), "a { b: 2; }")
|
|
|
|
|
|
|
|
|
|
def test_a_failed_reset_says_nothing_changed(self):
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
previous = os.getcwd()
|
|
|
|
|
self.addCleanup(os.chdir, previous)
|
|
|
|
|
os.chdir(tempfile.mkdtemp())
|
|
|
|
|
original = scss.apply_reset
|
|
|
|
|
scss.apply_reset = lambda lst, db, cfg="./config.conf": (1, "boom")
|
|
|
|
|
self.addCleanup(setattr, scss, "apply_reset", original)
|
|
|
|
|
wrote, text = self.run_prompt(["a"])
|
|
|
|
|
self.assertFalse(wrote)
|
|
|
|
|
self.assertIn("nothing was changed", text)
|
|
|
|
|
|
|
|
|
|
|
[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
class TestTheMigrationRunsIt(unittest.TestCase):
|
|
|
|
|
def test_it_is_checked_at_the_same_place_as_the_cow_views(self):
|
|
|
|
|
# Les deux prédictions valent avant le palier, pas après : c'est le
|
|
|
|
|
# seul moment où l'on peut encore arbitrer sans avoir tout rejoué.
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
from script.todo.todo_upgrade import TodoUpgrade
|
|
|
|
|
|
|
|
|
|
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
|
|
|
|
self.assertIn("check_stale_scss.py", source)
|
|
|
|
|
self.assertLess(
|
|
|
|
|
source.index("check_stale_scss.py"),
|
|
|
|
|
source.index("4 - Upgrade version with OpenUpgrade"),
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
def test_it_gets_a_real_terminal(self):
|
|
|
|
|
# L'outil pose lui-même ses questions et peut ouvrir un plein écran :
|
|
|
|
|
# un tube les rendrait toutes injoignables, sans rien signaler.
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
from script.todo.todo_upgrade import TodoUpgrade
|
|
|
|
|
|
|
|
|
|
source = inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
|
|
|
|
# On ne compare pas à une mise en forme — black la change — mais à
|
|
|
|
|
# l'appel le plus proche EN AMONT : c'est lui qui exécute.
|
|
|
|
|
avant = source[: source.index("check_stale_scss.py")]
|
|
|
|
|
self.assertGreater(
|
|
|
|
|
avant.rfind("run_on_terminal("),
|
|
|
|
|
avant.rfind("todo_upgrade_execute("),
|
|
|
|
|
"l'outil repasse par l'exécuteur à tube",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[FIX] migration: reset the stale SCSS after the bump, not before
The fix was offered while the checkout was still on the previous
version. Answering « a » ran reset_asset in an Odoo 12 shell, which has
no web_editor.assets: KeyError, nothing changed, and the migration went
on to break at the next bump — measured on a real run.
Predicting early is right; fixing early is not. The early call is now
--report-only, and a second call comes after the bump, on the upgraded
database, where the checkout can do it. The tool also refuses on its
own: it reads the checkout sources for reset_asset rather than trusting
a version number, and says when to come back.
--- FR ---
[FIX] migration : réinitialiser le SCSS périmé après le palier, pas avant
La correction était proposée alors que le checkout était encore sur la
version précédente. Répondre « a » lançait reset_asset dans un shell
Odoo 12, sans web_editor.assets : KeyError, rien de modifié, et la
migration continuait jusqu'à casser au palier suivant. Mesuré.
Prédire tôt est juste ; corriger tôt ne l'est pas. L'appel précoce est
désormais --report-only, et un second vient après le palier, sur la
base montée de version, là où le checkout sait le faire. L'outil refuse
aussi de lui-même : il cherche reset_asset dans les sources du checkout
plutôt que de se fier à un numéro, et dit quand revenir.
Assisted-by: Claude Opus 5
(cherry picked from commit 13d4d0a33c63efcbafb5fb29313a5377ba5f508e)
2026-08-15 16:34:26 -04:00
|
|
|
class TestTheFixCannotRunTooEarly(unittest.TestCase):
|
|
|
|
|
"""Corriger exige la version d'ARRIVÉE, pas celle de départ.
|
|
|
|
|
|
|
|
|
|
Mesuré sur une vraie migration : la question a été posée avant le palier,
|
|
|
|
|
alors que le checkout était encore sur odoo12.0. Répondre « a » a lancé
|
|
|
|
|
reset_asset dans un shell Odoo 12, qui ne connaît pas `web_editor.assets`
|
|
|
|
|
— KeyError, rien de modifié, et la migration a continué jusqu'à casser au
|
|
|
|
|
palier suivant.
|
|
|
|
|
|
|
|
|
|
Prédire tôt reste juste. C'est corriger tôt qui ne l'est pas.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
from script.todo import todo_i18n
|
|
|
|
|
|
|
|
|
|
self.addCleanup(
|
|
|
|
|
setattr, todo_i18n, "_current_lang", todo_i18n._current_lang
|
|
|
|
|
)
|
|
|
|
|
todo_i18n._current_lang = "en"
|
|
|
|
|
|
|
|
|
|
def test_a_version_without_reset_asset_is_refused(self):
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
root = tempfile.mkdtemp()
|
|
|
|
|
old = os.path.join(root, "odoo12.0", "addons", "web_editor", "models")
|
|
|
|
|
os.makedirs(old)
|
|
|
|
|
with open(os.path.join(old, "assets.py"), "w") as handle:
|
|
|
|
|
handle.write("class Assets:\n pass\n")
|
|
|
|
|
self.assertFalse(scss.reset_supported(os.path.join(root, "odoo12.0")))
|
|
|
|
|
|
|
|
|
|
def test_a_version_with_it_is_allowed(self):
|
|
|
|
|
import os
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
root = tempfile.mkdtemp()
|
|
|
|
|
new = os.path.join(root, "odoo13.0", "addons", "web_editor", "models")
|
|
|
|
|
os.makedirs(new)
|
|
|
|
|
with open(os.path.join(new, "assets.py"), "w") as handle:
|
|
|
|
|
handle.write("def reset_asset(self, url, bundle):\n pass\n")
|
|
|
|
|
self.assertTrue(scss.reset_supported(os.path.join(root, "odoo13.0")))
|
|
|
|
|
|
|
|
|
|
def test_an_unknown_checkout_does_not_block(self):
|
|
|
|
|
# Rien pour trancher : refuser sur une supposition empêcherait de
|
|
|
|
|
# corriger là où c'est possible.
|
|
|
|
|
self.assertTrue(scss.reset_supported("odoo_no_such_dir_zz"))
|
|
|
|
|
|
|
|
|
|
def test_the_prompt_hides_the_fix_when_it_cannot_work(self):
|
|
|
|
|
# Offrir un choix qui échouera, c'est le faire prendre.
|
|
|
|
|
import contextlib
|
|
|
|
|
import io
|
|
|
|
|
|
|
|
|
|
original = scss.reset_supported
|
|
|
|
|
scss.reset_supported = lambda odoo_dir=None: False
|
|
|
|
|
self.addCleanup(setattr, scss, "reset_supported", original)
|
|
|
|
|
finding = {
|
|
|
|
|
"id": 1,
|
|
|
|
|
"url": "/a/b.custom.web.assets_frontend.scss",
|
|
|
|
|
"missing": ["x"],
|
|
|
|
|
"custom": "a",
|
|
|
|
|
"base_url": "/a/b.scss",
|
|
|
|
|
"bundle": "web.assets_frontend",
|
|
|
|
|
"module_path": None,
|
|
|
|
|
"module_content": "",
|
|
|
|
|
"version_dir": "odoo13.0",
|
|
|
|
|
"database": "db",
|
|
|
|
|
}
|
|
|
|
|
out = io.StringIO()
|
|
|
|
|
with contextlib.redirect_stdout(out):
|
|
|
|
|
wrote = scss.prompt([finding], "db", ask=lambda p: "a")
|
|
|
|
|
self.assertFalse(wrote)
|
|
|
|
|
self.assertIn("KeyError", out.getvalue())
|
|
|
|
|
self.assertNotIn("a = reset", out.getvalue())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheMigrationAsksAtTheRightMoment(unittest.TestCase):
|
|
|
|
|
"""Prédire avant le palier, corriger après — jamais l'inverse."""
|
|
|
|
|
|
|
|
|
|
def source(self):
|
|
|
|
|
import inspect
|
|
|
|
|
|
|
|
|
|
from script.todo.todo_upgrade import TodoUpgrade
|
|
|
|
|
|
|
|
|
|
return inspect.getsource(TodoUpgrade.execute_odoo_upgrade)
|
|
|
|
|
|
|
|
|
|
def test_the_early_call_cannot_offer_the_fix(self):
|
|
|
|
|
source = self.source()
|
|
|
|
|
premier = source.index("check_stale_scss.py")
|
|
|
|
|
fenetre = source[premier : premier + 400]
|
|
|
|
|
self.assertIn("--report-only", fenetre)
|
|
|
|
|
|
|
|
|
|
def test_a_second_call_comes_after_the_bump(self):
|
|
|
|
|
# Sans elle, la prédiction n'aurait jamais de suite : on saurait ce
|
|
|
|
|
# qui va casser sans jamais pouvoir le réparer.
|
|
|
|
|
source = self.source()
|
|
|
|
|
self.assertEqual(source.count("check_stale_scss.py"), 2)
|
|
|
|
|
second = source.rindex("check_stale_scss.py")
|
|
|
|
|
self.assertGreater(second, source.index("state_4_upgrade_odoo_lst"))
|
|
|
|
|
|
|
|
|
|
def test_the_second_call_targets_the_upgraded_database(self):
|
|
|
|
|
source = self.source()
|
|
|
|
|
second = source.rindex("check_stale_scss.py")
|
|
|
|
|
fenetre = source[second : second + 300]
|
|
|
|
|
self.assertIn("database_name_upgrade", fenetre)
|
|
|
|
|
self.assertNotIn("--report-only", fenetre)
|
|
|
|
|
|
|
|
|
|
|
[ADD] migration: read the stale SCSS, then fix it, without leaving the tool
The report named the attachment and printed a command to paste. Deciding
« reset it » still meant accepting to lose one did not know what: the
diff against the module file is the only thing resetting gives up.
The tool now shows it and asks. --diff prints it, --tui browses it,
--apply resets without asking; with no flag and a terminal it asks, and
looking does not answer — the prompt comes back after each read.
--apply writes the copies under private/ first: reset_asset deletes the
attachment, so without that the customized lines would be nowhere.
The migration runs it on the real terminal, or none of this would be
reachable from there — the same pipe that was closing the TUI.
--- FR ---
[ADD] migration : lire le SCSS périmé, puis le corriger, sans sortir
Le rapport nommait la pièce jointe et imprimait une commande à coller.
Répondre « réinitialise » revenait encore à accepter de perdre on ne
sait quoi : l'écart avec le fichier du module est la seule chose que la
réinitialisation abandonne.
L'outil le montre et pose la question. --diff l'imprime, --tui le
parcourt, --apply réinitialise sans demander ; sans drapeau et devant
un terminal il demande, et regarder ne répond pas — l'invite revient
après chaque lecture. --apply écrit d'abord les copies sous private/ :
reset_asset supprime la pièce jointe, sans quoi les lignes
personnalisées ne seraient plus nulle part.
La migration le lance sur le vrai terminal, sinon rien de tout cela n'y
serait atteignable — le tube même qui fermait la TUI.
Assisted-by: Claude Opus 5
(cherry picked from commit 9e6c88622a74f7be408a8b1c523838eeadda1da3)
2026-08-15 03:41:56 -04:00
|
|
|
class TestThePromptStaysOutOfAPipe(unittest.TestCase):
|
|
|
|
|
def test_it_only_asks_in_front_of_a_terminal(self):
|
|
|
|
|
# Une invite dans un tube bloquerait l'appelant sur une question que
|
|
|
|
|
# personne ne voit.
|
|
|
|
|
with open(scss.__file__) as handle:
|
|
|
|
|
source = handle.read()
|
|
|
|
|
self.assertIn("sys.stdin.isatty()", source)
|
|
|
|
|
|
[ADD] migration: predict which customized SCSS the next bump breaks
Customized SCSS lives in ir_attachment, frozen the day it is written,
still using the variables of that version. A bump can rename them:
website declared $o-theme-font-number in 12.0 and replaced the whole
mechanism in 13.0, so a 2020 customization stopped the frontend
bundle. Same shape as a stale COW view.
Reading the stored SCSS against the target sources answers before the
bump and names the attachment. Booting Odoo and opening a page answers
after, with « Style error » and no name — on a page a broken frontend
bundle is exactly what keeps you from reaching.
Measured: on the pre-bump database, targeting 13.0, it predicts the
failure; against its own 12.0 sources it reports nothing. That noise
floor is what makes it usable — the first version cried wolf on six
mixin parameters and named @include arguments.
--- FR ---
[ADD] migration : prédire quel SCSS personnalisé le palier va casser
Un SCSS personnalisé vit dans ir_attachment, figé le jour où il est
écrit, employant encore les variables de cette version-là. Un palier
peut les renommer : website déclarait $o-theme-font-number en 12.0 et a
remplacé le mécanisme en 13.0, arrêtant le bundle sur une
personnalisation de 2020. Même forme qu'une vue COW périmée.
Lire le SCSS stocké contre les sources cibles répond AVANT le palier et
nomme la pièce jointe. Démarrer Odoo et ouvrir une page répond après,
par « Style error » sans nom — et un bundle cassé est justement ce qui
empêche d'atteindre la page.
Mesuré : sur la base d'avant le palier, cible 13.0, il prédit la
panne ; contre ses propres sources 12.0 il ne signale rien. Ce bruit de
fond nul est ce qui le rend utilisable — la première version criait à
tort sur six paramètres de mixin et arguments nommés.
Assisted-by: Claude Opus 5
(cherry picked from commit c6611ee1431cfee94e321536d20052b8cca3ec10)
2026-08-15 03:21:25 -04:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|