From b6e20297709a6d8193b73d92f3557f534c39c27c Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Sun, 27 Sep 2026 11:28:29 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20E2E=20node-direct,=20timeouts=20r=C3=A9a?= =?UTF-8?q?listes=2025/30=20min=20(compl=C3=A9ment=20BUG-080)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 22 ++++++++++++++------- README.fr.md | 6 +++--- README.md | 6 +++--- VERSION | 2 +- desktop/Cargo.lock | 2 +- desktop/Cargo.toml | 2 +- desktop/tauri.conf.json | 2 +- docs/ROADMAP.md | 2 +- package.json | 2 +- playwright.config.ts | 9 +++++---- scripts/run-e2e-local.ps1 | 40 +++++++++++++++++++++++++++------------ tests/test_e2e_harness.py | 38 ++++++++++++++++++++++++++++--------- 12 files changed, 89 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 424b3c4..66204c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.13**. +> [Unreleased](#unreleased). La dernière version livrée est **2.28.14**. --- @@ -14,6 +14,10 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.28.14] — 2026-09-27 + +--- + ## [2.28.13] — 2026-09-27 ### Modifié @@ -78,14 +82,18 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### 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), + `run-e2e-local.ps1` : Playwright lancé via `node` direct sur la CLI locale + (jamais de prompt interactif, `Start-Process` ne sachant pas exécuter `npx` ; + paramètre `$Arguments`, `$Args` étant une variable automatique qui l'écraserait), 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`. + défaut 1800 s / 600 s, exit 124 au dépassement — au-delà du globalTimeout + pour un abandon propre avec rapport) ; `run-e2e-local.sh` : `npx --yes` + + mêmes bornes ; `playwright.config.ts` : `globalTimeout` (25 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-fous : `tests/test_e2e_harness.py` (8 tests). --- diff --git a/README.fr.md b/README.fr.md index ce46912..e38016f 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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.13-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.28.14-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.13). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.28.14). --- -*Projet : ObsiGate | Version : 2.28.13 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.28.14 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 575d524..558040d 100644 --- a/README.md +++ b/README.md @@ -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.13-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.28.14-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.13). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.28.14). --- -*Project: ObsiGate | Version: 2.28.13 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.28.14 | Last updated: September 2026* diff --git a/VERSION b/VERSION index c9f834e..3ebae6e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.28.13 +2.28.14 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index b45ff2a..3bc5f1b 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.28.13" +version = "2.28.14" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 06172bd..ed3e2c4 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.28.13" +version = "2.28.14" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index bf221af..e8a8272 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.28.13", + "version": "2.28.14", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3910eeb..d65da57 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.28.13 | **Dernière mise à jour :** 2026-09-27 +> **Version :** 2.28.14 | **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)** diff --git a/package.json b/package.json index 856615a..39c77f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.28.13", + "version": "2.28.14", "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": { diff --git a/playwright.config.ts b/playwright.config.ts index 13b25e4..253cca9 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,12 +9,13 @@ 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. + // BUG-080 : la suite (~120 tests, workers: 1, ~10-15 s/test sur un poste + // chargé) 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), + (process.env.CI ? 30 * 60 * 1000 : 25 * 60 * 1000), ), reportSlowTests: process.env.CI ? null : { max: 5, threshold: 30000 }, diff --git a/scripts/run-e2e-local.ps1 b/scripts/run-e2e-local.ps1 index 5128f5a..0cc69a4 100644 --- a/scripts/run-e2e-local.ps1 +++ b/scripts/run-e2e-local.ps1 @@ -10,7 +10,7 @@ conditions que le job CI `e2e`), lance la suite Playwright puis nettoie. .PARAMETER PlaywrightArgs - Arguments transmis à `npx playwright test`, ex. `-g "image viewer"`, + Arguments transmis à `playwright test` (via `node`), ex. `-g "image viewer"`, `--headed`. .EXAMPLE @@ -35,15 +35,20 @@ $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 } +# E2E_TIMEOUT_SEC dépasse volontairement le globalTimeout Playwright (25 min en +# local) pour que ce soit Playwright qui abandonne proprement (avec rapport) en premier. +$TestTimeoutSec = if ($env:E2E_TIMEOUT_SEC) { [int]$env:E2E_TIMEOUT_SEC } else { 1800 } $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) { +function Invoke-NativeWithTimeout([string]$Label, [int]$TimeoutSec, [string]$Exe, [string[]]$Arguments) { # 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`). + # NOTE : le paramètre NE DOIT PAS s'appeler `$Args` (variable automatique + # PowerShell qui l'écraserait → `node` lancé sans arguments, exit 0 + # silencieux immédiat en lisant un stdin vide). $stamp = Get-Date -Format "HH:mm:ss" Write-Host "[$stamp] $Label (timeout ${TimeoutSec}s)..." - $proc = Start-Process -FilePath $Exe -ArgumentList $Args -NoNewWindow -PassThru + $proc = Start-Process -FilePath $Exe -ArgumentList $Arguments -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))." @@ -70,7 +75,7 @@ function Assert-Command([string]$Name, [string]$Hint) { } Assert-Command "uv" "Installez-le : https://docs.astral.sh/uv/" -Assert-Command "npx" "Installez Node.js (>= 20)." +Assert-Command "node" "Installez Node.js (>= 20)." # ----- Venv Python 3.11 (créé une seule fois) ----- $Python = ".venv-e2e/Scripts/python.exe" @@ -130,21 +135,32 @@ try { } Write-Host "[OK] Serveur prêt." - # ----- Browsers Playwright (no-op s'ils sont déjà installés) ----- - # 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é. + # ----- Playwright via node direct (pas npx) ----- + # BUG-080 : `Start-Process` ne peut pas lancer `npx` (ni le `.ps1` ni le + # `.cmd` ne sont des applications Win32 directes) → on appelle la CLI + # locale via `node.exe`, sans prompt interactif possible. Skip de + # l'install si un chromium est déjà présent (sauf E2E_INSTALL_BROWSERS=1), + # timeouts dédiés sur chaque étape. + $PlaywrightCli = Join-Path $Root "node_modules/@playwright/test/cli.js" + if (-not (Test-Path -LiteralPath $PlaywrightCli)) { + throw "[ERR] $PlaywrightCli introuvable. Lancez d'abord : npm ci" + } if (($env:E2E_INSTALL_BROWSERS -eq "1") -or (-not (Test-ChromiumInstalled))) { - $code = Invoke-NativeWithTimeout "npx playwright install chromium" $BrowserTimeoutSec "npx" @("--yes", "playwright", "install", "chromium") + $code = Invoke-NativeWithTimeout "playwright install chromium" $BrowserTimeoutSec "node" @($PlaywrightCli, "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 ' ')" + Write-Host "[INFO] BASE_URL=$BaseUrl node $PlaywrightCli test --project=chromium-desktop $($PlaywrightArgs -join ' ')" $env:BASE_URL = $BaseUrl - $testArgs = @("--yes", "playwright", "test", "--project=chromium-desktop") + @($PlaywrightArgs) - $exitCode = Invoke-NativeWithTimeout "playwright test" $TestTimeoutSec "npx" $testArgs + $testArgs = @($PlaywrightCli, "test", "--project=chromium-desktop") + if ($PlaywrightArgs) { $testArgs += @($PlaywrightArgs) } + $exitCode = Invoke-NativeWithTimeout "playwright test" $TestTimeoutSec "node" $testArgs +} catch { + Write-Host "[ERR] $($_.Exception.Message)" + $exitCode = 1 } finally { Write-Host "[INFO] Arrêt du serveur (PID $($server.Id))..." if (-not $server.HasExited) { Stop-Process -Id $server.Id -Force -ErrorAction SilentlyContinue } diff --git a/tests/test_e2e_harness.py b/tests/test_e2e_harness.py index 9fe3ef7..a22edad 100644 --- a/tests/test_e2e_harness.py +++ b/tests/test_e2e_harness.py @@ -23,18 +23,34 @@ def _read(rel: str) -> str: class TestE2ELocalPs: SCRIPT = "scripts/run-e2e-local.ps1" - def test_npx_never_prompts(self): - """`npx --yes` partout : aucun prompt « Ok to proceed? » qui pend.""" + def test_playwright_via_node_no_npx(self): + """Playwright est lancé via `node` direct, jamais via `npx`. + + `Start-Process` ne peut pas exécuter `npx` (ni le `.ps1` ni le + `.cmd` ne sont des applications Win32 directes : "%1 is not a valid + Win32 application"), et `npx` sans `--yes` peut pendre sur un prompt + interactif. Seules les mentions en commentaires/logs sont tolérées. + """ 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" + assert not bare, f"invocations npx nues : {bare}" + assert "node_modules/@playwright/test/cli.js" in content, ( + "CLI Playwright locale attendue (via node)" + ) + # `$Args` est une variable automatique PowerShell : un paramètre de + # ce nom serait écrasé (helper lancé sans arguments → exit 0 muet). + # (commentaires `#` exclus : la mise en garde elle-même le cite). + code_lines = [ + line for line in content.splitlines() + if not line.strip().startswith("#") + ] + assert not re.search(r"\$Args\b", "\n".join(code_lines)), ( + "BUG-080 : paramètre `$Args` interdit (shadowing par $args automatique)" + ) def test_browser_install_skippable(self): """Install navigateurs sautée si chromium déjà présent (sauf forçage).""" @@ -43,12 +59,16 @@ class TestE2ELocalPs: 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).""" + """L'étape `playwright test` est bornée (E2E_TIMEOUT_SEC, défaut 1800). + + Le défaut dépasse le globalTimeout Playwright (25 min en local) pour + que ce soit Playwright qui abandonne proprement (avec rapport) en premier. + """ 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" + assert re.search(r"E2E_TIMEOUT_SEC.*else\s*\{\s*1800\s*\}", content), ( + "défaut E2E_TIMEOUT_SEC=1800 attendu" )