Commit Graph

34 Commits

Author SHA1 Message Date
4df930bfe6 fix: persister le token même dans le chemin d'erreur de get_informations/get_messages
Cause racine : quand pronotepy reçoit une PronoteAPIError (ex. code 20 « page
expirée »), il appelle refresh() en interne, qui peut roter le token en mémoire
(client.password mis à jour avec un nouveau jetonConnexionAppliMobile). Le retry
peut aussi échouer — l'exception atteint get_informations()/get_messages() qui
l'attrapent et retournent [] (mode dégradé). Mais _persist_credentials()
n'était appelé que dans le chemin de SUCCÈS — le token rafraîchi en mémoire
n'était jamais persisté. Au run suivant, token_login utilisait le token
périmé → échec KeyError 'dataSec' → PronoteAuthRotationError.

Correction : appeler _persist_credentials() aussi dans le chemin d'erreur de
get_informations() et get_messages(), avant le return []. L'ancien token est
déjà invalidé côté serveur lors du refresh — ne pas persister le nouveau token
garantit la perte du seul token valide.

Tests : 3 nouveaux tests (persistance dans le chemin d'erreur, sauvegarde du
token rafraîchi sur erreur). 694 passés, couverture 94.93%.
2026-09-10 13:34:51 +02:00
d26cef8d3d fix: persister le token après chaque opération de données + corriger doc QR code
Cause racine : pronotepy peut rafraîchir (rotater) le token en mémoire pendant
l'exécution via refresh() automatique après une PronoteAPIError. L'ancien code
ne persistait les credentials qu'après le login initial, pas après les
opérations de données. Le token roté en mémoire était perdu → au run suivant,
token_login échouait avec le token périmé (KeyError 'dataSec').

Correction :
- PronoteClient._persist_credentials() : méthode centralisée qui persiste
  export_credentials() après chaque opération réussie (get_lessons,
  get_homeworks, get_messages, get_informations)
- Le token rafraîchi par le serveur pendant l'exécution est maintenant
  toujours persisté, même si le pipeline échoue ensuite

Documentation :
- .env.example : variables QR plus visibles (exemple qr_token décommentable)
- AGENTS.md : QR code depuis le site web Pronote (pas l'app mobile),
  persistance après chaque opération de données
- Wiki GuidePronote : procédure corrigée (site web, pas app Android/iOS),
  mention de la persistance après chaque opération

Tests : 5 nouveaux tests de persistance (691 passés, couverture 94.92%)
2026-09-10 12:10:42 +02:00
0363898669 feat: authentification QR code / token pour Pronote
Ajoute le mode d'authentification PRONOTE_AUTH_MODE=qr_token comme alternative
au mode password pour les instances Pronote utilisant HubEduConnect/EduConnect
où l'authentification par mot de passe échoue (CAPTCHA, MFA, flux SAML).

Nouveaux éléments :
- PronoteSettings : auth_mode, qr_code_file, qr_pin (SecretStr)
- PronoteAuthState : persistance du token rotatif dans .pronote_auth_state.json
  (écriture atomique, permissions 0600, symlink-safe via O_EXCL|O_NOFOLLOW)
- PronoteClient._connect_qr_token() : token_login avec creds persistés,
  qrcode_login pour l'enrôlement initial, export_credentials persisté après
  chaque login réussi
- PronoteAuthRotationError : levée en cas d'échec de rotation du token,
  propagée sans wrapping à travers PronoteFetcher et fetch_step jusqu'à
  PipelineRunner.run() qui notifie via XMPP (si canal disponible et dry_run inactif)
- _is_pronotepy_configured() mode-aware : qr_token ne requiert que PRONOTE_URL
- _collect_auth_secrets() : redaction des secrets explicites (token, PIN, jeton QR)
  dans tous les logs du chemin d'authentification

Documentation :
- .env.example : PRONOTE_AUTH_MODE, PRONOTE_QR_CODE_FILE, PRONOTE_QR_PIN
- AGENTS.md : contrat d'authentification QR code / token
- Wiki GuidePronote : section enrôlement, exécutions suivantes, ré-enrôlement

Tests (686 passés, couverture 94.87%) :
- 5 tests config QR, 9 tests auth_state, 10 tests client QR, 3 tests propagation,
  4 tests intégration rotation end-to-end, 4 tests fallback mode-aware
- Tests de non-fuite : sentinelles distinctes pour token, PIN, jeton QR

Co-authored-by: coder/litellm/coder <coder@agents.invalid>
2026-09-08 23:15:06 +02:00
3b38253575 fix: PRONOTE_URL ignoré à cause du double préfixe env_prefix
Le champ pronote_url dans PronoteSettings avec env_prefix=PRONOTE_ produisait PRONOTE_PRONOTE_URL au lieu de PRONOTE_URL. Renomme le champ en url pour que le mécanisme standard produise PRONOTE_URL. Toutes les références mises à jour dans le code de production et les tests. Décision d'architecture : renommage préféré à un contournement par alias (mypy + dette technique).

Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
2026-09-08 21:24:10 +02:00
82b9877aad fix: PRONOTE_ENT optionnel pour les connexions pronotepy directes
PRONOTE_ENT était incorrectement traité comme obligatoire pour pronotepy. Rend ENT optionnel pour les connexions directes, conformément à la spécification. Corrige le mode pronotepy explicite et le mode auto sans iCal. 10 tests de régression ajoutés.

Co-authored-by: Antoine Van Elstraete <antoine@van-elstraete.net>
Co-committed-by: Antoine Van Elstraete <antoine@van-elstraete.net>
2026-09-08 20:54:03 +02:00
b474f02e90 fix(M14): harden secret scanner — prefixed vars, index blobs, extensionless files
Correct five findings from independent review and security audit of the
M14 deployment secret scanner:

scripts/check_secrets.py:
- Regex: \b[a-z0-9_]* prefix before sensitive keywords to detect
  PRONOTE_PASSWORD, CALDAV_PASSWORD, AI_API_KEY and similar prefixed
  variable names (was: \b which doesn't match before underscore)
- Regex: minimum secret value length reduced from {8,} to {3,} for
  literal, unquoted, and URL parameter patterns
- Regex: unquoted pattern {3,} -> {2,} for 3-char total minimum
- --staged: reads Git index blobs via `git show :<path>` instead of
  working-tree files (ContentProvider type alias, _staged_content_provider)
- Extensionless deployment files: _EXTRA_NAMES allowlist for pronote_sync
- ContentProvider type alias documented with #: Sphinx comment

tests/unit/test_check_secrets.py (4 new tests, 9 total):
- test_main_detects_prefixed_secret_assignment: PRONOTE_PASSWORD detected
- test_main_detects_short_secret_assignment: 6-char secret detected
- test_staged_mode_reads_index_content_not_working_tree: working-tree
  content set to non-matching value to distinguish index from worktree
- test_main_scans_extensionless_deployment_file: pronote_sync scanned

TODO.md: all 5 M14 checklist items checked

Validation: 636 tests, coverage 95.67%, ruff/mypy/bandit/pre-commit green.

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-08 17:20:08 +02:00
d60357a017 feat(M14): add deployment artifacts and secret check
Co-authored-by: Codex/gpt-5.6-terra <codex-gpt-5.6-terra@agents.invalid>
2026-09-08 16:42:59 +02:00
b2106e75ac fix(M10): apply FIXME_M10 corrections (transport, dry_run, format, security)
Fix all 8 findings from the independent review (FIXME_M10.md):

#1 Transport compatible with slixmpp 1.17.0 (D5):
  - Use real ClientXMPP type (remove Any), JID with resource
  - connect(host, port) explicit, no use_tls kwarg
  - enable_direct_tls/enable_starttls configured before connect
  - Single timeout via asyncio.Future for session_start/failed_auth/disconnected
  - Remove premature 'starttls' in features check, remove auto_reconnect
  - try/finally guarantees disconnect on all paths (#4)

#2 Factory dry_run no longer bypassed (D6):
  - Single send() entry point in SyncXmppChannel
  - dry_run check before any ClientXMPP creation
  - Remove XmppChannel.send() dual implementation

#3 Thread daemon removed — single asyncio.run(), documented limitation

#5 Richer message format:
  - Target date header, change type [Ajouté/Supprimé/Modifié]
  - Lesson times, homework due date, message author
  - No pronote_messages duplication (external_info = blog + other_info only)

#6 Error contract unified (D6):
  - Channel.send() -> bool never raises PipelineWarning
  - Errors logged with redaction, returns False
  - PipelineWarning(step='xmpp') will be created by pipeline M11

#7 Tests faithful to slixmpp 1.17.0 API:
  - FakeClientXMPP with real connect(host,port)/disconnect() signatures
  - Assertions on host, port, resource, mtype='chat'
  - No RuntimeWarning from unawaited coroutines

#8 .secrets.baseline restored from main

Coverage: 96.44% on channels/, 600 tests pass, pre-commit all-files green.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-08 02:16:28 +02:00
b61d314b7f test: add M10 integration tests and coverage to 99% (M10-U7)
Integration tests validating the 3 M10 acceptance criteria end-to-end
with mocked slixmpp:
1. XmppChannel.send sends formatted direct message
2. XMPP error → PipelineWarning, no unhandled exception
3. No secret in XMPP logs (sentinel-based verification)

Additional unit tests covering daemon-thread branch of SyncXmppChannel,
deferred failed_auth, session timeout, disconnected handler, and
_format_message edge cases. Channels coverage: 99.18%.

All M10 checklist items marked complete in TODO.md.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-08 00:58:40 +02:00
e07a6d709d feat: implement get_channel factory for XMPP channel (M10-U6)
get_channel(settings, dry_run=False) -> Channel | None with:
- enabled=False → None (no warning, no exception)
- enabled=True + missing jid/password/to/host → redacted warning log, None
- enabled=True + complete config → SyncXmppChannel instance
- Factory never raises exceptions (D2 non-blocking degradation)
- redact_secrets with extra_secrets=[password, jid, to] on warning logs

Re-exports Channel, XmppChannel, SyncXmppChannel from channels package.

19 unit tests covering disabled, misconfigured, complete, dry-run, and
secret-safe warning log scenarios.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:29:25 +02:00
1962e13eba feat: implement XmppChannel and SyncXmppChannel (M10-U4+U5)
XmppChannel sends direct messages via slixmpp ClientXMPP with:
- _format_message: 5 emoji sections (synthèse, agenda, devoirs, messages, infos)
  with sanitize_plaintext on all content (SEC-XMPP-06)
- send_async: public async method with connect, STARTTLS verification,
  send_message, disconnect lifecycle (D1, SEC-XMPP-04)
- send: sync wrapper via asyncio.run() for direct callers

SyncXmppChannel adapts async XmppChannel for synchronous pipeline use (D4):
- asyncio.run() when no event loop running (nominal pipeline)
- daemon thread with timeout when event loop already running
- Returns False on any error, never raises (non-blocking)

Security:
- auto_reconnect=False, failed_auth → disconnect + PipelineWarning (SEC-XMPP-04)
- __cause__ and __context__ cleared on all PipelineWarning raises (SEC-XMPP-05)
- redact_secrets with extra_secrets=[jid, password, to] on all logs (SEC-XMPP-02)
- STARTTLS features check post-connection, disconnect on failure (D1)
- dry-run mode logs redacted message without connecting

28 unit tests (20 channel + 8 adapter) covering success, errors, security.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 23:28:18 +02:00
dcf7f69c5a feat: add sanitize_plaintext for XMPP text sanitization (M10-U3)
Add sanitize_plaintext(text: str) -> str to utils/text.py for preparing
XMPP plain-text message bodies from untrusted Pronote/AI content.

- Strips HTML tags via BeautifulSoup (html.parser)
- Strips C0, DEL, and C1 control characters (preserves \t, \n, \r)
- Preserves Unicode including emojis (📌📅📚💬📢)
- Idempotent: f(f(x)) == f(x)
- Addresses SEC-XMPP-06: XMPP injection hardening

37 unit tests covering HTML, entities, control chars, emojis, idempotence.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:13 +02:00
7d765476de feat: define Channel Protocol for output channels (M10-U2)
Add @runtime_checkable Channel Protocol with send(XmppMessage) -> bool
as the structural contract for all output channels (XMPP, future CalDAV, etc).

6 unit tests covering protocol structure, conforming/non-conforming classes,
method signature introspection, and bool return type.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:04:01 +02:00
68a5d96c2a feat: add TLS policy and field constraints to XmppSettings (M10-U1)
Enforce TLS on non-loopback hosts via @field_validator on use_tls,
and add unconditional Field constraints on port (1-65535) and timeout (>0).

Security:
- use_tls=False rejected outside {localhost, 127.0.0.1, ::1} regardless of enabled
- field_validator on use_tls (not model_validator) prevents raw config leakage
- hide_input_in_errors=True as defense-in-depth
- Validation error messages contain no secrets (jid, password, recipient)

16 unit tests covering port/timeout bounds, TLS policy, loopback, secret safety.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:01:48 +02:00
a5a8183663 feat: add PipelineWarning for non-blocking pipeline errors (M10-U0)
Add PipelineWarning(PronoteSyncError) to the canonical error hierarchy.
This non-blocking warning type is used by the XMPP channel (and future
channels) to signal recoverable failures without breaking the pipeline.

- PipelineWarning inherits from PronoteSyncError, not Warning builtin
- Constructor: (message, step=None) with recoverable=True
- 8 unit tests covering inheritance, raising, catching, attributes

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-07 21:01:36 +02:00
13e058f22c feat: add openai-compatible provider for custom AI endpoints
Add AI_PROVIDER=openai-compatible mode that reuses OpenAISynthesisProvider
with a validated custom base_url, allowing any OpenAI-compatible API
(OpenRouter, Ollama, LiteLLM proxy, etc.) without new code.

Configuration:
- AISettings.provider now accepts openai-compatible
- New AISettings.allow_insecure_http: bool = False (HTTP opt-in)
- .env.example: commented examples for OpenRouter (HTTPS) and Ollama (HTTP)

Factory validation (_validate_openai_compatible_config):
- base_url and model required, api_key required (MVP)
- HTTPS enforced unless allow_insecure_http=true
- Credentials in URL rejected, sensitive query params rejected
  (including valueless params via keep_blank_values=True)
- Malformed URLs and missing hostname rejected (ValueError caught)
- No /v1 manipulation; degraded to None + warning on invalid config
- redact_url() used for all URL warnings

Tests: 13 new factory tests in test_synthesis.py covering routing,
URL validation, HTTP policy, credentials, sentinel non-leak, no-network.
Coverage: 91.57% (synthesis module).

Docs: GUIDE_DEV_PYTHON.md §9.5 updated with 3-provider table, validation
rules, and synchronized code example.

mypy override for openai.* (follow_imports=skip) to work around
mypy 2.3.1 internal error in pre-commit's isolated environment.

Co-authored-by: opencode/coder anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
Co-authored-by: opencode/test-engineer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
Co-authored-by: opencode/tech-writer anthropic.claude-sonnet-4-5 <anthropic.claude-sonnet-4-5@agents.invalid>
2026-09-07 19:44:40 +02:00
19cbf8f13f fix(M9): corrections d'audit FIXME_M9 — secrets, messages, validation, tests
Cinq corrections de l'audit FIXME_M9 :
- redact_secrets() étendue avec extra_secrets pour masquer les clés brutes ;
  providers stockent SecretStr jusqu'à l'appel SDK.
- _build_prompt() inclut le contenu des messages (tronqué à 500 car.) ;
  prompt système renforcé contre l'injection.
- _validate_output() supprime les emojis et rejette titre/liste/HTML → None.
- Tests litellm utilisent importorskip + LITELLM_LOCAL_MODEL_COST_MAP=true.
- .env.example désactive l'IA par défaut (AI_ENABLED=false).
- Documentation §9.2-§9.5 alignée avec l'implémentation (SDK openai, SecretStr,
  factory réelle, validation sortie, politique hors réseau).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 19:01:25 +02:00
92833060e2 feat(M9): synthèse IA — protocole, providers OpenAI/litellm, factory, tests
Synthèse optionnelle via SDK openai (client injectable, prompt système
FR, max 800 car., timeout 30 s, temp 0.3). Mode dégradé strict :
generate() ne lève jamais, retourne None si clé absente/timeout/erreur.
Provider litellm optionnel (extra ai-litellm) réutilisant le prompt
OpenAI. Factory get_synthesis_provider() selon AISettings. 23 tests
sans réseau, couverture synthesis/ 93%.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-07 17:07:05 +02:00
5907c9aeaf fix(M8): corrections d'audit FIXME_M8 — appariement, date, déterminisme, validateur
Quatre corrections bloquantes/majeures de l'audit FIXME_M8 :
- Appariement un-à-un déterministe (consommation du candidat sélectionné) ;
  1 réel / 2 théoriques → 1 REMOVED, 2 réels / 1 théorique → 1 ADDED.
- Filtrage strict par date : les cours réels hors target_date sont exclus
  du matching avec un warning logé (décision architecte : pas d'exception).
- Déterminisme des détails : formatage via sorted(set(...)) au lieu de
  set(...) brut, indépendant de PYTHONHASHSEED.
- Validateur AgendaChange strict : ADDED = lesson seule, REMOVED =
  theoretical_lesson seule, MODIFIED = les deux requis.
- Comparaison à la minute près dans _is_modified (cohérent avec _matches).
- Documentation §8.4/§8.5 alignée avec l'implémentation (tolérance 15 min,
  API compare(), normalize_subject référencé, appariement consommé).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 15:59:17 +02:00
093253a41c test(M8): tests unitaires pour AgendaComparator (17 cas)
Couvre : agendas vides, ADDED/REMOVED/MODIFIED, tolérance ±15 min
(bord inclusif), normalisation NFKC des matières, matching multi-candidats
par plus petit id, comparaison ordre-insensible des enseignants/salles,
statut != NORMAL, REMOVED par existence (pas par sélection), ordre
déterministe et idempotence. Couverture de sync/diff.py : 92%.

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-07 13:50:56 +02:00
a1bae41be8 fix(M7): corrections d'audit FIXME_M7 — sécurité, fenêtre, UID, timezone
Corrige les 5 constats de l'audit FIXME_M7 :

#1 (Bloquant) — Protection des événements non marqués :
- upsert_event() vérifie le marqueur X-PRONOTE-SYNC-MANAGED avant
  modification ; lève PronoteSyncError en cas de collision avec un
  événement non géré (aucune écriture)
- delete_event() vérifie le marqueur ; no-op avec warning si non géré
- Méthode privée _is_managed_event() factorisant le contrôle

#2 (Bloquant) — Fenêtre de synchronisation :
- Calcul en journées entières (minuit à minuit exclusif)
- Filtrage des données locales (lessons, homeworks, school_events) avant
  passage au planner
- Paramètre now injectable pour les tests

#3 (Bloquant) — UID canonique vs brut :
- list_managed_events() retourne (raw_uid, canonical_uid, vevent)
- compute_plan() matche par UID canonique, route les raw UID vers
  *_to_remove, retourne le mapping remote_raw_by_canonical
- executor.execute() utilise le raw UID pour les mises à jour (pas de
  doublon)
- Pas de migration destructive des UID distants existants

#4 (Correction) — Normalisation temporelle UTC :
- normalize_datetime_to_utc() dans utils/uid.py : naïve → Europe/Paris →
  UTC ; consciente → UTC
- Utilisée par generate_deterministic_uid() et component_to_signature()
- Deux représentations du même instant → même UID et même signature

#5 (Compatibilité) — date_search déprécié :
- Remplacement par calendar.search(start, end, event=True, expand=True)

Documentation :
- GUIDE_DEV_PYTHON.md : suppression des références obsolètes à
  sync/state.py et état SQLite/JSON ; mise à jour de l'API CalDAV
  (search au lieu de date_search, upsert par UID)
- TODO.md : M7 décoché (corrections en cours de validation)

Tests : 390 passés, couverture 95.61%

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 12:24:22 +02:00
b4b0247919 feat(M7): synchronisation différentielle CalDAV
Implémente la synchronisation des événements Pronote vers un calendrier
CalDAV (Nextcloud) de façon idempotente et sécurisée.

Production :
- sync/serialization.py : sérialisation Lesson/Homework/SchoolEvent vers
  VEVENT, signature sémantique (exclut DTSTAMP/CREATED/LAST-MODIFIED),
  enveloppe VCALENDAR complète avec VERSION:2.0 et PRODID
- sync/caldav.py : passerelle CalDAV isolant caldav>=1.3.0, résolution du
  calendrier via principal().calendars() avec boundary matching, upsert par
  UID (fetch-then-save), exceptions expurgées et __context__ propre, mot de
  passe non stocké en clair, context manager
- sync/planner.py : calcul explicite du CalDAVSyncPlan (add/update/remove
  par comparaison de signatures sémantiques, routage par préfixe d'UID)
- sync/executor.py : exécution du plan avec dry-run (aucune écriture),
  isolation des erreurs par événement, statut FAILED/SKIPPED/SUCCESS
- sync/synchronizer.py : orchestration en trois phases (scan, plan,
  exécution), SKIPPED si CalDAV non configuré
- sync/__init__.py : export synchronize()
- sources/pronote/client.py : normalisation UID via normalize_pronote_uid/
  generate_deterministic_uid (parité avec ical.py)
- config/settings.py : CalDAVSettings durci (url SecretStr, validation
  HTTPS, allow_insecure_http pour localhost, serializer redact_url)

Tests (381 passés, couverture 95.58%) :
- tests/unit/test_sync_serialization.py (21 tests)
- tests/unit/test_caldav_planner.py (16 tests)
- tests/unit/test_caldav_executor.py (18 tests)
- tests/unit/test_caldav_gateway.py (24 tests)
- tests/unit/test_caldav_security.py (18 tests)
- tests/unit/test_uid_equivalence.py (8 tests)
- tests/integration/test_caldav_sync.py (11 tests, faux serveur en mémoire)
- tests/conftest.py : fixtures partagées

Documentation :
- GUIDE_DEV_PYTHON.md §7 : API réelle caldav>=1.3.0, principal().calendars(),
  VCALENDAR complet, upsert par UID, pas d'état local, événements non gérés
  protégés, CalDAVSettings durci (SecretStr, HTTPS, allow_insecure_http)
- TODO.md : M7 coché
- .env.example : CALDAV_ALLOW_INSECURE_HTTP=false

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-07 09:24:18 +02:00
1d26d49e74 fix(M6): corrections d'audit FIXME_M6 — normalisation, secret, tests, doc
Corrige les 5 points de l'audit FIXME_M6 :

1. Normalisation des matières : fonction normalize_subject (NFKC +
   unification des espaces + suppression ponctuation + minuscule)
   partagée par la génération d'ID et le futur comparateur M8.
2. Expurgation du secret dans l'erreur de collision d'IDs :
   redact_secrets enveloppe l'identifiant dans le message.
3. Test even/odd avec même matière pour isoler la parité comme seul
   différenciateur d'ID ; tests de normalisation (casse, espaces,
   Unicode) ; test de non-fuite de secret.
4. TODO.md M6 : 8 items cochés après validation.
5. GUIDE_DEV §8.4 : bloc de code corrigé (clôture, types Lesson/
   TheoreticalLesson, comparaison des horaires en minutes, début ET
   fin, référence à normalize_subject).

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 23:30:47 +02:00
f9a1a5aa43 feat(M6): agenda théorique JSON avec parité des semaines et vacances scolaires
Implémentation complète de la source d'agenda théorique :

- model.py : modèles Pydantic de parsing JSON (TheoreticalLessonEntry,
  TheoreticalAgendaFile) avec validation des formats d'heure et de l'ordre
  début/fin.
- parity.py : WeekParityService déterministe calculant la parité d'une
  semaine (paire/impaire) à partir d'une date de référence.
- holidays.py : SchoolHolidayCalendar lisant un fichier JSON de vacances
  scolaires (zone A) avec bornes inclusives.
- provider.py : protocole TheoreticalAgendaProvider (get_lessons,
  get_lessons_for_range).
- file.py : JsonTheoreticalAgendaProvider implémentant le protocole :
  filtrage par parité et vacances, génération d'IDs déterministes
  incluant le type de semaine, validation de l'unicité des IDs,
  tri stable par identifiant.
- __init__.py : factory get_theoretical_provider câblant la
  configuration (None si désactivé, erreur si config de parité partielle).
- Fixtures : theoretical.json (9 leçons all/even/odd) et
  school_holidays.json (zone A, 4 périodes).
- 57 tests unitaires couvrant parsing, parité, vacances, provider,
  factory, déduplication de range, collisions d'IDs.
- Guide : §8 et §12 alignés avec le format JSON.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 23:06:29 +02:00
bfae1ca87f fix(M5): corrections d'audit — transport HTTP, statuts d'erreur, cache atomique
Corrige les 5 points de l'audit FIXME_M5 :

1. (Bloquant) Sépare transport HTTP et parsing : utilise requests.get()
   avec timeout explicite et en-têtes conditionnels, puis transmet le
   contenu à feedparser.parse() — supprime le paramètre inexistant
   request_timeout qui faisait échouer toute récupération réelle.
2. Rejette les statuts HTTP 4xx/5xx via raise_for_status() avant le
   parsing.
3. Préserve les validateurs de cache (etag, last_modified) d'entrée sur
   les chemins d'échec (exception, bozo) au lieu de les écraser à None.
4. Sauvegarde atomique de BlogRSSState : écrit dans un .tmp puis
   Path.replace() pour éviter la corruption sur interruption.
5. Déduplication normale silencieuse : les GUID déjà connus sont
   ignorés sans warning ; seuls les doublons intra-flux génèrent un
   avertissement.

Tests : 49 tests (32 client + 17 state) dont 11 nouveaux couvrant
transport HTTP réel, statuts 401/404/500, préservation des validateurs,
en-têtes conditionnels, doublons intra-flux et sauvegarde atomique.
Guide : §5 bis.7.1 aligné avec le nouveau pattern transport/parsing.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 20:58:04 +02:00
6d1a7a649f feat(M5): source blog RSS — fetch, parsing, déduplication et état persistant
Implémentation complète de la source blog RSS du collège :
- BlogRSSClient (sources/blog/rss.py) : client sans état récupérant et
  parsant le flux via feedparser, avec déduplication par ensemble de
  GUIDs connus, cache HTTP conditionnel (ETag/Last-Modified), conversion
  HTML→texte (BeautifulSoup), tri déterministe (date desc puis id asc),
  et mode dégradé (flux invalide/erreur → warning expurgé + liste vide).
- BlogRSSFetchResult (sources/blog/result.py) : résultat immuable
  contenant articles, en-têtes de cache et indicateur not_modified.
- BlogRSSState (sources/blog/state.py) : persistance JSON tolérante
  (GUIDs triés, version, ETag, Last-Modified) avec redaction des chemins
  dans les logs.
- Fixture tests/fixtures/blog_rss.xml : flux RSS 2.0 anonymisé, 3
  articles, dates fixes, ordre non chronologique.
- 38 tests unitaires (22 client + 16 state) couvrant parsing nominal,
  déduplication intra-flux, 304, bozo, erreurs réseau, non-fuite de
  secrets, tri secondaire, persistance d'état et tolérance aux fichiers
  corrompus.
- Documentation : TODO.md M5 coché, GUIDE_DEV_PYTHON.md §5 bis aligné
  avec l'API livrée (known_guids, BlogRSSFetchResult, BlogRSSState).
- Configuration : feedparser ajouté aux additional_dependencies du hook
  mypy pre-commit pour aligner l'environnement isolé avec le .venv.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 20:23:49 +02:00
16f9b57dc2 fix(M4): modes explicites ical/pronotepy stricts — suppression du repli réciproque
Les modes explicites (ical, pronotepy) ne basculent plus silencieusement vers
l'autre source en cas d'échec. Seul le mode auto conserve le repli iCal →
pronotepy sur exception. Conforme au contrat de TODO.md M4 et AGENTS.md.

- fallback.py : _agenda_sources() et _homework_sources() retournent
  (source, None) en mode explicite ; docstrings mises à jour.
- test_fallback.py : 4 tests remplacés, 4 tests ajoutés (non-repli +
  empty-list-is-success), 4 tests ajustés ; :rtype: None ajouté.

Co-authored-by: coder/coder coder@agents.invalid
Co-authored-by: test-engineer/test-engineer test-engineer@agents.invalid
2026-09-06 17:42:04 +02:00
bb1f90bf5f test(M4): couverture fallback.py 81→98% (11 tests ciblés)
- _fetch_agenda_ical sans ical_url → ValueError
- _agenda_sources AUTO pronotepy seul, _homework_sources modes
  explicites + AUTO + aucune source configurée
- fetch_agenda/fetch_homework : repli échoue → PipelineCriticalError,
  repli retourne [] → warning d'ambiguité
- fetch_informations : log redact_exception + re-raise sans secrets
- Couverture pronote_sync/sources/pronote/ : 95% (≥90% requis)

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 15:39:07 +02:00
9d9a55ed40 test+docs(M4): tests get_lessons/get_homeworks + repli unique + doc sync
Tests:
- test_pronote_client : get_lessons/get_homeworks success + error
  propagation, ENT resolution, factory parent/student, mode dégradé
  messages/informations (ConnectionError, TimeoutError)
- test_fallback : repli unique ICAL→pronotepy et PRONOTEPY→iCal,
  filtre target_date, fetch_messages log + re-raise sans secrets,
  aucune source configurée → PipelineCriticalError
- 59 tests passent (16 client + 17 fallback + 26 iCal)

Documentation:
- TODO.md : 7 items M4 cochés, contrat repli corrigé (unique, non
  réciproque) selon décision D4
- GUIDE_DEV_PYTHON.md : get_agenda_fallback → get_lessons/get_homeworks,
  pronote_url/account_type ajoutés au modèle + env vars + .env.example,
  AgendaSource StrEnum, §5.1.8 fallback contract mis à jour (repli unique)

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
Co-authored-by: opencode/tech-writer <tech-writer@agents.invalid>
2026-09-06 15:36:42 +02:00
1b550aa818 fix(M4): pronote_url + account_type et parsing iCal (STATUS:CANCELLED + blocs multi-dates)
Configuration (D1+D3) :
- PronoteSettings : ajout pronote_url (str | None) et account_type
  (Literal student/parent, défaut parent)

Parsing iCal (Fix 4+7) :
- parse_ical : détection STATUS:CANCELLED en plus de CATEGORIES
- parse_body : list[tuple[date, str]] au lieu de dict[date, str]
  pour préserver les blocs multiples à la même date
- parse_homework_blocks : adapté aux listes

Co-authored-by: opencode/coder <coder@agents.invalid>
2026-09-06 15:23:34 +02:00
e0cd073f9a test(M4): tests unitaires iCal, client pronotepy et repli
- test_ical.py : 20 tests (fetch file://, parsing VEVENT, détection
  cours annulé, collect_homeworks avec déduplication, normalisation,
  génération d'ID déterministe, redaction des erreurs)
- test_pronote_client.py : 9 tests (Protocol, messages mockés,
  informations, agenda fallback, credentials manquants, sécurité
  mot de passe)
- test_fallback.py : 16 tests (modes ICAL/PRONOTEPY/AUTO, repli,
  PipelineCriticalError si les deux sources échouent, redaction
  des erreurs)
- pre-commit : ajout de responses et pytest-mock au hook mypy
- Total : 42 nouveaux tests M4 (122 tests au total sur le projet)

Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 14:10:16 +02:00
292aef4f5b fix(M3): immuabilité et validation des modèles — correction des écarts FIXME_M3
Immuabilité :
- Collections des modèles frozen : list → tuple (Lesson, TheoreticalLesson,
  SchoolEvent, AgendaDiff, Homework, XmppMessage, ExternalInfo)
- Gel de HomeworkBlock et ExternalInfo (sous-modèles imbriqués dans contrats)
- Valeurs par défaut immuables : Field(default=(), ...)

Validation de domaine :
- TheoreticalLesson.day_of_week : Field(ge=0, le=6)
- Lesson/TheoreticalLesson : start < end (validateur de modèle)
- SchoolEvent : from_date <= to_date
- AgendaChange : cohérence type/payload (ADDED/MODIFIED → lesson,
  REMOVED → theoretical_lesson)
- CalDAVSyncResult : compteurs Field(ge=0) sur added/updated/removed

Tests : 66 tests (construction/sérialisation, immuabilité, invariants)
couvrant le gel, la mutabilité légitime, et les cas invalides par invariant.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 12:36:27 +02:00
2f6184df0b fix(M2): configuration, secrets et rédaction — correction des écarts FIXME_M2
Sécurité :
- ical_url : str → SecretStr | None (masquage dans str/repr/model_dump_json)
- En-têtes Authorization/Proxy-Authorization : masquage complet de la valeur
- _URL_PATTERN : insensible à la casse (HTTPS:// reconnu)
- redact_url() : masquage du nom d'utilisateur (userinfo complet)

Configuration :
- Sous-configs : Field(default_factory=...) pour rechargement à chaque appel
- Suppression du singleton settings (injection de dépendances)
- .env.example : ajout AI_PROVIDER et CALDAV_CALENDAR_PATH
- Guide : BLOG_RSS_ENABLED → BLOG_ENABLED, AI_MODEL=None par défaut
- .secrets.baseline : ligne décalée 5112 → 5117 (faux positif audité)
- pre-commit : ajout de pytest aux additional_dependencies du hook mypy

Tests : 15 tests (config + redaction) couvrant rechargement, masquage
SecretStr, en-têtes Authorization, URL auth intégrée et casse variable.

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/test-engineer <test-engineer@agents.invalid>
2026-09-06 12:35:55 +02:00
a0ac75cec6 fix(M1): correction des écarts d'audit FIXME_M1
- requires-python : >=3.13 → >=3.13.5 (alignement sur le contrat TODO.md)
- test de fumée : ajout de tests/unit/test_smoke.py (pytest sort code 0)
- .gitignore : ajout de la règle *.tmp.ics pour les artefacts iCal temporaires
- .secrets.baseline : audit du faux positif GUIDE_DEV_PYTHON.md:5112
  (is_secret: false, is_verified: true → VERIFIED_FALSE)

Co-authored-by: opencode/coder <coder@agents.invalid>
Co-authored-by: opencode/verifier <verifier@agents.invalid>
2026-09-06 11:59:50 +02:00