Files
ObsiGate/tests/test_ci_workflow.py
T
bruno 14b8032635
CI / security (push) Successful in 1m37s
CI / lint (push) Successful in 2m18s
CI / test (push) Successful in 4m8s
CI / build (push) Successful in 1m38s
CI / e2e (push) Successful in 14m28s
fix: CI lint — config-ai-keys.test.mjs rejoint l'étape JSDOM (complément BUG-082)
2026-09-27 09:44:18 -04:00

105 lines
4.1 KiB
Python

"""Garde-fous du workflow CI Gitea (BUG-082, BUG-083).
Sans dépendance (pas de PyYAML) : analyse ligne à ligne de
`.gitea/workflows/ci.yml`, suffisante pour les conventions de ce fichier.
"""
from __future__ import annotations
import re
from pathlib import Path
CI_YML = Path(__file__).resolve().parent.parent / ".gitea" / "workflows" / "ci.yml"
REPO_ROOT = Path(__file__).resolve().parent.parent
def _run_bodies() -> list[tuple[int, str]]:
"""Toutes les lignes shell de chaque bloc `run:` → [(n° ligne, code)]."""
lines = CI_YML.read_text(encoding="utf-8").splitlines()
bodies: list[tuple[int, str]] = []
i = 0
while i < len(lines):
m = re.match(r"^(\s*)run:(?:\s*\|\s*)?$", lines[i])
inline = re.match(r"^(\s*)run:\s+(\S.*)$", lines[i])
if m:
base = len(m.group(1))
i += 1
while i < len(lines):
cur = lines[i]
if not cur.strip():
i += 1
continue
if len(cur) - len(cur.lstrip()) <= base:
break
bodies.append((i + 1, cur.strip()))
i += 1
elif inline:
bodies.append((i + 1, inline.group(2).strip()))
i += 1
else:
i += 1
return bodies
class TestRunnerProofScripts:
def test_no_hash_inside_run_bodies(self):
"""BUG-083 : aucun `#` dans le code shell des `run:`.
Le runner Gitea Act tronque naïvement au premier `#` (même entre
guillemets) : `echo "... see #87)"` devenait une citation non
fermée → `unexpected EOF while looking for matching '"'` (job
`security` rouge). Les lignes-commentaires shell (`# ...`) restent
autorisées : leur troncature est sémantiquement neutre.
"""
offenders = [
f"L{n}: {code}"
for n, code in _run_bodies()
if not code.startswith("#") and "#" in code
]
assert not offenders, (
"BUG-083 : `#` interdit dans le code des `run:` "
f"(tronqué par le runner) :\n" + "\n".join(offenders)
)
class TestFrontendStepsHaveTheirDeps:
@staticmethod
def _root_step_files() -> list[str]:
"""Fichiers `node tests/frontend/<f>` de l'étape racine (sans jsdom)."""
text = CI_YML.read_text(encoding="utf-8")
root_part = text.split("Frontend JSDOM tests", 1)[0]
root_steps = root_part.split("Frontend unit tests", 1)[1]
return re.findall(r"node tests/frontend/(\S+\.mjs)", root_steps)
@staticmethod
def _has_static_jsdom_import(rel: str) -> bool:
path = REPO_ROOT / "tests" / "frontend" / rel
return any(
re.match(r"^\s*import\b.*\bfrom\s+['\"]jsdom['\"]", line)
or re.match(r"""\brequire\(\s*['"]jsdom['"]\s*\)""", line)
for line in path.read_text(encoding="utf-8").splitlines()
)
def test_root_step_files_need_no_jsdom(self):
"""BUG-082 : l'étape racine tourne sans `tests/frontend/node_modules`
(installé seulement par l'étape JSDOM) : aucun de ses fichiers ne
doit importer `jsdom` statiquement — sinon `ERR_MODULE_NOT_FOUND`
et `lint` rouge (cas `upload.test.mjs`, puis `config-ai-keys.test.mjs`).
"""
offenders = [f for f in self._root_step_files() if self._has_static_jsdom_import(f)]
assert not offenders, (
"BUG-082 : ces fichiers importent `jsdom` mais tournent dans "
"l'étape racine (sans node_modules) — les déplacer dans l'étape "
f"JSDOM :\n" + "\n".join(offenders)
)
def test_jsdom_dependent_tests_run_in_jsdom_step(self):
"""BUG-082 : les suites à import statique `jsdom` tournent bien dans
l'étape JSDOM (les deux branches)."""
text = CI_YML.read_text(encoding="utf-8")
jsdom_part = text.split("Frontend JSDOM tests", 1)[1]
for suite in ("node upload.test.mjs", "node config-ai-keys.test.mjs"):
assert jsdom_part.count(suite) >= 2, (
f"BUG-082 : `{suite}` attendu dans les deux branches de "
"l'étape JSDOM"
)