Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6e2029770 | ||
|
|
6b878caff3 | ||
|
|
e9b7a317c1 | ||
|
|
14b8032635 | ||
|
|
7dfe26c83d | ||
|
|
24229316c7 |
@@ -44,13 +44,11 @@ jobs:
|
||||
node tests/frontend/config-mobile.test.mjs
|
||||
node tests/frontend/settings-order-avatar.test.mjs
|
||||
node tests/frontend/mobile-toolbar.test.mjs
|
||||
node tests/frontend/upload.test.mjs
|
||||
node tests/frontend/pretty.test.mjs
|
||||
node tests/frontend/media-viewer.test.mjs
|
||||
node tests/frontend/mfa-settings.test.mjs
|
||||
node tests/frontend/config-ai-keys.test.mjs
|
||||
|
||||
- name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition)
|
||||
- name: Frontend JSDOM tests (PaneManager + Excalidraw + Plugins + AI + SW + Collab + Mobile + Semantic + Desktop + Inline edition + Upload)
|
||||
run: |
|
||||
cd tests/frontend
|
||||
if [ -d node_modules ]; then
|
||||
@@ -68,6 +66,8 @@ jobs:
|
||||
node toolbar-order.test.mjs
|
||||
node editor-inline.test.mjs
|
||||
node ai-quick-actions.test.mjs
|
||||
node upload.test.mjs
|
||||
node config-ai-keys.test.mjs
|
||||
else
|
||||
echo "tests/frontend/node_modules missing - installing jsdom"
|
||||
npm install --no-audit --no-fund --silent
|
||||
@@ -85,6 +85,8 @@ jobs:
|
||||
node toolbar-order.test.mjs
|
||||
node editor-inline.test.mjs
|
||||
node ai-quick-actions.test.mjs
|
||||
node upload.test.mjs
|
||||
node config-ai-keys.test.mjs
|
||||
fi
|
||||
|
||||
# ── Tests ─────────────────────────────────────────────────────────
|
||||
@@ -141,7 +143,10 @@ jobs:
|
||||
# Reste non bloquant tant que les montées de version requises
|
||||
# (starlette via fastapi, weasyprint) ne sont pas qualifiées :
|
||||
# upgrade FastAPI = chantier de régression dédié, hors périmètre.
|
||||
run: pip-audit || echo "pip-audit found vulnerabilities (non-blocking, see #87)"
|
||||
# NOTE runner Gitea Act (BUG-083) : aucun `#` dans le `run:`
|
||||
# ci-dessous (tronqué au premier `#`, même entre guillemets, ce qui
|
||||
# cassait la citation de l'echo) — la réf #87 ne vit qu'ici.
|
||||
run: pip-audit || echo "pip-audit found vulnerabilities (non-blocking)"
|
||||
|
||||
# ── Docker build ──────────────────────────────────────────────────
|
||||
build:
|
||||
|
||||
+74
-7
@@ -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.8**.
|
||||
> [Unreleased](#unreleased). La dernière version livrée est **2.28.14**.
|
||||
|
||||
---
|
||||
|
||||
@@ -14,19 +14,86 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
|
||||
## [2.28.14] — 2026-09-27
|
||||
|
||||
---
|
||||
|
||||
## [2.28.13] — 2026-09-27
|
||||
|
||||
### Modifié
|
||||
|
||||
- **#87 (T5c) — `script-src` sans `'unsafe-inline'`.**
|
||||
Seuls les scripts avec nonce frais (`backend/csp.py`, T5b) ou servis par
|
||||
`'self'`/CDN listés s'exécutent ; `style-src` garde `'unsafe-inline'`
|
||||
(chantier séparé). Vérifié : `test_csp_nonce.py` 5/5, 0 handler inline
|
||||
restant dans les pages HTML (propriétés `onXxx = fn` en JS non concernées
|
||||
par la CSP).
|
||||
|
||||
---
|
||||
|
||||
## [2.28.12] — 2026-09-27
|
||||
|
||||
### Corrigé
|
||||
|
||||
- **BUG-081 — `GET /api/auth/mfa/status` → 500 quand l'auth est désactivée.**
|
||||
Le pseudo-user `anonymous` (auth désactivée, mode E2E/CI) n'a aucune entrée
|
||||
en store : `get_user(...)` → `None` puis `AttributeError` sur `user.get`.
|
||||
Garde `None` → payload « MFA désactivé » (`mfa_enabled: false`,
|
||||
`totp_enabled: false`, `webauthn_credentials: 0`). Test : `tests/test_mfa.py`
|
||||
(`TestMfaStatusAuthDisabled`, échoue en 500 sans le correctif).
|
||||
|
||||
---
|
||||
|
||||
## [2.28.11] — 2026-09-27
|
||||
|
||||
---
|
||||
|
||||
## [2.28.10] — 2026-09-27
|
||||
|
||||
### Corrigé
|
||||
|
||||
- **BUG-083 — job CI `security` rouge : le runner tronquait le `#` du `run:` pip-audit.**
|
||||
Le runner Gitea Act coupe naïvement au premier `#` (même entre
|
||||
guillemets) : `echo "... see #87)"` devenait une citation non fermée
|
||||
(`unexpected EOF while looking for matching '"'"`). Seul `run:` du
|
||||
workflow avec un `#` ; l'echo n'a plus de `#` (réf `#87` en commentaire
|
||||
YAML, jamais vu par le shell). Garde-fou : `tests/test_ci_workflow.py`
|
||||
(aucun `#` dans le code des `run:`, `upload.test.mjs` verrouillé dans
|
||||
l'étape JSDOM — BUG-082).
|
||||
|
||||
---
|
||||
|
||||
## [2.28.9] — 2026-09-27
|
||||
|
||||
### Corrigé
|
||||
|
||||
- **BUG-082 — CI `lint` rouge : suites frontend exigeant `jsdom`.**
|
||||
`tests/frontend/upload.test.mjs` puis `config-ai-keys.test.mjs` (imports
|
||||
statiques `jsdom`, introduits par `#89`) étaient exécutés dans l'étape
|
||||
frontend racine où `jsdom` n'est jamais installé (`ERR_MODULE_NOT_FOUND`,
|
||||
rouge depuis `7bee4a2`). Déplacés dans l'étape JSDOM (les deux branches,
|
||||
après install si besoin) ; garde-fou `tests/test_ci_workflow.py` :
|
||||
aucun fichier de l'étape racine ne doit importer `jsdom` statiquement.
|
||||
|
||||
---
|
||||
|
||||
## [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),
|
||||
`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).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+3
-3
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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.8).
|
||||
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.8 | Dernière mise à jour : Septembre 2026*
|
||||
*Projet : ObsiGate | Version : 2.28.14 | Dernière mise à jour : Septembre 2026*
|
||||
|
||||
@@ -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.
|
||||
|
||||
[]()
|
||||
[]()
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://www.docker.com/)
|
||||
[](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.8).
|
||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.28.14).
|
||||
|
||||
---
|
||||
|
||||
*Project: ObsiGate | Version: 2.28.8 | Last updated: September 2026*
|
||||
*Project: ObsiGate | Version: 2.28.14 | Last updated: September 2026*
|
||||
|
||||
@@ -823,6 +823,16 @@ async def mfa_status(current_user=Depends(require_auth)):
|
||||
"""Return current user's MFA status."""
|
||||
from .user_store import get_user
|
||||
user = get_user(current_user["username"])
|
||||
if user is None:
|
||||
# BUG-081 : auth désactivée (OBSIGATE_AUTH_ENABLED=false) → le
|
||||
# pseudo-user "anonymous" n'a aucune entrée en store : pas de MFA,
|
||||
# et surtout pas de 500 (`AttributeError` sur `user.get`).
|
||||
return {
|
||||
"mfa_enabled": False,
|
||||
"mfa_method": None,
|
||||
"totp_enabled": False,
|
||||
"webauthn_credentials": 0,
|
||||
}
|
||||
return {
|
||||
"mfa_enabled": user.get("mfa_enabled", False),
|
||||
"mfa_method": user.get("mfa_method"),
|
||||
|
||||
+5
-1
@@ -182,9 +182,13 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||||
# A route may set a stricter per-response policy (e.g. ``sandbox`` for
|
||||
# standalone SVG, #108-B3); keep it instead of overwriting it.
|
||||
if "Content-Security-Policy" not in response.headers:
|
||||
# #87 T5c : `script-src` sans 'unsafe-inline' — seuls les scripts
|
||||
# avec un nonce frais (`backend.csp`) ou servis par 'self'/CDN
|
||||
# listés s'exécutent. `style-src` garde 'unsafe-inline' (attributs
|
||||
# `style=` et `el.style` omniprésents — chantier séparé).
|
||||
response.headers["Content-Security-Policy"] = (
|
||||
"default-src 'self'; "
|
||||
f"script-src 'self' 'unsafe-inline' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
||||
f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
||||
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; "
|
||||
"img-src 'self' data: blob:; "
|
||||
"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net; "
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.28.8"
|
||||
version = "2.28.14"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.28.8"
|
||||
version = "2.28.14"
|
||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||
authors = ["Bruno Charest"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||
"productName": "ObsiGate",
|
||||
"version": "2.28.8",
|
||||
"version": "2.28.14",
|
||||
"identifier": "com.obsigate.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../frontend",
|
||||
|
||||
@@ -188,8 +188,7 @@ 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-081* | `GET /api/auth/mfa/status` → 500 quand l'auth est désactivée (`user` None, `AttributeError` sur `user.get`) | 🟢 corrigé | P1 | ⚙️ backend | IA | `backend/auth/router.py::mfa_status`, `tests/test_mfa.py` | Auth désactivée : `curl http://127.0.0.1:2029/api/auth/mfa/status` → 500 (reproduit live 2026-09-27) | Garde `user is None` → payload MFA désactivé (`mfa_enabled: false`, `totp_enabled: false`, `webauthn_credentials: 0`) ; test `TestMfaStatusAuthDisabled` (échoue en 500 sans le correctif). Vérifié : `test_mfa.py` 32 passed, ruff/mypy 0 | `require_auth` laisse passer le pseudo-user anonymous, `get_user(username)` → None non gardé. Trouvé via les logs E2E pendant BUG-080 |
|
||||
| *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)
|
||||
@@ -276,6 +275,9 @@ 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 |
|
||||
| 2026-09-27 | BUG-082 | Correction CI | `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-082** : `lint` rouge (`ERR_MODULE_NOT_FOUND: jsdom`, rouge depuis `7bee4a2`) — les fichiers de l'étape frontend racine à import statique `jsdom` (`upload.test.mjs`, puis `config-ai-keys.test.mjs` révélé par le CI après le 1er fix), alors que `jsdom` n'est installé que dans `tests/frontend/node_modules` (étape JSDOM). Les deux déplacés dans l'étape JSDOM (les deux branches) ; garde-fou `test_ci_workflow.py` généralisé (aucun fichier racine à import statique jsdom + suites verrouillées en JSDOM, contre-preuve OK). Vérifié : étape racine verte (11 suites) + `upload` et `config-ai-keys` verts depuis `tests/frontend/`. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-27 | BUG-083 | Correction CI | `.gitea/workflows/ci.yml`, `tests/test_ci_workflow.py` (nouveau), `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-083** : job `security` rouge — 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 '"'"`, `/var/run/act/workflow/4` ligne 2). Seul `run:` du workflow avec un `#` (les `#` des noms d'étapes Bandit/Npm audit sont inoffensifs, ces étapes passent). Correctif : echo sans `#` (réf `#87` en commentaire YAML). Garde-fou `test_ci_workflow.py` (aucun `#` dans le code des `run:`, `upload.test.mjs` verrouillé en étape JSDOM — BUG-082) + contre-preuve sur l'ancien `ci.yml`. Vérifié : 56 passed. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
| 2026-09-27 | BUG-081 | Correction | `backend/auth/router.py`, `tests/test_mfa.py`, `CHANGELOG.md`, `docs/ISSUES_TODOLIST.md` | **BUG-081** : `GET /api/auth/mfa/status` répondait 500 quand l'auth est désactivée — le pseudo-user `anonymous` n'a aucune entrée en store (`get_user` → `None`, `AttributeError` sur `user.get`). Garde `user is None` → payload « MFA désactivé ». Test `TestMfaStatusAuthDisabled` (échoue en 500 sans le correctif). Vérifié : `test_mfa.py` 32 passed, ruff/mypy 0. | 🟢 corrigé (en attente vérif utilisateur) |
|
||||
|
||||
---
|
||||
|
||||
@@ -286,7 +288,9 @@ Avant de corriger quoi que ce soit, un agent IA doit :
|
||||
|
||||
| # | Titre | Date résolution | Résolu par | Correctif / Commit | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| *(aucun pour l'instant)* | | | | | |
|
||||
| *BUG-083* | Job CI `security` rouge : le runner Gitea Act tronque le script `pip-audit` au premier `#` (citation de l'echo non fermée → `unexpected EOF while looking for matching '"'`) | 2026-09-27 | Utilisateur | `run:` assaini (echo sans `#`, réf `#87` en commentaire YAML) ; `tests/test_ci_workflow.py` (2 tests : aucun `#` dans le code des `run:`, `upload.test.mjs` verrouillé en étape JSDOM) ; vérifié : 56 passed (ci_workflow + e2e_harness + version), contre-preuve OK sur l'ancien `ci.yml` | Seul `run:` du workflow contenant un `#` (`see #87` dans l'echo). Les `#` des noms d'étapes (Bandit, Npm audit) sont inoffensifs (ces étapes passent). Correctif : echo sans `#`, réf `#87` en commentaire YAML |
|
||||
| *BUG-082* | CI `lint` rouge : suites frontend à import statique `jsdom` exécutées dans l'étape racine où `jsdom` n'est jamais installé | 2026-09-27 | Utilisateur | `upload.test.mjs` + `config-ai-keys.test.mjs` déplacés dans l'étape JSDOM (les deux branches) ; garde-fou `test_ci_workflow.py` (aucun fichier racine à import statique jsdom + suites verrouillées en JSDOM) ; vérifié : étape racine verte + `upload` et `config-ai-keys` verts depuis `tests/frontend/` | `jsdom` ne vit que dans `tests/frontend/node_modules` (installé par l'étape JSDOM). Correctif : déplacer les suites concernées dans l'étape JSDOM |
|
||||
| *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) | 2026-09-27 | Utilisateur | `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) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ObsiGate — Roadmap
|
||||
|
||||
> **Version :** 2.28.8 | **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)**
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsigate",
|
||||
"version": "2.28.8",
|
||||
"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": {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
|
||||
+28
-12
@@ -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 }
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""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"
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -367,3 +367,64 @@ class TestMfaApiEndpoints:
|
||||
data = login_resp.json()
|
||||
assert "access_token" in data
|
||||
assert data.get("mfa_required") is None
|
||||
|
||||
|
||||
# ── BUG-081 : /api/auth/mfa/status avec auth désactivée ──────────────────
|
||||
|
||||
@pytest.fixture
|
||||
def mfa_client_noauth():
|
||||
"""TestClient avec auth DÉSACTIVÉE (OBSIGATE_AUTH_ENABLED=false)."""
|
||||
tmp = Path(tempfile.mkdtemp())
|
||||
data_dir = tmp / "data"
|
||||
data_dir.mkdir()
|
||||
|
||||
orig_cwd = os.getcwd()
|
||||
test_vault_path = os.path.abspath("test-vault")
|
||||
os.chdir(str(tmp))
|
||||
|
||||
os.environ["VAULT_1_NAME"] = "TestVault"
|
||||
os.environ["VAULT_1_PATH"] = test_vault_path
|
||||
os.environ["OBSIGATE_AUTH_ENABLED"] = "false"
|
||||
os.environ["OBSIGATE_WATCHER_ENABLED"] = "false"
|
||||
|
||||
import backend.main
|
||||
backend.main._load_config = lambda: {"watcher_enabled": False}
|
||||
|
||||
from backend.main import app
|
||||
from backend.indexer import build_index, index
|
||||
for key in list(index.keys()):
|
||||
del index[key]
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(build_index())
|
||||
|
||||
from backend.search import init_inverted_index
|
||||
init_inverted_index()
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
client = TestClient(app, raise_server_exceptions=False)
|
||||
yield client
|
||||
|
||||
if hasattr(client, 'close'):
|
||||
client.close()
|
||||
loop.run_until_complete(asyncio.sleep(0))
|
||||
|
||||
os.chdir(orig_cwd)
|
||||
shutil.rmtree(str(tmp), ignore_errors=True)
|
||||
for k in ["VAULT_1_NAME", "VAULT_1_PATH", "OBSIGATE_AUTH_ENABLED",
|
||||
"OBSIGATE_WATCHER_ENABLED"]:
|
||||
os.environ.pop(k, None)
|
||||
|
||||
|
||||
class TestMfaStatusAuthDisabled:
|
||||
"""BUG-081 : `GET /api/auth/mfa/status` ne doit pas répondre 500 quand
|
||||
l'auth est désactivée (pseudo-user `anonymous` sans entrée en store)."""
|
||||
|
||||
def test_mfa_status_anonymous_returns_disabled(self, mfa_client_noauth):
|
||||
resp = mfa_client_noauth.get("/api/auth/mfa/status")
|
||||
assert resp.status_code == 200, f"BUG-081: {resp.status_code} {resp.text[:200]}"
|
||||
body = resp.json()
|
||||
assert body["mfa_enabled"] is False
|
||||
assert body["totp_enabled"] is False
|
||||
assert body["webauthn_credentials"] == 0
|
||||
|
||||
Reference in New Issue
Block a user