erplibre/script/odoo/migration/smoke_public_url.py

770 lines
26 KiB
Python
Raw Normal View History

[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -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)
"""Request every public URL of a migrated database, and report what breaks.
Why this exists
---------------
A migration can finish, load every module, and still serve a 500 on a page
nobody thought to open. Measured on a real one: ``/blog/<blog>/post/<post>``
answered 500 because a COW copy frozen on the previous version no longer held
the section a child view xpaths into. Nothing in the migration log said so —
the module loading had succeeded.
Where the list comes from
-------------------------
``/sitemap.xml``: the list Odoo itself publishes for search engines, built by
``website.enumerate_pages()``. It covers the controller routes declared with
``sitemap=True`` and the records behind them — pages, blog posts, products.
It cannot be read from ``odoo-bin shell``: ``enumerate_pages()`` asks for
``http.root.get_db_router(request.db)`` and raises « object unbound » without
a real request. So the server is started, asked, and stopped.
What counts as a failure
------------------------
Any status >= 400. The sitemap is the PUBLIC list: a page listed there and
answering 403 or 404 is as wrong as one answering 500, just less loud.
Exit codes: 0 every URL answered, 1 some failed, 2 the tool failed.
"""
import argparse
import os
import re
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
import signal
import socket
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
import subprocess
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
sys.path.append(
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
)
try:
from script.todo.todo_i18n import t
except Exception: # pragma: no cover - repli si i18n indisponible
def t(key: str) -> str:
return key
[ADD] migration: make Enter mean the answer you always give Auto-run promised to "take the default after five seconds", but every default was EMPTY: it took nothing. Worse, half the prompts of a migration are asked by separate processes — the theme uninstaller, the stale-SCSS detector, the smoke test. They knew nothing of auto-run and waited forever for a keystroke that never came. The countdown now lives in one file and travels through the ENVIRONMENT, the only channel a fork shares. Enter takes the default everywhere, not only under auto-run: a prompt that prints (Y/n) and does otherwise is worse than no prompt. Every text was rewritten to say what Enter does, and every default kept an explicit way out. Two defaults now write. Both are tenable only because the backup runs first, and a test locks that ORDER rather than trusting a promise. --- FR --- [ADD] migration : faire d'Entrée la réponse qu'on donne toujours L'auto-exécution promettait « le défaut après cinq secondes », mais tous les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des invites d'une migration sont posées par d'autres processus — le désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils ignoraient l'auto-exécution et attendaient sans fin une frappe. Le compte à rebours tient désormais dans un seul fichier et voyage par l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut partout, pas seulement en auto : une invite qui affiche (Y/n) et fait l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée, et chaque défaut garde une issue explicite. Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse. Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
try:
from script.todo import auto_ask
except Exception: # pragma: no cover - repli si le pilote est absent
auto_ask = None
[FIX] migration: never ask a question the terminal cannot show The theme uninstaller ends by asking what to do with the leftovers, and the migration ran it through the capturing executor. Its stdout was a pipe, Python buffers by blocks, so the prompt stayed invisible while the process waited. It reads as a freeze: you press Enter blind, the first keystroke answers unseen and the extra ones fall into the next question. Two locks, because one is not enough. The migration runs the script on the real terminal. And the three tools that ask now require BOTH ends: something to read the answer from, and something to show the question on. Guarding on stdin alone was the bug — measured, stdin tty=True with stdout tty=False, and the question was asked into a pipe. --- FR --- [FIX] migration : ne jamais poser une question que le terminal ne montre pas Le désinstalleur de thème finit par demander quoi faire des restes, et la migration le lançait par l'exécuteur qui capture. Sa sortie partait dans un tube, Python bufferise par blocs, et l'invite restait invisible pendant l'attente. Cela se lit comme un blocage : on tape Entrée à l'aveugle, la première frappe répond sans être vue et les suivantes tombent dans la question d'après. Deux verrous, car un seul ne suffit pas. La migration lance le script sur le vrai terminal. Et les trois outils qui questionnent exigent désormais les DEUX bouts : de quoi lire la réponse, et de quoi montrer la question. Ne garder que stdin était le défaut — mesuré, stdin tty=True et stdout tty=False, la question partait dans un tube. Assisted-by: Claude Opus 5
2026-08-17 02:41:40 -04:00
def can_ask():
"""Peut-on poser une question ICI ?
Il faut deux choses, pas une : de quoi LIRE la réponse (stdin sur un
terminal) et de quoi MONTRER la question (stdout aussi). Ne tester que
stdin laisse poser une invite qui part dans un tube : elle reste en
tampon, invisible, pendant que le processus attend — on croit à un
blocage et l'on tape Entrée à l'aveugle.
"""
return sys.stdin.isatty() and sys.stdout.isatty()
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
RE_LOC = re.compile(r"<loc>\s*([^<\s]+)\s*</loc>", re.I)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
# « [view_id: 3282, xml_id: n/a, model: n/a, parent_id: 3281] ». Seule la
# phrase qui précède est traduite ; cette ligne-ci ne l'est pas, donc la lire
# marche dans les deux langues. Le PARENT est le coupable : c'est la copie
# figée dans laquelle l'enfant ne trouve plus son xpath.
RE_CONTEXT = re.compile(r"\[view_id: (\d+),.*?parent_id: (\d+)\]")
2026-08-17 07:59:28 -04:00
# « Template: website.submenu ». Une QWebException de RENDU ne porte pas le
# bloc [view_id …] : elle nomme le gabarit. Mesuré au palier 17 — une copie
# figée appelait `submenu.clean_url()`, méthode renommée `_clean_url()` dans
# la version, et 34 URL sur 37 rendaient 500 sans que rien ne désigne la vue
# fautive.
RE_TEMPLATE = re.compile(r"^Template:\s*([\w.]+)\s*$", re.M)
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
# Un port à part : la migration tourne souvent à côté d'une instance vivante,
# et lui voler 8069 ferait échouer le test pour une raison sans rapport.
DEFAULT_PORT = 8169
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
def run_psql(database, sql):
"""Lecture seule, garantie par le serveur PostgreSQL lui-même."""
env = os.environ.copy()
env["PGOPTIONS"] = "-c default_transaction_read_only=on"
env["PSQLRC"] = ""
done = subprocess.run(
["psql", "-X", "-w", "-d", database, "-tAF", "\x1f", "-c", sql],
capture_output=True,
text=True,
env=env,
)
if done.returncode:
return []
return [line.split("\x1f") for line in done.stdout.splitlines() if line]
def start_server(database, port, config_path="./config.conf", log_path=None):
"""Démarrer Odoo, son journal dans un FICHIER.
Pas un tube : Python bufferise par blocs quand sa sortie n'est pas un
terminal, et un fil de lecture court alors après des lignes qui n'ont
pas encore été écrites. Mesuré — 24 lignes vues sur 198, et la trace qui
nomme la vue fautive faisait partie des absentes. Un fichier se relit
entièrement, quand on veut.
"""
handle = open(log_path, "w", encoding="utf-8") if log_path else None
server = subprocess.Popen(
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
[
"./run.sh",
"-c",
config_path,
"-d",
database,
"--http-port",
str(port),
"--log-level=warn",
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
# Deux lignes par requête, et elles portent le CHEMIN. Sans
# elles une trace du journal ne peut être rattachée à rien :
# on l'attribuait à la dernière requête, qui n'était pas la
# sienne. Mesuré — /my accusé d'une panne appartenant à une
# application testée après lui.
"--log-handler=werkzeug:INFO",
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
],
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
stdout=handle or subprocess.DEVNULL,
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
stderr=subprocess.STDOUT,
text=True,
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
# Son propre groupe de processus : « ./run.sh » est un script bash
# qui ne transmet rien à son enfant. Un terminate() sur lui tuait le
# script et laissait odoo-bin vivant, tenant le port. L'essai suivant
# démarrait alors un serveur qui ne pouvait pas se lier, et
# interrogeait sans le savoir CELUI D'AVANT — mesuré, deux fois.
start_new_session=True,
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
server.erplibre_log = handle
return server
def read_log(log_path):
"""Le journal du serveur, tel qu'écrit jusqu'ici."""
if not log_path or not os.path.isfile(log_path):
return []
with open(log_path, "r", encoding="utf-8", errors="replace") as handle:
return handle.read().splitlines()
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
def fetch(url, timeout=30):
"""(statut, corps). Statut 0 quand la connexion elle-même échoue."""
try:
with urllib.request.urlopen(url, timeout=timeout) as answer:
return answer.getcode(), answer.read().decode(
"utf-8", errors="replace"
)
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode("utf-8", errors="replace")
except Exception:
return 0, ""
def wait_ready(base_url, timeout=180, sleep=2):
"""Attendre que le serveur réponde. False s'il n'est jamais venu."""
deadline = time.time() + timeout
while time.time() < deadline:
status, _body = fetch(base_url + "/web/login", timeout=5)
if status:
return True
time.sleep(sleep)
return False
def sitemap_urls(base_url):
"""Les URL du sitemap, index compris, ramenées sur l'hôte local.
Le sitemap porte le domaine du site (technolibre.ca) ; on teste une base
servie en local. Garder le domaine ferait interroger la production —
c'est le genre d'erreur qui ne se voit qu'après.
"""
status, body = fetch(base_url + "/sitemap.xml")
if not status or status >= 400:
return [], status
lst_loc = RE_LOC.findall(body)
# Un index de sitemaps ne contient que des sitemaps : on descend d'un cran.
if "<sitemapindex" in body.lower():
lst_page = []
for loc in lst_loc:
_status, sub = fetch(local_url(base_url, loc))
lst_page.extend(RE_LOC.findall(sub))
lst_loc = lst_page
seen, lst_url = set(), []
for loc in lst_loc:
url = local_url(base_url, loc)
if url not in seen:
seen.add(url)
lst_url.append(url)
return lst_url, status
def local_url(base_url, loc):
"""Remplacer le schéma et l'hôte du sitemap par ceux qu'on teste."""
parsed = urllib.parse.urlparse(loc)
path = parsed.path or "/"
if parsed.query:
path += "?" + parsed.query
return base_url.rstrip("/") + path
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
# Odoo écrit sa trace APRÈS avoir répondu, et par le tube d'un script shell :
# mesuré, elle peut arriver près de trois secondes plus tard. Une demi-seconde
# concluait « aucune vue en cause » sur des pages qui en nommaient une.
LOG_DELAY = 3.0
def attach_missing_parents(lst_failure, lst_log):
"""Repêcher les contextes arrivés trop tard pour leur tranche.
Le découpage par URL est une commodité, pas une garantie : le journal est
asynchrone. Ce second passage lit TOUT ce qui a été capturé et rattache
ce qui n'avait été rattaché à rien — mieux vaut un coupable mal attribué
qu'un coupable perdu.
"""
known = {pid for _u, _s, lst in lst_failure for pid in lst}
extra = []
for line in lst_log:
[FIX] migration: reset the copy that is actually stale, and say when a key misses Answering « all » reset one copy of two and reported success. Two defects behind it, both mine. A requested key matching no finding did nothing, silently: the tool returned early on « nothing drifted » and never looked. Detection is differential — it only sees a copy whose CHILD breaks — so a stale copy without children escapes it. A key can now be reset outside detection, an unknown one is named, and the exit code is 2. And the culprit is not always the parent. On /contactus the parent was identical to its module view; the CHILD held the stale arch. Both are proposed now, parent first — it is the more common case. Measured on the migration: 33 of 33 public URLs answer. --- FR --- [FIX] migration : réinitialiser la copie vraiment périmée, et signaler une clé sans objet Répondre « toutes » réinitialisait une copie sur deux et annonçait un succès. Deux défauts derrière, tous deux à moi. Une clé demandée ne correspondant à aucun constat ne faisait rien, en silence : l'outil sortait sur « rien n'a dérivé » sans jamais chercher. La détection est différentielle — elle ne voit qu'une copie dont un ENFANT casse — donc une copie périmée sans enfant lui échappe. Une clé peut désormais être réinitialisée hors détection, une clé inconnue est nommée, et le code de sortie vaut 2. Et le coupable n'est pas toujours le parent. Sur /contactus, le parent était identique à sa vue module ; c'est l'ENFANT qui portait l'arch périmée. Les deux sont proposés, le parent d'abord — le cas le plus fréquent. Mesuré sur la migration : 33 URL publiques sur 33 répondent. Assisted-by: Claude Opus 5
2026-08-17 04:49:32 -04:00
# LES DEUX : le parent ET l'enfant. Mesuré sur /contactus — le
# parent était identique à sa vue module, et c'est l'ENFANT qui
# portait l'arch périmée. Ne proposer que le parent envoyait
# réinitialiser une copie qui allait déjà bien.
for view_id, parent_id in RE_CONTEXT.findall(line):
for candidate in (parent_id, view_id):
if candidate not in known and candidate not in extra:
extra.append(candidate)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
if not extra:
return lst_failure
rebuilt = []
placed = False
for url, status, lst_parent in lst_failure:
if not lst_parent and not placed:
lst_parent = list(extra)
placed = True
rebuilt.append((url, status, lst_parent))
if not placed and rebuilt:
url, status, lst_parent = rebuilt[0]
rebuilt[0] = (url, status, lst_parent + extra)
return rebuilt
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
def check_urls(lst_url, timeout=30):
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
"""[(url, statut, [])] pour celles qui ont échoué.
Les vues en cause sont rattachées après coup, en relisant le journal du
serveur : elles y arrivent quand Odoo vide son tampon, pas quand la
requête revient.
"""
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
lst_failure = []
for url in lst_url:
status, _body = fetch(url, timeout=timeout)
if status == 0 or status >= 400:
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
lst_failure.append((url, status, []))
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
return lst_failure
2026-08-17 07:59:28 -04:00
def template_keys(lst_log, database):
"""Les gabarits nommés par une QWebException, s'ils ont une copie COW.
Nommer une clé sans copie enverrait réinitialiser une vue module — donc
ne rien faire, en silence. On ne propose que ce qui peut l'être.
"""
lst_key = []
for line in lst_log:
for key in RE_TEMPLATE.findall(line):
if key not in lst_key:
lst_key.append(key)
if not lst_key:
return []
quoted = ",".join("'" + k.replace("'", "''") + "'" for k in lst_key)
rows = run_psql(
database,
"SELECT DISTINCT key FROM ir_ui_view"
f" WHERE website_id IS NOT NULL AND key IN ({quoted});",
)
with_copy = {row[0] for row in rows if row and row[0]}
return [key for key in lst_key if key in with_copy]
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
def culprit_keys(database, lst_failure):
"""Les clés des vues parentes mises en cause, sans doublon.
C'est ce qu'on passe à reset_stale_cow_views : il travaille par clé, et
relever un identifiant dans une trace pour le traduire à la main est
exactement la recopie où l'on se trompe.
"""
lst_id = []
for _url, _status, lst_parent in lst_failure:
for parent_id in lst_parent:
if parent_id not in lst_id:
lst_id.append(parent_id)
if not lst_id:
return []
ids = ",".join(str(int(x)) for x in lst_id)
rows = run_psql(
database,
f"SELECT id, key, website_id FROM ir_ui_view WHERE id IN ({ids})"
" AND key IS NOT NULL ORDER BY id;",
)
lst_key = []
for row in rows:
if len(row) >= 2 and row[1] and row[1] not in lst_key:
lst_key.append(row[1])
return lst_key
def render(lst_url, lst_failure, lst_key=None):
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
if not lst_url:
return f"⚠️ {t('The sitemap listed no URL: nothing was tested.')}\n"
if not lst_failure:
return (
f"✅ -> {len(lst_url)} {t('public URL(s) answered without error.')}"
"\n"
)
lines = [
f"❌ {len(lst_failure)} {t('of')} {len(lst_url)}"
f" {t('public URL(s) failed')} :"
]
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
for url, status, lst_parent in lst_failure:
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
label = status or t("no answer")
lines.append(f" [{label}] {url}")
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
if lst_parent:
lines.append(
f" {t('parent view(s) in cause')} :"
f" {', '.join(lst_parent)}"
)
if lst_key:
lines.append(
f" {t('Those parents are copies frozen on an older version;')}"
f" {t('resetting them onto the module view is the fix')} :"
)
for key in lst_key:
lines.append(f" {key}")
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
lines.append(
f" {t('A page listed for search engines that does not answer is')}"
f" {t('a page your visitors do not reach either.')}"
)
return "\n".join(lines) + "\n"
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
def apply_reset(database, lst_key):
"""Réinitialiser ces copies sur leur vue module. ÉCRIT en base.
On délègue à reset_stale_cow_views : il sauvegarde l'arch précédente
avant d'écrire, et c'est déjà lui qu'on documente partout ailleurs. En
refaire une seconde version ici, c'est se donner deux comportements à
tenir d'accord.
"""
cmd = [
sys.executable,
os.path.join(
"script", "odoo", "migration", "reset_stale_cow_views.py"
),
"-d",
database,
]
for key in lst_key:
cmd += ["--reset", key]
cmd.append("--apply")
done = subprocess.run(cmd, capture_output=True, text=True)
return done.returncode, done.stdout + done.stderr
[ADD] migration: make Enter mean the answer you always give Auto-run promised to "take the default after five seconds", but every default was EMPTY: it took nothing. Worse, half the prompts of a migration are asked by separate processes — the theme uninstaller, the stale-SCSS detector, the smoke test. They knew nothing of auto-run and waited forever for a keystroke that never came. The countdown now lives in one file and travels through the ENVIRONMENT, the only channel a fork shares. Enter takes the default everywhere, not only under auto-run: a prompt that prints (Y/n) and does otherwise is worse than no prompt. Every text was rewritten to say what Enter does, and every default kept an explicit way out. Two defaults now write. Both are tenable only because the backup runs first, and a test locks that ORDER rather than trusting a promise. --- FR --- [ADD] migration : faire d'Entrée la réponse qu'on donne toujours L'auto-exécution promettait « le défaut après cinq secondes », mais tous les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des invites d'une migration sont posées par d'autres processus — le désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils ignoraient l'auto-exécution et attendaient sans fin une frappe. Le compte à rebours tient désormais dans un seul fichier et voyage par l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut partout, pas seulement en auto : une invite qui affiche (Y/n) et fait l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée, et chaque défaut garde une issue explicite. Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse. Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
DEFAULT_ANSWER = "a"
def prompt(database, lst_failure, lst_key, ask=None):
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
"""Proposer de corriger, puis dire ce qu'il reste. Rend les clés traitées.
Détecter sans offrir le geste, c'est laisser relever des identifiants
dans une trace pour les traduire en clés à la main — au moment précis où
l'on veut juste que la page réponde.
"""
if not lst_key:
print(f"ℹ -> {t('No parent view named: nothing to offer.')}")
return []
[ADD] migration: make Enter mean the answer you always give Auto-run promised to "take the default after five seconds", but every default was EMPTY: it took nothing. Worse, half the prompts of a migration are asked by separate processes — the theme uninstaller, the stale-SCSS detector, the smoke test. They knew nothing of auto-run and waited forever for a keystroke that never came. The countdown now lives in one file and travels through the ENVIRONMENT, the only channel a fork shares. Enter takes the default everywhere, not only under auto-run: a prompt that prints (Y/n) and does otherwise is worse than no prompt. Every text was rewritten to say what Enter does, and every default kept an explicit way out. Two defaults now write. Both are tenable only because the backup runs first, and a test locks that ORDER rather than trusting a promise. --- FR --- [ADD] migration : faire d'Entrée la réponse qu'on donne toujours L'auto-exécution promettait « le défaut après cinq secondes », mais tous les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des invites d'une migration sont posées par d'autres processus — le désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils ignoraient l'auto-exécution et attendaient sans fin une frappe. Le compte à rebours tient désormais dans un seul fichier et voyage par l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut partout, pas seulement en auto : une invite qui affiche (Y/n) et fait l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée, et chaque défaut garde une issue explicite. Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse. Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
if ask is None:
# Lancé à part par la migration : sans lecteur temporisé, cette
# question arrêtait net une exécution automatique.
ask = (
auto_ask.make_ask(DEFAULT_ANSWER)
if auto_ask
else (lambda prompt="": input(prompt) or DEFAULT_ANSWER)
)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
print(f"\n✨ {t('Copies to reset onto their module view')} :")
for index, key in enumerate(lst_key, start=1):
print(f" [{index}] {key}")
print(f" [a] {t('All of the list above')}")
answer = (
ask(
f"💬 {t('Which one(s) to reset?')}"
[ADD] migration: make Enter mean the answer you always give Auto-run promised to "take the default after five seconds", but every default was EMPTY: it took nothing. Worse, half the prompts of a migration are asked by separate processes — the theme uninstaller, the stale-SCSS detector, the smoke test. They knew nothing of auto-run and waited forever for a keystroke that never came. The countdown now lives in one file and travels through the ENVIRONMENT, the only channel a fork shares. Enter takes the default everywhere, not only under auto-run: a prompt that prints (Y/n) and does otherwise is worse than no prompt. Every text was rewritten to say what Enter does, and every default kept an explicit way out. Two defaults now write. Both are tenable only because the backup runs first, and a test locks that ORDER rather than trusting a promise. --- FR --- [ADD] migration : faire d'Entrée la réponse qu'on donne toujours L'auto-exécution promettait « le défaut après cinq secondes », mais tous les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des invites d'une migration sont posées par d'autres processus — le désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils ignoraient l'auto-exécution et attendaient sans fin une frappe. Le compte à rebours tient désormais dans un seul fichier et voyage par l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut partout, pas seulement en auto : une invite qui affiche (Y/n) et fait l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée, et chaque défaut garde une issue explicite. Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse. Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
f" ({t('numbers separated by commas, Enter = all, n =')}"
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
f" {t('nothing')}) : "
)
.strip()
.lower()
)
[ADD] migration: make Enter mean the answer you always give Auto-run promised to "take the default after five seconds", but every default was EMPTY: it took nothing. Worse, half the prompts of a migration are asked by separate processes — the theme uninstaller, the stale-SCSS detector, the smoke test. They knew nothing of auto-run and waited forever for a keystroke that never came. The countdown now lives in one file and travels through the ENVIRONMENT, the only channel a fork shares. Enter takes the default everywhere, not only under auto-run: a prompt that prints (Y/n) and does otherwise is worse than no prompt. Every text was rewritten to say what Enter does, and every default kept an explicit way out. Two defaults now write. Both are tenable only because the backup runs first, and a test locks that ORDER rather than trusting a promise. --- FR --- [ADD] migration : faire d'Entrée la réponse qu'on donne toujours L'auto-exécution promettait « le défaut après cinq secondes », mais tous les défauts étaient VIDES : elle ne prenait rien. Pire, la moitié des invites d'une migration sont posées par d'autres processus — le désinstalleur de thème, le détecteur de SCSS figé, le test de fumée. Ils ignoraient l'auto-exécution et attendaient sans fin une frappe. Le compte à rebours tient désormais dans un seul fichier et voyage par l'ENVIRONNEMENT, le seul canal qu'un fork partage. Entrée vaut le défaut partout, pas seulement en auto : une invite qui affiche (Y/n) et fait l'inverse est pire que pas d'invite. Chaque texte dit ce que fait Entrée, et chaque défaut garde une issue explicite. Deux défauts écrivent. Ils ne tiennent que parce que la sauvegarde passe d'abord, et un test verrouille cet ORDRE plutôt qu'une promesse. Assisted-by: Claude Opus 5
2026-08-17 20:47:17 -04:00
# « n », et non plus le vide : Entrée vaut « toutes » maintenant, et une
# sortie sans mot pour dire non serait une sortie sans issue.
if not answer or answer == "n":
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
print(f"ℹ -> {t('Kept. Nothing was reset.')}")
return []
if answer == "a":
lst_chosen = list(lst_key)
else:
lst_chosen = []
for part in answer.replace(" ", "").split(","):
if part.isdigit() and 1 <= int(part) <= len(lst_key):
lst_chosen.append(lst_key[int(part) - 1])
if not lst_chosen:
print(f"⚠️ {t('Unknown choice, nothing was reset.')}")
return []
status, output = apply_reset(database, lst_chosen)
print(output.strip()[-2000:])
if status == 2:
print(f"❌ {t('Reset failed, nothing was changed.')}")
return []
return lst_chosen
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
def render_internal(internal):
"""Afficher le rapport du back-office. Rend True s'il a échoué.
Ne rien afficher quand la base n'a pas été neutralisée serait laisser
croire que le back-office a été testé et qu'il va bien.
"""
if internal is None:
return False
if "skipped" in internal:
print(f"\nℹ️ {t('Back office not browsed')} : {internal['skipped']}")
return False
import smoke_internal_ui
print(smoke_internal_ui.render(internal["results"], internal["failures"]))
return bool(internal["failures"])
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
def attach_internal_log(internal_report, log_path):
"""Donner à la passe back-office la trace que sa page ne montrait pas.
APRÈS l'arrêt du serveur, et pas avant : un serveur qui écrit dans un
fichier bufferise et ne vide qu'en s'arrêtant. La page d'erreur d'Odoo
en production, elle, se contente de « 500: Internal Server Error » —
de quoi constater, pas de quoi réparer.
"""
if not internal_report or not internal_report.get("failures"):
return
try:
import smoke_internal_ui
except ImportError:
return
smoke_internal_ui.attach_log_reason(
internal_report["failures"], read_log(log_path)
)
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
def internal_phase(
base_url,
database,
enabled=True,
login="test",
password="test",
limit=20,
every_menu=False,
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
lst_portal=None,
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
):
"""Le back-office, si la base a été neutralisée. Rend None sinon.
La condition n'est pas un drapeau mais l'utilisateur lui-même : la
neutralisation pose un compte `test` avec les groupes du
superutilisateur, et c'est le seul moyen d'entrer sans connaître le mot
de passe de quelqu'un. Une migration reprise a pu sauter l'étape, et le
fichier de progression dirait quand même « fait ».
Un échec ICI ne doit pas emporter le test public : il est mesuré, il
est rapporté, mais le rapport des URL publiques a sa propre valeur.
"""
if not enabled:
return None
try:
import smoke_internal_ui
except ImportError:
return None
etat = smoke_internal_ui.user_state(database, login, run_psql=run_psql)
if etat == "absent":
return {"skipped": t("no test user: the database was not neutralized")}
if etat != "present":
# « je ne sais pas » n'est pas « tout va bien » : le dire autrement
# ferait passer un back-office jamais ouvert pour un back-office sain.
return {"skipped": t("could not tell whether the test user exists")}
try:
lst_result, lst_failure = smoke_internal_ui.run(
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
base_url,
database,
login,
password,
limit,
every_menu=every_menu,
lst_portal=(
smoke_internal_ui.DEFAULT_PORTAL_PATHS
if lst_portal is None
else lst_portal
),
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
)
except RuntimeError as exc:
return {"skipped": str(exc)}
return {"results": lst_result, "failures": lst_failure}
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
def run(
database,
port,
config_path,
limit=None,
timeout=30,
boot=180,
interactive=False,
auto_apply=False,
ask=input,
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
internal=True,
internal_login="test",
internal_password="test",
internal_limit=20,
every_menu=False,
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
portal=None,
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
):
"""Démarrer, interroger, arrêter, LIRE, éventuellement corriger, revérifier.
L'ordre porte tout le correctif : un serveur qui écrit dans un fichier
bufferise et ne vide qu'en s'arrêtant. Lire avant l'arrêt donnait
vingt-quatre lignes de démarrage et zéro trace — donc « aucune vue en
cause » sur des pages qui en nommaient une. Mesuré deux fois avant d'être
compris.
"""
import tempfile
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
base_url = f"http://127.0.0.1:{port}"
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
log_path = os.path.join(
tempfile.gettempdir(), f"erplibre_smoke_{database}_{port}.log"
)
if port_is_taken(port):
raise RuntimeError(
f"{t('Something already listens on port')} {port} :"
f" {t('it would be tested instead of this database.')}"
)
server = start_server(database, port, config_path, log_path=log_path)
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
try:
if not wait_ready(base_url, timeout=boot):
raise RuntimeError(
f"{t('The server never answered on')} {base_url}"
)
lst_url, status = sitemap_urls(base_url)
if not status:
raise RuntimeError(f"{t('Could not read')} {base_url}/sitemap.xml")
if limit:
lst_url = lst_url[:limit]
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
lst_failure = check_urls(lst_url, timeout=timeout)
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
# ICI, pendant que le serveur tourne : le démarrage d'Odoo est ce
# qui coûte des minutes, pas les requêtes. Un deuxième outil avec
# son propre serveur doublerait l'attente pour rien.
internal_report = internal_phase(
base_url,
database,
enabled=internal,
login=internal_login,
password=internal_password,
limit=internal_limit,
every_menu=every_menu,
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
lst_portal=portal,
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
)
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
finally:
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
stop_server(server)
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
attach_internal_log(internal_report, log_path)
2026-08-17 07:59:28 -04:00
lst_key = []
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
if lst_failure:
2026-08-17 07:59:28 -04:00
lst_log = read_log(log_path)
lst_failure = attach_missing_parents(lst_failure, lst_log)
lst_key = culprit_keys(database, lst_failure)
# Les deux sources : le contexte d'héritage quand il existe, le nom
# du gabarit quand l'échec vient du rendu.
for key in template_keys(lst_log, database):
if key not in lst_key:
lst_key.append(key)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
if not lst_failure or not (interactive or auto_apply):
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
return lst_url, lst_failure, lst_key, None, internal_report
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
print(render(lst_url, lst_failure, lst_key))
if auto_apply:
lst_done = lst_key
if lst_done:
code, output = apply_reset(database, lst_done)
print(output.strip()[-2000:])
if code == 2:
lst_done = []
else:
lst_done = prompt(database, lst_failure, lst_key, ask=ask)
if not lst_done:
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
return lst_url, lst_failure, lst_key, None, internal_report
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
server = start_server(database, port, config_path, log_path=log_path)
try:
if not wait_ready(base_url, timeout=boot):
raise RuntimeError(
f"{t('The server never answered on')} {base_url}"
)
lst_again = check_urls(
[url for url, _s, _p in lst_failure], timeout=timeout
)
finally:
stop_server(server)
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
return lst_url, lst_failure, lst_key, lst_again, internal_report
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
def stop_server(server):
"""Arrêter tout le GROUPE : sinon odoo-bin survit à son script."""
try:
group = os.getpgid(server.pid)
except OSError:
group = None
if group is not None:
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
try:
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
os.killpg(group, signal.SIGTERM)
except OSError:
pass
try:
server.wait(timeout=30)
except subprocess.TimeoutExpired:
if group is not None:
try:
os.killpg(group, signal.SIGKILL)
except OSError:
pass
server.kill()
handle = getattr(server, "erplibre_log", None)
if handle:
handle.close()
def port_is_taken(port, host="127.0.0.1"):
"""Quelqu'un écoute-t-il déjà là ?
Sans cette question, un serveur resté d'un essai précédent répond à
« le serveur est-il prêt ? », et l'on teste sa base à lui en croyant
tester la sienne. C'est exactement ce qui est arrivé ici.
"""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(1)
return sock.connect_ex((host, port)) == 0
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
def main(argv=None):
parser = argparse.ArgumentParser(
description=(
"Start Odoo on a database, request every URL of its sitemap, and"
" report those that fail."
)
)
parser.add_argument("-d", "--database", required=True)
parser.add_argument("-c", "--config", default="./config.conf")
parser.add_argument("-p", "--port", type=int, default=DEFAULT_PORT)
parser.add_argument(
"--limit", type=int, default=None, help="test only the first N URLs"
)
parser.add_argument("--timeout", type=int, default=30)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
parser.add_argument(
"--apply",
action="store_true",
help="reset the views in cause without asking (WRITES)",
)
parser.add_argument(
"--report-only",
action="store_true",
help="never ask anything, even in front of a terminal",
)
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
parser.add_argument(
"--boot-timeout",
type=int,
default=180,
help="how long to wait for the server to answer",
)
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
parser.add_argument(
"--no-internal",
action="store_true",
help="skip the back-office pass done as the neutralization test user",
)
parser.add_argument("--login", default="test")
parser.add_argument("--password", default="test")
parser.add_argument(
"--record-limit",
type=int,
default=20,
help="how many records each app loads on its first page",
)
parser.add_argument(
"--all-menus",
action="store_true",
help="open every menu with an action, not just each app's first page",
)
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
parser.add_argument(
"--portal",
default="/my",
help="portal paths opened while signed in (comma separated, empty"
" to skip)",
)
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
config = parser.parse_args(argv)
print(
f"⧖ {t('Starting Odoo on')} '{config.database}'"
f" ({t('port')} {config.port})…"
)
[FIX] migration: never ask a question the terminal cannot show The theme uninstaller ends by asking what to do with the leftovers, and the migration ran it through the capturing executor. Its stdout was a pipe, Python buffers by blocks, so the prompt stayed invisible while the process waited. It reads as a freeze: you press Enter blind, the first keystroke answers unseen and the extra ones fall into the next question. Two locks, because one is not enough. The migration runs the script on the real terminal. And the three tools that ask now require BOTH ends: something to read the answer from, and something to show the question on. Guarding on stdin alone was the bug — measured, stdin tty=True with stdout tty=False, and the question was asked into a pipe. --- FR --- [FIX] migration : ne jamais poser une question que le terminal ne montre pas Le désinstalleur de thème finit par demander quoi faire des restes, et la migration le lançait par l'exécuteur qui capture. Sa sortie partait dans un tube, Python bufferise par blocs, et l'invite restait invisible pendant l'attente. Cela se lit comme un blocage : on tape Entrée à l'aveugle, la première frappe répond sans être vue et les suivantes tombent dans la question d'après. Deux verrous, car un seul ne suffit pas. La migration lance le script sur le vrai terminal. Et les trois outils qui questionnent exigent désormais les DEUX bouts : de quoi lire la réponse, et de quoi montrer la question. Ne garder que stdin était le défaut — mesuré, stdin tty=True et stdout tty=False, la question partait dans un tube. Assisted-by: Claude Opus 5
2026-08-17 02:41:40 -04:00
interactive = not config.report_only and can_ask()
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
try:
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
lst_url, lst_failure, lst_key, lst_again, internal = run(
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
config.database,
config.port,
config.config,
limit=config.limit,
timeout=config.timeout,
boot=config.boot_timeout,
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
interactive=interactive,
auto_apply=config.apply,
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
internal=not config.no_internal,
internal_login=config.login,
internal_password=config.password,
internal_limit=config.record_limit,
every_menu=config.all_menus,
[ADD] migration: open /my as the test user, and blame the right request The portal is a third rendering, neither the public site nor the back office: QWeb frontend with counters that each query their own model. The sitemap does not list /my — the page needs a session — and no RPC call goes through it. A migration can break it with nothing else noticing. Measured on a real database: /my answered 500 on a user field a module no longer defines, while every public page and sixteen apps were fine. Two guards the run proved necessary. Requested without a session, /my redirects to the login form and answers 200: counting that as a success would announce a healthy portal never seen. And the production error page shows no traceback, so the reason comes from the server log — attributed by PATH, because the portal is checked first and taking the last trace blamed it for an app's failure. A wrong cause costs more than none. --- FR --- [ADD] migration : ouvrir /my avec l'utilisateur test, et accuser la bonne requête Le portail est un troisième rendu, ni le site public ni le back-office : du QWeb frontend, avec ses compteurs qui interrogent chacun leur modèle. Le sitemap ne liste pas /my — la page demande une session — et aucun appel RPC n'y passe. Une migration peut le casser sans que rien ne le dise. Mesuré sur une vraie base : /my rendait 500 sur un champ utilisateur qu'un module ne définit plus, alors que les pages publiques et seize applications allaient bien. Deux garde-fous que l'essai a rendus nécessaires. Sans session, /my redirige vers le formulaire de connexion et rend 200 : le compter pour une réussite annoncerait un portail jamais vu. Et la page d'erreur de production ne montre aucune trace : la raison vient du journal, attribuée par CHEMIN — le portail est interrogé en premier, et prendre la dernière trace l'accusait de la panne d'une application. Assisted-by: Claude Opus 5
2026-08-17 22:48:40 -04:00
portal=[
path.strip()
for path in (config.portal or "").split(",")
if path.strip()
],
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
)
except RuntimeError as exc:
print(f"❌ {exc}")
return 2
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
internal_failed = render_internal(internal)
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
if lst_again is None:
print(render(lst_url, lst_failure, lst_key))
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
return 1 if (lst_failure or internal_failed) else 0
[ADD] migration: name the views behind a failing URL, and offer the reset The smoke test stopped at « these URLs answer 500 ». Turning that into a fix meant reading an id out of a traceback and translating it to a key by hand, mid-migration — the copy where a character goes missing. It now reads the error context Odoo logs — [view_id: N, parent_id: M], the one line it does not translate — resolves the parent to its key, offers the reset numbered with « all », and re-requests the failing URLs afterwards. Applying without re-asking would be calling it fixed without having seen it answer. Two defects found while measuring, both mine. ./run.sh is a bash wrapper: terminate() killed it and left odoo-bin holding the port, six orphans in six runs, each later run silently querying the first one's server. And the log was read before stopping, so only the 24 startup lines existed — hence « no view in cause » on pages that named one. --- FR --- [ADD] migration : nommer les vues derrière une URL en échec, et corriger Le test s'arrêtait à « ces URL répondent 500 ». En faire un correctif demandait de relever un id dans une trace et de le traduire en clé à la main, en pleine migration — la recopie où un caractère se perd. Il lit désormais le contexte qu'Odoo journalise — [view_id: N, parent_id: M], la seule ligne qu'il ne traduit pas —, résout le parent en clé, propose la réinitialisation numérotée avec « toutes », et redemande ensuite les URL en échec. Appliquer sans redemander, ce serait déclarer réparé sans l'avoir vu répondre. Deux défauts trouvés en mesurant, tous deux à moi. ./run.sh est une enveloppe bash : terminate() la tuait et laissait odoo-bin tenir le port, six orphelins en six essais, chaque essai suivant interrogeant sans le savoir le serveur du premier. Et le journal était lu avant l'arrêt, donc seules les 24 lignes de démarrage existaient — d'où « aucune vue en cause » sur des pages qui en nommaient une. Assisted-by: Claude Opus 5 (cherry picked from commit 30fbcd1aee20c4028ea6b24bd04eb518f0587ffe)
2026-08-16 06:18:45 -04:00
# Après correction on ne redit pas le diagnostic : on dit ce qu'il RESTE.
print(
f"\n↻ {t('Re-checked the')} {len(lst_failure)}"
f" {t('failing URL(s) after the reset')} :"
)
print(render([url for url, _s, _p in lst_failure], lst_again, None))
[ADD] migration: open every app as the neutralization test user The public smoke test opens what a visitor reaches. It says nothing about the back office, which is where a migration does most of its damage: a field dropped from a model but still named in a form, a module installed in the database whose code no longer ships with the target version. None of it stops the module loading — it stops the day someone opens the app. Neutralizing installs a `test` / `test` login carrying the SYSTEM user's groups, and it survives the module's uninstall. So the tool checks for that user rather than trusting a flag, and it rides the server the public pass already started: booting Odoo is what costs minutes, not requests. Measured on a real 18.0 database of 25 apps, which found four defects of mine: web_search_read changed signature in 17, a bare 404 hides an unregistered model, embedded sub-views are not the parent's fields, and an empty psql result meant two different things. --- FR --- [ADD] migration : ouvrir chaque application avec l'utilisateur test Le test de fumée public ouvre ce qu'un visiteur atteint. Il ne dit rien du back-office, où une migration fait pourtant l'essentiel de ses dégâts : un champ retiré du modèle mais toujours nommé dans un formulaire, un module installé en base dont le code n'accompagne plus la version cible. Rien de cela n'arrête le chargement ; cela arrête le jour où l'on ouvre l'appli. La neutralisation pose un compte `test` / `test` portant les groupes du superutilisateur, et il survit à la désinstallation du module. L'outil vérifie donc cet utilisateur plutôt qu'un drapeau, et réutilise le serveur déjà démarré : c'est le démarrage qui coûte, pas les requêtes. Mesuré sur une vraie base 18.0 de 25 applications, ce qui a révélé quatre défauts de mon fait. Assisted-by: Claude Opus 5
2026-08-17 22:16:40 -04:00
return 1 if (lst_again or internal_failed) else 0
[ADD] migration: request every public URL before calling it done A migration can load every module, log nothing, and still serve 500s on pages nobody thought to open. Measured on the real one: 2 of 33 public URLs failed — a blog post and /contactus — after a bump the log called successful. The list is the sitemap, what Odoo publishes for search engines. It cannot be read from odoo-bin shell: enumerate_pages() asks for http.root.get_db_router(request.db) and raises « object unbound » without a real request. So the server is started on its own port, asked, and always stopped — a forgotten one holds the port and fails the next bump. Offered before the Selenium prompt, default no: it boots a server and can take minutes. --- FR --- [ADD] migration : interroger chaque URL publique avant de conclure Une migration peut charger tous ses modules, ne rien écrire au journal, et servir quand même des 500 sur des pages que personne n'ouvre. Mesuré : 2 URL publiques sur 33 échouaient — un billet de blogue et /contactus — après un palier que le journal disait réussi. La liste est le sitemap, celle qu'Odoo publie pour les moteurs. Elle ne se lit pas depuis odoo-bin shell : enumerate_pages() réclame http.root.get_db_router(request.db) et lève « object unbound » sans requête réelle. Le serveur est donc démarré sur son propre port, interrogé, et toujours arrêté — un serveur oublié tient le port et fait échouer le palier suivant. Proposé avant l'invite Selenium, par défaut non : cela démarre un serveur et peut durer. Assisted-by: Claude Opus 5 (cherry picked from commit 277e85b3d06173beeb92be792467751b31ac0696)
2026-08-16 05:50:00 -04:00
if __name__ == "__main__":
sys.exit(main())