[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Transfert des dépôts ERPLibre dans l'application mobile.
|
|
|
|
|
|
|
|
|
|
L'application embarque le code des dépôts du manifeste pour le parcourir hors
|
|
|
|
|
ligne. Ils y entrent en PACKS, et c'est ce qui rend la chose possible : un APK
|
|
|
|
|
est un ZIP borné à 65535 entrées, quand les 139 dépôts pèsent plus de 116 000
|
|
|
|
|
fichiers. Un fichier par source donnait « Too many zip entries 123678
|
|
|
|
|
(MAX=65535) » — la compilation s'arrêtait là, et l'application ne portait rien.
|
|
|
|
|
|
|
|
|
|
Regroupés en tranches de 4 Mo, ces fichiers tiennent en 391 entrées. Mesuré sur
|
|
|
|
|
la VM : 3 002 entrées dans l'APK, 282 Mo, et 20 fichiers relus depuis les packs
|
|
|
|
|
identiques octet pour octet à leur source.
|
|
|
|
|
|
|
|
|
|
Ce que ces tests vérifient : qu'un transfert vide, tronqué ou incohérent est
|
|
|
|
|
DIT, et non pris pour bon. Les trois pannes correspondantes ont chacune leur
|
|
|
|
|
fixture.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
|
from script.mobile import check_bundle_transfer as cbt # noqa: E402
|
|
|
|
|
|
|
|
|
|
# Contenu des sources factices : le nom du fichier -> ses octets.
|
|
|
|
|
SOURCES = {
|
|
|
|
|
"odoo/release.py": b"version_info = (18, 0)\n",
|
|
|
|
|
"odoo/api.py": b"def method():\n return 1\n",
|
|
|
|
|
"addons/sale/i18n/fr.po": b'msgid "x"\nmsgstr "y"\n',
|
|
|
|
|
"README.md": b"# ERPLibre\n",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_bundle(
|
|
|
|
|
tmp: Path,
|
|
|
|
|
sources=None,
|
|
|
|
|
*,
|
|
|
|
|
with_workspace=True,
|
|
|
|
|
break_pack=False,
|
|
|
|
|
drop_pack=False,
|
|
|
|
|
drop_index=False,
|
|
|
|
|
no_manifest=False,
|
|
|
|
|
):
|
|
|
|
|
"""Fabrique un faux bundle, et la source qui va avec.
|
|
|
|
|
|
|
|
|
|
Les avaries sont paramétrées plutôt que codées en dur : chaque test nomme
|
|
|
|
|
celle qu'il éprouve, et la fixture reste unique."""
|
|
|
|
|
sources = SOURCES if sources is None else sources
|
|
|
|
|
mobile = tmp / "mobile" / "erplibre_home_mobile"
|
|
|
|
|
repos = mobile / "dist" / "repos"
|
|
|
|
|
slug = "github-com-ERPLibre-odoo"
|
|
|
|
|
repo_dir = repos / slug
|
|
|
|
|
repo_dir.mkdir(parents=True)
|
|
|
|
|
if not no_manifest:
|
|
|
|
|
(repos / "manifest.json").write_text(
|
|
|
|
|
json.dumps(
|
|
|
|
|
[
|
|
|
|
|
{
|
|
|
|
|
"url": "https://github.com/ERPLibre/odoo",
|
|
|
|
|
"name": "odoo",
|
|
|
|
|
"path": "odoo18.0/odoo",
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"revision": "18.0",
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
index = [{"path": "odoo", "type": "dir"}]
|
|
|
|
|
blob = b""
|
|
|
|
|
items = list(sources.items())
|
|
|
|
|
for pos, (rel, data) in enumerate(items):
|
|
|
|
|
# L'avarie ne touche que la DERNIÈRE entrée : gonfler toutes les
|
|
|
|
|
# tailles décalerait chaque lecture et ferait échouer la comparaison
|
|
|
|
|
# avant le contrôle de bornes — ce n'est pas la panne qu'on éprouve.
|
|
|
|
|
last = pos == len(items) - 1
|
|
|
|
|
index.append(
|
|
|
|
|
{
|
|
|
|
|
"path": rel,
|
|
|
|
|
"type": "file",
|
|
|
|
|
"chunk": 0,
|
|
|
|
|
"offset": len(blob),
|
|
|
|
|
"size": len(data) + (7 if (break_pack and last) else 0),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
blob += data
|
|
|
|
|
if not drop_index:
|
|
|
|
|
(repo_dir / "index.json").write_text(json.dumps(index))
|
|
|
|
|
if not drop_pack:
|
|
|
|
|
(repo_dir / "pack-000.bin").write_bytes(blob)
|
|
|
|
|
if with_workspace:
|
|
|
|
|
for rel, data in sources.items():
|
|
|
|
|
src = tmp / "odoo18.0/odoo" / rel
|
|
|
|
|
src.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
src.write_bytes(data)
|
|
|
|
|
return mobile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAGoodTransfer(unittest.TestCase):
|
|
|
|
|
def test_it_counts_repos_files_and_packs(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp)
|
|
|
|
|
rep = cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertEqual(1, rep["repos"])
|
|
|
|
|
self.assertEqual(len(SOURCES), rep["files"])
|
|
|
|
|
self.assertEqual(1, rep["packs"])
|
|
|
|
|
|
|
|
|
|
def test_it_reads_the_files_back_from_the_pack(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp)
|
|
|
|
|
rep = cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertEqual(len(SOURCES), rep["checked"])
|
|
|
|
|
|
|
|
|
|
def test_it_compares_them_to_the_source(self):
|
|
|
|
|
"""La seule vérification qui prouve un transfert FIDÈLE, et pas
|
|
|
|
|
seulement cohérent."""
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp)
|
|
|
|
|
rep = cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertEqual(len(SOURCES), rep["compared"])
|
|
|
|
|
|
|
|
|
|
def test_without_a_workspace_it_still_reads_the_packs(self):
|
|
|
|
|
"""Hors du checkout, la comparaison n'est pas possible ; la lecture,
|
|
|
|
|
elle, l'est toujours."""
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, with_workspace=False)
|
|
|
|
|
rep = cbt.check(mobile, None, min_files=1)
|
|
|
|
|
self.assertEqual(len(SOURCES), rep["checked"])
|
|
|
|
|
self.assertEqual(0, rep["compared"])
|
|
|
|
|
|
|
|
|
|
def test_read_from_pack_returns_the_exact_bytes(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp)
|
|
|
|
|
repo_dir = mobile / "dist/repos/github-com-ERPLibre-odoo"
|
|
|
|
|
index = json.loads((repo_dir / "index.json").read_text())
|
|
|
|
|
entry = next(
|
|
|
|
|
e for e in index if e["path"] == "addons/sale/i18n/fr.po"
|
|
|
|
|
)
|
|
|
|
|
got = cbt.read_from_pack(repo_dir, entry)
|
|
|
|
|
self.assertEqual(SOURCES["addons/sale/i18n/fr.po"], got)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheThreeFailures(unittest.TestCase):
|
|
|
|
|
"""Vide, tronqué, incohérent : trois pannes qu'un « build OK » ne dit pas."""
|
|
|
|
|
|
|
|
|
|
def test_no_manifest_names_the_build(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, no_manifest=True)
|
|
|
|
|
with self.assertRaises(FileNotFoundError) as ctx:
|
|
|
|
|
cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertIn("build", str(ctx.exception))
|
|
|
|
|
|
|
|
|
|
def test_a_repo_without_index_is_named(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, drop_index=True)
|
|
|
|
|
with self.assertRaises(FileNotFoundError) as ctx:
|
|
|
|
|
cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertIn("odoo", str(ctx.exception))
|
|
|
|
|
|
|
|
|
|
def test_a_missing_pack_is_named(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, drop_pack=True)
|
|
|
|
|
with self.assertRaises(FileNotFoundError) as ctx:
|
|
|
|
|
cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertIn("pack-000.bin", str(ctx.exception))
|
|
|
|
|
|
|
|
|
|
def test_an_index_that_promises_too_much_is_refused(self):
|
|
|
|
|
"""Index et pack d'une compilation différente : le message doit nommer
|
|
|
|
|
la tranche et les tailles, pas rendre un octet manquant en silence."""
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, break_pack=True)
|
|
|
|
|
with self.assertRaises(ValueError) as ctx:
|
|
|
|
|
cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertIn("pack-000.bin", str(ctx.exception))
|
|
|
|
|
|
|
|
|
|
def test_an_empty_transfer_is_refused(self):
|
|
|
|
|
"""C'est le cas qui a existé pendant un temps : le bundle compilait,
|
|
|
|
|
sans un seul dépôt dedans. « Réussi » ne voulait rien dire."""
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, sources={"a.py": b"x\n"})
|
|
|
|
|
with self.assertRaises(ValueError) as ctx:
|
|
|
|
|
cbt.check(mobile, tmp) # seuil par défaut
|
|
|
|
|
self.assertIn("maigre", str(ctx.exception))
|
|
|
|
|
|
|
|
|
|
def test_a_file_that_differs_from_the_source_is_named(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp)
|
|
|
|
|
(tmp / "odoo18.0/odoo/README.md").write_bytes(b"autre chose\n")
|
|
|
|
|
with self.assertRaises(ValueError) as ctx:
|
|
|
|
|
cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertIn("README.md", str(ctx.exception))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheThreshold(unittest.TestCase):
|
|
|
|
|
def test_the_default_threshold_rules_out_an_empty_bundle(self):
|
|
|
|
|
"""Le seul dépôt odoo en porte près de 40 000 : mille est un plancher
|
|
|
|
|
qu'un vrai transfert dépasse de deux ordres de grandeur."""
|
|
|
|
|
self.assertGreaterEqual(cbt.MIN_FILES, 1000)
|
|
|
|
|
|
|
|
|
|
def test_the_sample_is_deterministic(self):
|
|
|
|
|
"""Une graine fixe : deux exécutions lisent les MÊMES fichiers, donc un
|
|
|
|
|
échec est reproductible."""
|
|
|
|
|
self.assertIsInstance(cbt.SEED, int)
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp)
|
|
|
|
|
first = cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
second = cbt.check(mobile, tmp, min_files=1)
|
|
|
|
|
self.assertEqual(first, second)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheCommandLine(unittest.TestCase):
|
|
|
|
|
def test_it_says_the_counts_and_returns_zero(self):
|
|
|
|
|
import io
|
|
|
|
|
import contextlib
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
mobile = build_bundle(tmp, sources=SOURCES)
|
|
|
|
|
argv = [
|
|
|
|
|
"check_bundle_transfer.py",
|
|
|
|
|
str(mobile),
|
|
|
|
|
"--workspace",
|
|
|
|
|
str(tmp),
|
|
|
|
|
]
|
|
|
|
|
buf = io.StringIO()
|
|
|
|
|
with unittest.mock.patch.object(sys, "argv", argv), mock_min(1):
|
|
|
|
|
with contextlib.redirect_stdout(buf):
|
|
|
|
|
code = cbt.main()
|
|
|
|
|
self.assertEqual(0, code)
|
|
|
|
|
self.assertIn("dépôts", buf.getvalue())
|
|
|
|
|
|
|
|
|
|
def test_a_failure_is_one_line_not_a_traceback(self):
|
|
|
|
|
"""Le message part dans un journal d'installation : une trace Python y
|
|
|
|
|
serait illisible, et la cause noyée."""
|
|
|
|
|
import io
|
|
|
|
|
import contextlib
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as d:
|
|
|
|
|
tmp = Path(d)
|
|
|
|
|
argv = ["check_bundle_transfer.py", str(tmp / "nulle-part")]
|
|
|
|
|
buf = io.StringIO()
|
|
|
|
|
with unittest.mock.patch.object(sys, "argv", argv):
|
|
|
|
|
with contextlib.redirect_stdout(buf):
|
|
|
|
|
code = cbt.main()
|
|
|
|
|
self.assertEqual(1, code)
|
|
|
|
|
self.assertIn("⚠", buf.getvalue())
|
|
|
|
|
self.assertNotIn("Traceback", buf.getvalue())
|
|
|
|
|
|
|
|
|
|
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
|
|
|
MOBILE = REPO / "mobile" / "erplibre_home_mobile"
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
# Le plafond d'entrees d'un ZIP, et donc d'un APK.
|
|
|
|
|
ZIP_ENTRY_LIMIT = 65535
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTheRealBundle(unittest.TestCase):
|
|
|
|
|
"""Le VRAI transfert, quand le dépôt mobile est installé et compilé.
|
|
|
|
|
|
|
|
|
|
C'est ici que ces tests DÉCLARENT leur dépendance : sans
|
|
|
|
|
mobile/erplibre_home_mobile, ils se disent ignorés plutôt que de passer en
|
|
|
|
|
silence — un test vert sans son dépôt ne prouve rien. Le lanceur
|
|
|
|
|
(script/test/run_unit_test.sh) annonce la même dépendance avant de
|
|
|
|
|
commencer.
|
|
|
|
|
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
Ce qu'ils gardent : qu'une compilation réelle range les sources dans des
|
|
|
|
|
CONTENEURS — archives tar.gz ou tranches pack, les deux dispositions
|
|
|
|
|
conviennent. Un retour au fichier-par-source ferait revenir la limite du
|
|
|
|
|
ZIP — 123 678 entrées pour un plafond de 65 535, silencieusement, jusqu'à
|
|
|
|
|
l'APK. C'est le nombre d'entrées qui compte, pas la forme du conteneur.
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def setUpClass(cls):
|
|
|
|
|
if not MOBILE.is_dir():
|
|
|
|
|
# Pas de « relative_to » : il lève quand le chemin sort du
|
|
|
|
|
# dépôt, et une erreur n'est pas un « ignoré » — mesuré en
|
|
|
|
|
# simulant l'absence.
|
|
|
|
|
raise unittest.SkipTest(
|
|
|
|
|
"mobile/erplibre_home_mobile absent :"
|
|
|
|
|
" ./mobile/install_mobile_dev.sh"
|
|
|
|
|
)
|
|
|
|
|
cls.repos = MOBILE / "dist" / "repos"
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
manifeste = cls.repos / "manifest.json"
|
|
|
|
|
if not manifeste.is_file():
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
raise unittest.SkipTest(
|
|
|
|
|
"dépôt mobile présent mais pas compilé :"
|
|
|
|
|
" ./mobile/compile_and_run.sh (ou npm run build)"
|
|
|
|
|
)
|
[ADD] proxmox : déployer des VM sur un hôte Proxmox distant
Nouvelle entrée sous QEMU/KVM, avec l'équivalent de ses dix-sept commandes.
Toute la différence tient en une phrase : l'hyperviseur est ailleurs. On
choisit donc l'hôte — VM QEMU locale, adresse, ou ~/.ssh/config — et on le
vérifie : pveversion le prouve, id/sudo décident du privilège, et une clé
d'hôte inconnue s'enregistre par ssh-keyscan plutôt qu'en désactivant le
contrôle.
Quatre pièges trouvés sur un hôte réel. Une Proxmox installée sur Debian n'a
aucun pont : on en propose un INTERNE, car ajouter l'interface physique
déplace l'adresse de l'hôte et coupe la session — à distance, sans retour.
--- EN ---
A new entry under QEMU/KVM, with the counterpart of its seventeen commands.
The whole difference fits in one sentence: the hypervisor is elsewhere. So the
host is chosen — local QEMU VM, address, or ~/.ssh/config — and then checked:
pveversion proves it, id/sudo decide about privilege, and an unknown host key
is recorded with ssh-keyscan rather than by disabling the check.
Four traps found on a real host. A Proxmox installed on Debian has no bridge:
we offer an INTERNAL one, because adding the physical NIC moves the host
address and cuts the session — remotely, with no way back.
Assisted-by: Claude Opus 5
2026-08-23 05:54:38 -04:00
|
|
|
# Manifeste PRÉSENT mais VIDE : l'application a été compilée sans le
|
|
|
|
|
# transfert des dépôts. C'est un choix légitime, pas une régression —
|
|
|
|
|
# et le distinguer importe, car ces tests échouaient alors sur
|
|
|
|
|
# « aucun dépôt à vérifier », ce qui se lit comme une panne du
|
|
|
|
|
# transfert. Vu le 23 août 2026 sur un build de 07:55 : manifeste à
|
|
|
|
|
# zéro entrée, aucun pack.
|
|
|
|
|
try:
|
|
|
|
|
entrees = json.loads(manifeste.read_text())
|
|
|
|
|
except (OSError, ValueError) as exc:
|
|
|
|
|
raise unittest.SkipTest(f"manifeste illisible : {exc}")
|
|
|
|
|
if not entrees:
|
|
|
|
|
raise unittest.SkipTest(
|
|
|
|
|
"compilé SANS les dépôts (manifeste vide) :"
|
|
|
|
|
" relancer ./mobile/compile_and_run.sh pour les inclure"
|
|
|
|
|
)
|
2026-08-24 04:17:56 -04:00
|
|
|
# Manifeste PLEIN mais index MANQUANTS : un transfert interrompu, ou
|
|
|
|
|
# un build qui a écrit le manifeste avant les paquets. Vu le
|
|
|
|
|
# 24 août 2026 — « <slug> : index.json absent » remontait en ERREUR,
|
|
|
|
|
# ce qui se lit comme une régression du transfert alors que rien
|
|
|
|
|
# n'était encore transféré. Un état incomplet s'IGNORE ; seule une
|
|
|
|
|
# incohérence entre ce qui est là et le dépôt doit échouer.
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
# L'index se cherche par la MÊME résolution que le vérificateur : la
|
|
|
|
|
# chercher ici en dur, sous `<slug>/index.json`, faisait sauter ces
|
|
|
|
|
# tests sur toute compilation en archives — ils regardaient ailleurs au
|
|
|
|
|
# lieu de vérifier.
|
2026-08-24 04:17:56 -04:00
|
|
|
for entree in entrees:
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
if not isinstance(entree, dict):
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
cbt.index_path(cls.repos, entree)
|
|
|
|
|
except (OSError, KeyError) as exc:
|
2026-08-24 04:17:56 -04:00
|
|
|
raise unittest.SkipTest(
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
f"paquet incomplet ({exc}) :"
|
2026-08-24 04:17:56 -04:00
|
|
|
" relancer ./mobile/compile_and_run.sh"
|
|
|
|
|
)
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
|
|
|
|
|
def test_the_transfer_is_coherent(self):
|
|
|
|
|
rep = cbt.check(MOBILE, REPO)
|
|
|
|
|
self.assertGreater(rep["repos"], 1)
|
|
|
|
|
self.assertGreater(rep["files"], cbt.MIN_FILES)
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
# Peu importe la disposition : ce qui doit être vrai, c'est que les
|
|
|
|
|
# sources soient dans des conteneurs et non une entrée ZIP chacune.
|
|
|
|
|
self.assertGreater(rep["packs"] + rep["archives"], 0)
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
|
|
|
|
|
def test_a_sample_matches_the_source(self):
|
|
|
|
|
"""La seule vérification qui prouve un transfert FIDÈLE."""
|
|
|
|
|
rep = cbt.check(MOBILE, REPO)
|
|
|
|
|
self.assertGreater(rep["compared"], 0)
|
|
|
|
|
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
def test_the_apk_stays_under_the_zip_entry_limit(self):
|
|
|
|
|
"""Le garde-fou de la limite du ZIP, énoncé comme l'invariant qu'il est.
|
|
|
|
|
|
|
|
|
|
Ce test exigeait un champ « chunk » sur chaque fichier, c'est-à-dire la
|
|
|
|
|
disposition en packs. Il échouait donc sur une compilation en archives
|
|
|
|
|
alors que la limite y est tenue — il testait la FORME au lieu de ce qui
|
|
|
|
|
importe : combien d'entrées le ZIP de l'APK va porter. Un fichier par
|
|
|
|
|
source en réclamerait 124 350 pour un plafond de 65 535."""
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
man = json.loads((self.repos / "manifest.json").read_text())
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
entries_in_apk = 0
|
|
|
|
|
files = 0
|
|
|
|
|
for proj in man:
|
|
|
|
|
index = cbt.index_path(self.repos, proj)
|
|
|
|
|
listing = json.loads(index.read_text())
|
|
|
|
|
files += len([e for e in listing if e.get("type") == "file"])
|
|
|
|
|
entries_in_apk += 1 # l'index lui-même
|
|
|
|
|
if cbt.archive_path(self.repos, proj) is not None:
|
|
|
|
|
entries_in_apk += 1 # une archive
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
continue
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
entries_in_apk += len(
|
|
|
|
|
list((self.repos / proj["slug"]).glob("pack-*.bin"))
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
)
|
[FIX] mobile: the bundle check refused a real build
The checker knew only the pack layout. A real build ships one tar.gz per
repository, so it raised "<slug> : index.json absent" and, being guarded by
`|| exit 1`, stopped compile_and_run.sh before cap sync — no APK. Broken
since 2026-08-20 for anyone on the current mobile main: the parent half of
that work landed, the mobile half producing packs never did.
It now accepts both layouts, so whichever half lands next, it holds.
Two things got better on the way. The byte-for-byte comparison against the
source — the only check that proves fidelity rather than coherence — was
reporting zero comparisons, because no entry matched the shape it looked
for; it now compares twenty. And presence is no longer sampled: streaming
all 139 archives costs 6 s and accounts for every one of the 124 350
promised files, where a sample of twenty could not see a ghost it did not
draw.
The test guarding the ZIP limit demanded a `chunk` field on every file —
the pack layout, not the limit. It counts entries now: 278 against 65 535.
Its real-bundle class had been taught to skip on this very symptom rather
than fail; it runs again.
--- FR ---
Le vérificateur ne connaissait que la disposition en packs. Une compilation
réelle livre un tar.gz par dépôt : il levait « <slug> : index.json absent »
et, gardé par `|| exit 1`, arrêtait compile_and_run.sh avant cap sync — pas
d'APK. Cassé depuis le 2026-08-20 pour quiconque est sur le main mobile
actuel : la moitié parente de ce travail a atterri, la moitié mobile qui
produit les packs jamais.
Il accepte désormais les deux dispositions : quelle que soit la moitié qui
atterrit ensuite, il tient.
Deux choses se sont améliorées en chemin. La comparaison octet pour octet
contre la source — la seule qui prouve la fidélité et non la cohérence —
rapportait zéro comparaison, faute d'entrée à la forme attendue ; elle en
compare vingt. Et la présence n'est plus échantillonnée : traverser les 139
archives coûte 6 s et rend compte de chacun des 124 350 fichiers promis, là
où vingt tirages ne pouvaient pas voir un fantôme non tiré.
Le test qui gardait la limite du ZIP exigeait un champ `chunk` sur chaque
fichier — la disposition, pas la limite. Il compte les entrées désormais :
278 pour 65 535. Sa classe sur le vrai bundle avait appris à s'ignorer sur
ce symptôme même plutôt qu'à échouer ; elle tourne à nouveau.
Assisted-by: Claude Opus 5
2026-08-24 05:50:04 -04:00
|
|
|
self.assertGreater(files, cbt.MIN_FILES, "aucun dépôt à vérifier")
|
|
|
|
|
self.assertLess(
|
|
|
|
|
entries_in_apk,
|
|
|
|
|
ZIP_ENTRY_LIMIT,
|
|
|
|
|
f"{entries_in_apk} entrées dans l'APK pour {files} fichiers",
|
|
|
|
|
)
|
|
|
|
|
# Et pas seulement « sous la limite » : l'écart doit rester d'un ordre
|
|
|
|
|
# de grandeur, sans quoi le prochain dépôt ajouté la ferait sauter.
|
|
|
|
|
self.assertLess(entries_in_apk, files // 10)
|
[ADD] make: une cible pour les tests unitaires, dépendance mobile déclarée
Ces 382 tests ne tournaient que lancés à la main, donc jamais. « make test »
demande une base de données et plusieurs minutes ; ceux-ci lisent le code et
exécutent les fragments de shell générés, sudo, pgrep et pkill bouchonnés, en
une dizaine de secondes. Deux cibles : test_unit, et test_unit_file pour la
boucle d'écriture.
La dépendance à mobile/erplibre_home_mobile est DITE plutôt que supposée : le
lanceur l'annonce présente ou absente, et les tests du vrai transfert se
déclarent ignorés — avec la commande qui manque — au lieu de passer en silence.
Vérifié dans les trois états : dépôt absent, présent non compilé, compilé.
Au passage, compile_and_run.sh vérifie le transfert des dépôts, comme
l'installation d'une VM et par le même script.
--- EN ---
These 382 tests only ran when invoked by hand, so never. "make test" wants a
database and several minutes; these read the code and run the generated shell
fragments with sudo, pgrep and pkill stubbed, in about ten seconds. Two targets:
test_unit, and test_unit_file for the writing loop.
The dependency on mobile/erplibre_home_mobile is STATED rather than assumed: the
runner announces it present or absent, and the real-transfer tests declare
themselves skipped — naming the missing command — instead of passing quietly.
Checked in all three states: repo absent, present but unbuilt, built.
Along the way, compile_and_run.sh verifies the repo transfer, like a VM install
and through the same script.
Assisted-by: Claude Opus 5
2026-08-21 20:06:23 -04:00
|
|
|
|
|
|
|
|
def test_no_bundled_test_file_lingers_as_a_source(self):
|
|
|
|
|
"""Effet de bord mesuré, et il compte : empaquetés, les 1 599 fichiers
|
|
|
|
|
de test des dépôts Odoo ne sont plus ramassés par vitest — 1 423
|
|
|
|
|
fichiers de test ramenés à 75, 35 s ramenées à 3 s."""
|
|
|
|
|
stray = list(self.repos.glob("*/**/*.test.ts"))
|
|
|
|
|
self.assertEqual([], stray)
|
|
|
|
|
|
|
|
|
|
|
[FIX] script mobile: transférer les dépôts ERPLibre dans l'APK, et le vérifier
Le contournement a vécu : les dépôts n'étaient plus embarqués du tout, l'APK
était refusé pour ses 123 678 entrées quand un ZIP en tient 65 535. Ils entrent
désormais en packs — tranches de 4 Mo et un index par dépôt disant où trouver
chaque fichier — ce qui ramène le compte à 391 entrées sans rien perdre du
contenu. Le côté application est dans le dépôt mobile ; ce commit porte la
vérification et retire le contournement.
Mesuré sur une VM : 139 dépôts, 116 156 fichiers, APK de 282 Mo à 3 002 entrées,
et 20 fichiers relus depuis les packs identiques octet pour octet à leur source.
L'installation le vérifie et échoue sinon : une application qui ne porte pas le
code qu'elle doit montrer n'est pas celle demandée.
--- EN ---
The stopgap has served its time: the repositories were not embedded at all, and
the APK was refused for its 123,678 entries where a ZIP holds 65,535. They now
enter as packs — 4 MB slices and one index per repository saying where each file
lives — which brings the count to 391 entries without losing any content. The
app side lives in the mobile repository; this commit carries the verification
and drops the workaround.
Measured on a VM: 139 repositories, 116,156 files, a 282 MB APK with 3,002
entries, and 20 files read back from the packs identical byte for byte to their
source. The install verifies it and fails otherwise: an app that does not carry
the code it must show is not the one that was asked for.
Assisted-by: Claude Opus 5
2026-08-20 03:51:14 -04:00
|
|
|
import contextlib as _contextlib # noqa: E402
|
|
|
|
|
import unittest.mock # noqa: E402
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@_contextlib.contextmanager
|
|
|
|
|
def mock_min(value):
|
|
|
|
|
"""Abaisse le plancher le temps d'un test de ligne de commande."""
|
|
|
|
|
old = cbt.MIN_FILES
|
|
|
|
|
cbt.MIN_FILES = value
|
|
|
|
|
try:
|
|
|
|
|
yield
|
|
|
|
|
finally:
|
|
|
|
|
cbt.MIN_FILES = old
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
unittest.main()
|