fix: harnais E2E local anti-blocage BUG-080
CI / lint (push) Failing after 1m51s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Failing after 1m33s

This commit is contained in:
2026-09-27 09:08:58 -04:00
parent d70ecd0968
commit 7d70e0fb75
15 changed files with 209 additions and 18 deletions
+17 -1
View File
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
> **En cours de développement** : les changements à venir sont listés dans la section
> [Unreleased](#unreleased). La dernière version livrée est **2.28.7**.
> [Unreleased](#unreleased). La dernière version livrée est **2.28.8**.
---
@@ -14,6 +14,22 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.28.8] — 2026-09-27
### Corrigé
- **BUG-080 — harnais E2E local anti-blocage (plus de run pendu toute la nuit).**
`run-e2e-local.ps1/.sh` : `npx --yes` (jamais de prompt interactif),
installation Chromium sautée si déjà présent (`E2E_INSTALL_BROWSERS=1`
pour forcer), étapes `install`/`test` bornées (`E2E_TIMEOUT_SEC`,
défaut 900 s / 600 s, exit 124 au dépassement) ; `playwright.config.ts` :
`globalTimeout` (15 min en local, 30 min en CI, `E2E_GLOBAL_TIMEOUT_MS`
pour surcharger) ; `e2e-server.ps1` : pidfile resynchronisé sur le vrai
propriétaire du port et `stop` qui tue l'arbre complet (fini les serveurs
orphelins qui squattent le port 2029). Garde-fou : `tests/test_e2e_harness.py`.
---
## [2.28.7] — 2026-09-26
### Ajouté
+3 -3
View File
@@ -4,7 +4,7 @@
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
[![Version](https://img.shields.io/badge/Version-2.28.7-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.28.8-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -976,8 +976,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.28.7).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.28.8).
---
*Projet : ObsiGate | Version : 2.28.7 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.28.8 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -2,7 +2,7 @@
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
[![Version](https://img.shields.io/badge/Version-2.28.7-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.28.8-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -1151,8 +1151,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.28.7).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.28.8).
---
*Project: ObsiGate | Version: 2.28.7 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.28.8 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.28.7
2.28.8
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.28.7"
version = "2.28.8"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.28.7"
version = "2.28.8"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.28.7",
"version": "2.28.8",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+3
View File
@@ -188,6 +188,8 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| *BUG-076* | [🟡 IMPORTANT] Assistant IA : après une action de l'agent, l'arborescence et le document ouvert ne sont pas rafraîchis dynamiquement | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Mode agent : créer/supprimer un fichier ou dossier, modifier le document ouvert → l'UI ne bouge pas | `frontend/js/bookslm.js` : `MUTATING_TOOLS`/`FILE_WRITE_TOOLS`, refresh d'arborescence débouncé sur event `tool`, `_notifyFileWritten` étendu (xlsx/docx/csv/pdf). Tests : `tests/frontend/ai.test.mjs`, `tests/frontend/editor-inline.test.mjs` | Aucun refresh explicite sur les événements `tool` mutateurs (repose uniquement sur le watcher SSE) ; `_notifyFileWritten` ignore les créations de documents (xlsx/docx/csv/pdf) |
| *BUG-077* | [🟡 IMPORTANT] Assistant IA : aucun bouton « Stop » pour arrêter l'exécution de l'agent à tout moment | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/bookslm.js` | Mode agent : lancer une longue tâche → le bouton Envoyer est désactivé, impossible d'arrêter (seule la fermeture du panneau abort) | `frontend/js/bookslm.js` + `frontend/style.css` : bouton d'envoi → Stop (`_syncSendButton`/`_stopGeneration`/`_markStopped`), i18n `ai.stop`/`ai.stopped`. Tests : `tests/frontend/ai.test.mjs` (+2) | `_abortCtrl` n'est déclenché que par `close()` ; aucun signal d'arrêt côté client pendant le stream |
| *BUG-078* | [🟡 IMPORTANT] Fichiers de code : la coloration syntaxique (highlight.js) disparaît — les feuilles de thème sont basculées à partir de la **clé** de thème au lieu du **mode** | 🟢 corrigé | P1 | 📱 frontend | IA | `frontend/js/themes.js`, `frontend/js/ui.js`, `tests/frontend/unit.test.mjs` | Ouvrir un fichier `.py`/`.sh`/`.ps1`/`.yml` : le code s'affiche en texte brut, sans couleurs | `frontend/js/themes.js` : `applyTheme` bascule `hljs-theme-dark`/`hljs-theme-light` selon le **mode** (`isDark`). `frontend/js/ui.js` : `initTheme`/`applyTheme` résolvent le mode persisté (`obsigate-theme-mode`) au lieu de traiter la clé (`defaut-obsigate`) comme un mode. Test : `unit.test.mjs` (+1). | Les deux feuilles étaient désactivées car `defaut-obsigate !== "dark"` et `!== "light"` ; résultat **non déterministe** selon l'ordre `UI.initTheme()` (clé) / `Sync.init()` → `themes.initThemes()` (mode). Vérifié Playwright : 5/5 chargements colorés (`.py`), sépia/contraste élevé sur la palette claire |
| *BUG-081* | `GET /api/auth/mfa/status` → 500 quand l'auth est désactivée (`user` None, `AttributeError` sur `user.get`) | 🔴 ouvert | P1 | ⚙️ backend | IA | `backend/auth/router.py::mfa_status` (l.821-831) | Auth désactivée : `curl http://127.0.0.1:2029/api/auth/mfa/status` → 500 (reproduit live 2026-09-27, `e2e-server.err.log` l.116-183) | — | `require_auth` laisse passer le pseudo-user anonymous, `get_user(username)` → None non gardé. Trouvé via les logs E2E pendant BUG-080 |
| *BUG-080* | [🔴 BLOQUANT] E2E locaux bloqués toute la nuit : `npm run test:e2e:ps` ne termine jamais (serveurs orphelins sur le port 2029, `npx playwright install` sans `--yes` ni garde-fou, suite ~130 tests sans timeout global) | 🟢 corrigé | P0 | 🧩 tests | IA | `scripts/run-e2e-local.ps1`, `scripts/run-e2e-local.sh`, `scripts/e2e-server.ps1`, `playwright.config.ts`, `tests/test_e2e_harness.py` (nouveau) | `npm run test:e2e:ps` | `run-e2e-local` : `npx --yes`, skip install Chromium si présent (`E2E_INSTALL_BROWSERS=1`), timeouts `E2E_TIMEOUT_SEC` (900)/`E2E_BROWSER_INSTALL_TIMEOUT_SEC` (600, exit 124) ; `playwright.config.ts` : `globalTimeout` 15 min local / 30 min CI (`E2E_GLOBAL_TIMEOUT_MS`) ; `e2e-server.ps1` : pidfile = vrai owner du port, `stop` tue l'arbre complet. Tests : `tests/test_e2e_harness.py` (8/8), cycle start/stop live (pidfile cohérent, port libéré) | Constat 2026-09-27 : `e2e-server.ps1 start` OK (READY 12 s) mais run suivant pendu toute la nuit ; 2 python orphelins (PID 81180 parent + 81936 sur le port, pidfile périmé). Double processus systématique (parent `.venv` parqué + enfant qui sert — aussi sur flowdeck/3.13 : environnemental, sans impact après correctif). Trouvé au passage : BUG-081 (`/api/auth/mfa/status` → 500 auth désactivée) |
| *BUG-079* | `GET /api/diagnostics` → 500 « dictionary changed size during iteration » (stats d'index) | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/main.py` | Charger la page de diagnostic pendant une indexation : `GET /api/diagnostics` → 500 | `backend/main.py` (`api_diagnostics`) : snapshot avant itération — `list(index.items())` et `inv.word_index.copy()` (copie C atomique sous le GIL) ; test de non-régression `tests/test_api_main.py::TestConfig::test_diagnostics_concurrent_index_writes` | Le handler itérait les dicts en direct alors que l'indexeur les modifiait depuis un autre thread (rebuild initial dans `_search_executor`, hooks incrémentaux `add_document`/`remove_document`) → `RuntimeError` dans le générateur → 500. Test déterministe (`RaceDict` fait grossir le dict en cours d'itération) : échoue sans le correctif, passe avec. Vérifié : pytest 1305 passed / 6 skipped, ruff 0, mypy 0 |
### TODOs techniques (améliorations / nouvelles tâches)
@@ -273,6 +275,7 @@ Avant de corriger quoi que ce soit, un agent IA doit :
| 2026-09-24 | #115, #117, BUG-078 | Feature + correction | `frontend/js/themes.js`, `frontend/js/ui.js`, `frontend/js/viewer.js`, `frontend/js/config.js`, `frontend/index.html`, `frontend/style.css`, `frontend/popout.html`, `frontend/locales/{fr,en}.json`, `frontend/icons/avatar/*` (nouveau), `tests/frontend/unit.test.mjs`, `tests/frontend/toolbar-order.test.mjs`, `tests/frontend/settings-order-avatar.test.mjs`, `docs/features/viewer-toolbar-highlight-avatars.md` (nouvelle), `docs/ROADMAP.md`, `CHANGELOG.md` | **#115** barre d'outils de lecture épinglée : `viewer.js`/`popout.html` sortent `.file-actions` de `.file-header` dans un `.file-toolbar` enfant direct de `.content-area` (`position: sticky; top: 0`), masqué en mode lecture. **BUG-078** coloration syntaxique : le basculement des feuilles highlight.js suit le **mode** (`themes.applyTheme` + `ui.initTheme/applyTheme` lisent `obsigate-theme-mode`) au lieu de la clé de thème qui désactivait les deux feuilles. **#117** avatars prédéfinis : galerie de 12 images (`frontend/icons/avatar/`) dans `#cfg-profile`, clic → recadrage 256 px (pipeline import) + `PATCH /api/auth/me`, avatars actifs surlignés (`obsigate-avatar-preset`), import personnalisé et suppression conservés. Vérifié : Playwright (coloration 5/5 déterministe, toolbar épinglée à `barTop` constant au défilement), `unit.test.mjs` 12/12, `toolbar-order` 13/13, `settings-order-avatar` 12/12, JSDOM editor-inline/pane-manager/mobile-editor/image-viewer/pdf-viewer/config-mobile/media-viewer/excalidraw verts, pytest 1304 passed / 6 skipped, ruff/mypy 0, validate-imports 40 modules. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-24 | BUG-079 | Correction | `backend/main.py`, `tests/test_api_main.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-079** : `GET /api/diagnostics` renvoyait 500 « dictionary changed size during iteration ». Le handler itérait `inv.word_index.values()` et `index.items()` en direct alors que l'indexeur les modifiait depuis un autre thread (rebuild initial dans `_search_executor`, hooks incrémentaux `add_document`/`remove_document`) → `RuntimeError` dans le générateur. Correctif : **snapshot avant itération** (`list(index.items())`, `inv.word_index.copy()`) — copie C atomique sous le GIL, pas de verrou ajouté. Test de non-régression déterministe (`RaceDict` fait grossir le dict pendant l'itération ; échoue sans le correctif, passe avec). Vérifié : pytest 1305 passed / 6 skipped, ruff 0, mypy 0 (80 fichiers), validate-imports 40 modules, unit 12/12. | 🟢 corrigé (en attente vérif utilisateur) |
| 2026-09-27 | BUG-080, BUG-081 | Correction + enregistrement | `scripts/run-e2e-local.ps1`, `scripts/run-e2e-local.sh`, `scripts/e2e-server.ps1`, `playwright.config.ts`, `tests/test_e2e_harness.py` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-080** : run E2E local pendu toute la nuit → harnais anti-blocage : `npx --yes` (plus de prompt interactif), install Chromium sautée si présent (`E2E_INSTALL_BROWSERS=1`), timeouts `E2E_TIMEOUT_SEC` (900)/`E2E_BROWSER_INSTALL_TIMEOUT_SEC` (600, exit 124), `globalTimeout` Playwright (15 min local / 30 min CI, `E2E_GLOBAL_TIMEOUT_MS`), pidfile resynchronisé sur le vrai owner du port + `stop` qui tue l'arbre complet (orphelins 81180/81936 nettoyés, port 2029 libéré). Diagnostic : double processus systématique (parent `.venv` parqué + enfant qui sert — environnemental, aussi sur flowdeck/3.13). **BUG-081** (ouvert, non traité) : `GET /api/auth/mfa/status` → 500 auth désactivée (`user` None, `router.py:827`, reproduit live). Vérifié : `test_e2e_harness.py` 8/8, cycle start/stop live (pidfile cohérent, port libéré). | 🟢 corrigé (en attente vérif utilisateur) ; BUG-081 🔴 ouvert |
---
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.28.7 | **Dernière mise à jour :** 2026-09-26
> **Version :** 2.28.8 | **Dernière mise à jour :** 2026-09-27
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.28.7",
"version": "2.28.8",
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
"main": "patch.js",
"directories": {
+8
View File
@@ -9,6 +9,14 @@ export default defineConfig({
reporter: process.env.CI ? 'github' : 'list',
timeout: 60000,
expect: { timeout: 10000 },
// BUG-080 : la suite (~130 tests, workers: 1) ne doit jamais pendre toute
// la nuit. Au-delà du timeout global, Playwright abandonne avec un échec
// explicite au lieu de bloquer. Surchargable : E2E_GLOBAL_TIMEOUT_MS.
globalTimeout: Number(
process.env.E2E_GLOBAL_TIMEOUT_MS ??
(process.env.CI ? 30 * 60 * 1000 : 15 * 60 * 1000),
),
reportSlowTests: process.env.CI ? null : { max: 5, threshold: 30000 },
use: {
baseURL: process.env.BASE_URL || 'http://localhost:2029',
+10
View File
@@ -51,6 +51,11 @@ function Stop-Server {
if (Test-Path -LiteralPath $PidFile) {
$srvPid = (Get-Content -LiteralPath $PidFile -TotalCount 1).Trim()
if ($srvPid -match '^\d+$') {
# BUG-080 : le PID enregistré peut avoir ré-exécuté uvicorn dans un
# processus enfant (constaté : parent .venv + enfant uv-python sur
# le port) — tuer l'arbre complet, pas seulement la racine.
Get-CimInstance Win32_Process -Filter "ParentProcessId=$srvPid" -ErrorAction SilentlyContinue |
ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $killed += $_.ProcessId }
Stop-Process -Id $srvPid -Force -ErrorAction SilentlyContinue
$killed += $srvPid
}
@@ -146,6 +151,11 @@ switch ($Command) {
}
$health = Invoke-WebRequest -Uri "$BaseUrl/api/health" -TimeoutSec 5 -UseBasicParsing |
Select-Object -ExpandProperty Content | ConvertFrom-Json
# BUG-080 : le PID `Start-Process` peut ne pas être celui qui écoute
# (ré-exécution enfant constatée) — persister le vrai propriétaire du
# port pour un `stop` fiable, sans serveurs orphelins.
$owner = Get-PortOwner
if ($owner) { $owner.Pid | Set-Content -LiteralPath $PidFile }
Write-Host "[OK] READY — ObsiGate v$($health.version) sur $BaseUrl ($($health.total_files) fichiers)."
}
}
+39 -3
View File
@@ -34,6 +34,35 @@ $BaseUrl = "http://127.0.0.1:$Port"
$ServerLog = "data/e2e-server.log"
$ServerErrLog = "data/e2e-server.err.log"
# BUG-080 : garde-fous anti-blocage (un run E2E ne doit jamais pendre toute la nuit).
$TestTimeoutSec = if ($env:E2E_TIMEOUT_SEC) { [int]$env:E2E_TIMEOUT_SEC } else { 900 }
$BrowserTimeoutSec = if ($env:E2E_BROWSER_INSTALL_TIMEOUT_SEC) { [int]$env:E2E_BROWSER_INSTALL_TIMEOUT_SEC } else { 600 }
function Invoke-NativeWithTimeout([string]$Label, [int]$TimeoutSec, [string]$Exe, [string[]]$Args) {
# Lance un processus natif en gardant la sortie console en direct, et le
# tue après $TimeoutSec s'il n'a pas terminé (exit 124, comme `timeout`).
$stamp = Get-Date -Format "HH:mm:ss"
Write-Host "[$stamp] $Label (timeout ${TimeoutSec}s)..."
$proc = Start-Process -FilePath $Exe -ArgumentList $Args -NoNewWindow -PassThru
$proc | Wait-Process -Timeout $TimeoutSec -ErrorAction SilentlyContinue
if (-not $proc.HasExited) {
Write-Host "[ERR] $Label : timeout après ${TimeoutSec}s, arrêt du processus (PID $($proc.Id))."
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
return 124
}
return $proc.ExitCode
}
function Test-ChromiumInstalled {
$base = Join-Path $env:USERPROFILE "AppData\Local\ms-playwright"
if (-not (Test-Path -LiteralPath $base)) { return $false }
$hit = Get-ChildItem -LiteralPath $base -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name -like "chromium-*" } |
Where-Object { Test-Path -LiteralPath (Join-Path $_.FullName "chrome-win\chrome.exe") } |
Select-Object -First 1
return ($null -ne $hit)
}
function Assert-Command([string]$Name, [string]$Hint) {
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
throw "[ERR] $Name introuvable. $Hint"
@@ -102,13 +131,20 @@ try {
Write-Host "[OK] Serveur prêt."
# ----- Browsers Playwright (no-op s'ils sont déjà installés) -----
npx playwright install chromium
# BUG-080 : `--yes` (jamais de prompt interactif npx qui pend), skip si un
# chromium est déjà présent (sauf E2E_INSTALL_BROWSERS=1), timeout dédié.
if (($env:E2E_INSTALL_BROWSERS -eq "1") -or (-not (Test-ChromiumInstalled))) {
$code = Invoke-NativeWithTimeout "npx playwright install chromium" $BrowserTimeoutSec "npx" @("--yes", "playwright", "install", "chromium")
if ($code -ne 0) { exit $code }
} else {
Write-Host "[INFO] Chromium Playwright déjà installé, étape sautée (E2E_INSTALL_BROWSERS=1 pour forcer)."
}
# ----- Exécution de la suite (projet CI : chromium-desktop) -----
Write-Host "[INFO] BASE_URL=$BaseUrl npx playwright test --project=chromium-desktop $($PlaywrightArgs -join ' ')"
$env:BASE_URL = $BaseUrl
& npx playwright test --project=chromium-desktop @PlaywrightArgs
$exitCode = $LASTEXITCODE
$testArgs = @("--yes", "playwright", "test", "--project=chromium-desktop") + @($PlaywrightArgs)
$exitCode = Invoke-NativeWithTimeout "playwright test" $TestTimeoutSec "npx" $testArgs
} finally {
Write-Host "[INFO] Arrêt du serveur (PID $($server.Id))..."
if (-not $server.HasExited) { Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue }
+23 -2
View File
@@ -23,6 +23,21 @@ cd "$(dirname "$0")/.."
PORT="${E2E_PORT:-2029}"
BASE_URL="http://127.0.0.1:$PORT"
SERVER_LOG="data/e2e-server.log"
# BUG-080 : garde-fous anti-blocage (un run E2E ne doit jamais pendre toute la nuit).
E2E_TIMEOUT_SEC="${E2E_TIMEOUT_SEC:-900}"
E2E_BROWSER_INSTALL_TIMEOUT_SEC="${E2E_BROWSER_INSTALL_TIMEOUT_SEC:-600}"
# Exécute "$@" avec un timeout dur (exit 124 comme `timeout`), sans timeout si
# la commande `timeout` est absente (ex. macOS sans coreutils).
run_with_timeout() {
local limit="$1"; shift
if command -v timeout &>/dev/null; then
timeout "$limit" "$@"
else
echo "[WARN] commande 'timeout' absente : $1 sans limite de ${limit}s" >&2
"$@"
fi
}
# ----- Prérequis -----
if ! command -v uv &>/dev/null; then
@@ -102,8 +117,14 @@ curl -sf "$BASE_URL/api/health" >/dev/null || {
}
# ----- Browsers Playwright (no-op s'ils sont déjà installés) -----
npx playwright install chromium
# BUG-080 : `--yes` (jamais de prompt interactif npx qui pend), skip si un
# chromium est déjà présent (sauf E2E_INSTALL_BROWSERS=1), timeout dédié.
if [[ "${E2E_INSTALL_BROWSERS:-0}" == "1" ]] || ! ls -d ~/.cache/ms-playwright/chromium-*/chrome-linux/chrome &>/dev/null; then
run_with_timeout "$E2E_BROWSER_INSTALL_TIMEOUT_SEC" npx --yes playwright install chromium
else
echo "[INFO] Chromium Playwright déjà installé, étape sautée (E2E_INSTALL_BROWSERS=1 pour forcer)."
fi
# ----- Exécution de la suite (projet CI : chromium-desktop) -----
echo "[INFO] BASE_URL=$BASE_URL npx playwright test --project=chromium-desktop $*"
BASE_URL="$BASE_URL" npx playwright test --project=chromium-desktop "$@"
BASE_URL="$BASE_URL" run_with_timeout "$E2E_TIMEOUT_SEC" npx --yes playwright test --project=chromium-desktop "$@"
+97
View File
@@ -0,0 +1,97 @@
"""Garde-fous anti-blocage du harnais E2E local (BUG-080).
Contexte : un run `npm run test:e2e:ps` est resté pendu toute la nuit —
serveurs orphelins sur le port 2029, `npx` sans `--yes` (prompt interactif
qui attend indéfiniment), installation des navigateurs systématique et suite
Playwright (~130 tests, workers: 1) sans aucun timeout global.
Ces tests statiques vérifient que chaque couche du harnais possède son
garde-fou, afin qu'un run E2E échoue vite au lieu de bloquer indéfiniment.
"""
from __future__ import annotations
import re
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
def _read(rel: str) -> str:
return (REPO_ROOT / rel).read_text(encoding="utf-8")
class TestE2ELocalPs:
SCRIPT = "scripts/run-e2e-local.ps1"
def test_npx_never_prompts(self):
"""`npx --yes` partout : aucun prompt « Ok to proceed? » qui pend."""
content = _read(self.SCRIPT)
# Aucune invocation nue `npx ...` / `& npx ...` (toujours via le helper
# avec `--yes`) ; les mentions dans commentaires/Write-Host sont OK.
bare = [
line.strip()
for line in content.splitlines()
if re.match(r"^\s*(?:&\s*)?npx\s", line)
]
assert not bare, f"invocations npx nues (sans --yes) : {bare}"
assert content.count("--yes") >= 2, "au moins install + test en --yes"
def test_browser_install_skippable(self):
"""Install navigateurs sautée si chromium déjà présent (sauf forçage)."""
content = _read(self.SCRIPT)
assert "Test-ChromiumInstalled" in content
assert "E2E_INSTALL_BROWSERS" in content
def test_test_step_has_timeout(self):
"""L'étape `playwright test` est bornée (E2E_TIMEOUT_SEC, défaut 900)."""
content = _read(self.SCRIPT)
assert "E2E_TIMEOUT_SEC" in content
assert "Wait-Process -Timeout" in content
assert re.search(r"E2E_TIMEOUT_SEC.*else\s*\{\s*900\s*\}", content), (
"défaut E2E_TIMEOUT_SEC=900 attendu"
)
class TestE2ELocalSh:
SCRIPT = "scripts/run-e2e-local.sh"
def test_npx_never_prompts(self):
content = _read(self.SCRIPT)
for line in content.splitlines():
stripped = line.strip()
if stripped.startswith("#") or stripped.startswith("echo") or "npx" not in stripped:
continue
if "playwright" in stripped:
assert "--yes" in stripped, f"appel npx sans --yes : {stripped}"
def test_test_step_has_timeout(self):
content = _read(self.SCRIPT)
assert "E2E_TIMEOUT_SEC" in content
assert "run_with_timeout" in content
class TestE2EServerPs:
SCRIPT = "scripts/e2e-server.ps1"
def test_pidfile_refreshed_with_port_owner(self):
"""Le pidfile est resynchronisé sur le vrai PID d'écoute après READY."""
content = _read(self.SCRIPT)
assert "Get-PortOwner" in content
ready_pos = content.find("[OK] READY")
assert ready_pos != -1
assert "Set-Content -LiteralPath $PidFile" in content[ready_pos - 600:ready_pos]
def test_stop_kills_process_tree(self):
"""`stop` tue aussi les enfants du PID enregistré (pas d'orphelins)."""
content = _read(self.SCRIPT)
assert "ParentProcessId=$srvPid" in content
class TestPlaywrightConfig:
CONFIG = "playwright.config.ts"
def test_global_timeout_set(self):
"""Timeout global : la suite abandonne au lieu de pendre toute la nuit."""
content = _read(self.CONFIG)
assert "globalTimeout" in content
assert "E2E_GLOBAL_TIMEOUT_MS" in content