Files
Blomios 6969dc7988 chore(wip): état runtime .ideai (conversations, layouts, mémoire, skills)
Snapshot de l'état runtime accumulé sur develop : logs/handoffs de
conversations, layouts, notes mémoire (dont git-owns-commit-merge-decisions)
et catalogue de skills.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:14:01 +02:00

63 KiB
Raw Permalink Blame History

upTo: 7c627a21-5a9f-470e-9137-a63697ae87da objective: Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont

Objectif : Tâche : ajouter UN test fonctionnel anti-régression de la communication inter-agent (round-trip question/réponse) au plus haut niveau de fidélité possible SANS lancer de vrai CLI IA (zéro token). Cont

  • Response: LP4-1 — Passe de tests d'intégration de l'enforcement Landlock : VERT. La propriété de sûreté n°1 (IdeA jamais sandboxé) est VÉRIFIÉE. Aucun écart. ## Tests ajoutés (4) - sandbox::landlock::tests::enforcement_is_confined_to_the_enforcing_thread_idea_is_never_sandboxedPROPRIÉTÉ DE SÛRETÉ n°1. enforce() lancé sur un thread jetable (comme l'adapter), grant RW sur granted. On prouve d'abord que le thread jetable EST sandboxé (write hors racine = PermissionDenied), PUIS que le thread parent (≈ process IdeA) écrit ET lit toujours HORS racines ⇒ le domaine Landlock ne fuit pas sur IdeA. - sandbox::landlock::tests::read_only_plan_fences_reads_to_the_grantdimension READ (RO). Fichiers pré-créés avant enforce. Grant RO sur granted : lecture SOUS la racine = OK ; lecture HORS = PermissionDenied (EACCES). Confirme et documente le comportement signalé par DevBackend : dès qu'un grant RO est posé, la classe read est handled ⇒ TOUTES les lectures hors racines sont fermées (un plan read-restreint réaliste devra inclure les chemins système — concern LP4-2). - sandbox::landlock::tests::empty_plan_under_deny_posture_is_enforced_without_restrictionplan vide / bash-only sous posture Deny. Un plan allowed vide (= bash-only après compile LP4-0) avec Posture::DenyEnforced sans aucune restriction et SANS KernelTooOld (la branche fail-closed n'est atteinte que si une classe a réellement été handled). - sandbox::tests::noop_enforcer_is_unsupported_and_never_errors (renforcé) — NoopSandbox : kind()==Unsupported ; enforceOk(Unsupported) jamais Err, désormais bouclé sur les 3 postures (Allow/Ask/Deny) ET avec un grant présent. (Existants conservés : landlock_write_only_plan_fences_writes_to_the_grant, empty_plan_is_a_noop_enforced, default_enforcer_matches_the_build_target.) ## Réellement tourné vs skippé sur ce kernel - Tout a réellement tourné — 0 skip. Aucune ligne « skipping: Landlock not available » émise (vérifié avec --nocapture | grep -i skip ⇒ « no tests skipped »). Les gardes runtime de skip sont en place (comme SSH/WSL) mais inutiles ici : ce kernel supporte Landlock, l'enforcement est réel (FullyEnforced) — write hors-grant et read hors-grant renvoient bien PermissionDenied. ## Invariant 5 (fail-closed posture Deny) — NON testable sur ce kernel, documenté - La branche Err(KernelTooOld) n'est atteinte que si restrict_self() renvoie RulesetStatus::NotEnforced ET posture==Deny. Sur un kernel Landlock-capable, restrict_self renvoie toujours FullyEnforced/PartiallyEnforced, jamais NotEnforced. Impossible de forcer NotEnforced sans injecter/mocker le crate landlock (l'adapter ne prend pas de dépendance injectable pour le statut). Le pendant « pas d'OS sandbox + non-Deny ⇒ Ok(Unsupported) » est, lui, couvert par NoopSandbox. Recommandation (non bloquante) pour DevBackend si on veut tester cette branche : extraire le mapping (RulesetStatus, Posture) -> Result<SandboxStatus, SandboxError> en fn pure et la tester en table. ## Sortie pass/fail - cargo test -p infrastructure (lib) : 154 passed; 0 failed; 0 ignored. Bloc sandbox : 8 tests, tous ok (write-only, read-only, safety-confinement, empty, empty+Deny, noop×3-postures, default_enforcer). - cargo test -p infrastructure (tous binaires + doc-tests) : 0 failed. - cargo test --workspace : tout vert, 0 failed, aucune erreur de compilation. Aucune violation de la propriété de sûreté n°1. Rien de bloquant à remonter.
  • Prompt: Test bout-en-bout de l'enforcement OS (lot LP4-3, sandbox Landlock), à ajouter et faire passer. Contexte : la chaîne policy → plan → PTY est désormais câblée. DevBackend vient de livrer (non committé) : - composition root : PortablePtyAdapter::new().with_sandbox_enforcer(infrastructure::default_enforcer()) - launch path (crates/application/src/agent/lifecycle.rs, étape 5d) : spec.sandbox = compile_sandbox_plan(effective_permissions.as_ref(), &SandboxContext{ project_root, run_dir }). - l'enforcement effectif vit dans PortablePtyAdapter::spawn (chemin PTY brut), via spawn_command_sandboxed (thread restreint par Landlock puis fork). Objectif : UN test d'intégration de haute fidélité qui prouve que l'enforcement est réellement actif bout-en-bout, SANS lancer de vrai CLI IA (zéro token). Piste recommandée (la plus fidèle, déjà éprouvée par les tests Landlock existants dans crates/infrastructure/src/sandbox/landlock.rs) : passer par PortablePtyAdapter réel avec un enforcer Landlock, un SpawnSpec dont sandbox = Some(plan) où le plan autorise un répertoire et un Deny effectif sur un autre, et une commande shell inoffensive (sh -c 'echo x > /chemin/interdit') ; asserter que l'écriture échoue (fichier absent / exit non nul) alors qu'une écriture dans le chemin autorisé réussit. Skippe proprement (#[cfg(target_os="linux")] + détection de l'ABI Landlock comme le font les tests existants) si le kernel ne supporte pas Landlock, pour ne pas rendre la CI rouge sur un runner sans Landlock. Réutilise au maximum les helpers/patterns des tests Landlock existants (regarde sandbox/landlock.rs tests : read_only_plan_fences_reads_to_the_grant, landlock_write_only_plan_fences_writes_to_the_grant, enforcement_is_confined_to_the_enforcing_thread). Le test doit cibler le chemin PTY (pas structuré). À la fin : exécute le test et donne la sortie réelle. NE committe PAS (figé par l'orchestrateur). Rends via idea_reply : le fichier de test ajouté, ce qu'il prouve, la stratégie de skip, et la sortie brute d'exécution. Si ça échoue, rapport d'erreur clair avec la sortie — on renverra à DevBackend.
  • Response: VERT — test bout-en-bout de l'enforcement OS (lot LP4-3) ajouté et passant, enforcement Landlock réellement actif sur le chemin PTY (pas skippé sur ce kernel). ## Fichier modifié crates/infrastructure/src/pty/mod.rs — nouveau module de test #[cfg(all(test, target_os = "linux"))] mod sandbox_e2e_tests (en plus du module tests existant, intact). 2 tests + helpers. ## Ce que ça prouve Test principal pty_spawn_enforces_sandbox_plan_end_to_end — exerce la chaîne réelle complète, ZÉRO CLI IA / ZÉRO token : PortablePtyAdapter::new().with_sandbox_enforcer(default_enforcer()) (= LandlockSandbox) → SpawnSpec { sandbox: Some(plan) } avec plan = PathGrant{ allowed RW } seul, posture Askadapter.spawn(...) → branche (Some(plan), Some(enforcer))spawn_command_sandboxed (thread restreint par Landlock puis fork/exec). - Commande inoffensive : sh -c "echo outside > <denied>/out.txt; echo inside > <allowed>/in.txt". - Ordre volontaire : l'écriture HORS-grant d'abord, l'écriture DANS le grant ensuite. L'apparition du marqueur allowed/in.txt est donc un signal happens-after prouvant que la tentative hors-grant a déjà eu lieu → assertion déterministe, pas de course avec l'enfant. - Asserts : (1) allowed/in.txt finit par exister (écriture autorisée passe ; reads non gouvernés car seul RW posé → sh/libc chargent normalement) ; (2) denied/out.txt n'existe PAS → l'écriture hors-grant a été bloquée par le kernel = « SANDBOX BREACH » sinon. Test compagnon pty_spawn_without_plan_does_not_sandbox — même adapter+enforcer mais sandbox: None → l'écriture dans le même dossier réussit. Garde-fou anti faux-positif : prouve que le blocage du 1er test vient bien du plan enforcé, pas d'une restriction PTY ambiante ou d'un échec d'écriture sans rapport. ## Stratégie de skip (CI/kernel sans Landlock) - Tout le module est #[cfg(target_os = "linux")] (pas de compilation hors Linux). - Helper landlock_is_enforced() qui sonde l'ABI exactement comme les tests adapter existants : LandlockSandbox::enforce(plan RW) sur un thread jetable (restriction irréversible → jamais sur le thread de test), skip si SandboxStatus::Unsupported. Le test principal return proprement avec message eprintln! si non enforcé → CI verte sur runner sans Landlock LSM. - Helpers réutilisés/calqués sur sandbox/landlock.rs : fresh_dir(tag) (temp dir unique, zéro dép tempfile), enforce sur thread jetable, posture Ask. Attente bornée via wait_for(path, 5s). ## Sortie brute d'exécution $ cargo test -p infrastructure --lib sandbox_e2e_tests -- --nocapture --test-threads=1 Finished `test` profile [unoptimized + debuginfo] target(s) in 1.53s Running unittests src/lib.rs (target/debug/deps/infrastructure-2cc30d2a1793b4e4) running 2 tests test pty::sandbox_e2e_tests::pty_spawn_enforces_sandbox_plan_end_to_end ... ok test pty::sandbox_e2e_tests::pty_spawn_without_plan_does_not_sandbox ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 154 filtered out; finished in 0.05s Preuve que l'enforcement a réellement tourné (et non été skippé) sur ce kernel — les tests adapter Landlock existants ne logguent AUCUN « skipping » : $ cargo test -p infrastructure --lib sandbox::landlock -- --nocapture test sandbox::landlock::tests::read_only_plan_fences_reads_to_the_grant ... ok test sandbox::landlock::tests::landlock_write_only_plan_fences_writes_to_the_grant ... ok test sandbox::landlock::tests::enforcement_is_confined_to_the_enforcing_thread_idea_is_never_sandboxed ... ok test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 151 filtered out; finished in 0.00s Pas de commit (figé par l'orchestrateur, comme demandé). Aucune régression introduite : ajout de tests uniquement, module tests PTY existant inchangé.
  • Prompt: Tests bout-en-bout du lot LP4-4 — enforcement Landlock sur le chemin STRUCTURÉ (sessions Claude/Codex mode JSON). DevBackend a livré (non committé), build + suites existantes vertes. L'Architecte a validé l'approche et défini 7 invariants à couvrir. Objectif : prouver l'enforcement réellement actif bout-en-bout sur le chemin structuré, ZÉRO token (aucun vrai claude/codex — utilise un fake CLI / sh qui émet une ligne JSONL). Chaîne réelle livrée : StructuredSessionFactory::new().with_sandbox_enforcer(default_enforcer()) (composition root) ; AgentSessionFactory::start(.., sandbox: Option<&SandboxPlan>) apparie plan (par-appel) + enforcer (par-instance) ; SpawnLine.sandbox: Option<SandboxPlan> ; run_turn(spec, timeout, enforcer) route vers run_turn_sandboxed/drain_sandboxed (#[cfg(target_os="linux")], thread jetable restreint par enforce() AVANT le spawn std, puis std::process::Command::spawn depuis ce thread → héritage Landlock ; timeout via oneshot killer + tokio::time::timeout ; fail-closed sur Err d'enforce). Pas de pre_exec (forbid(unsafe_code) ; héritage credentials garanti par le noyau). Réutilise les patterns/helpers des tests Landlock existants (crates/infrastructure/src/sandbox/landlock.rs tests + le module sandbox_e2e_tests ajouté dans pty/mod.rs au lot LP4-3) : landlock_is_enforced() (skip propre kernel sans Landlock), enforce sur thread jetable, fresh_dir, attente bornée. LES 7 INVARIANTS À COUVRIR (de l'Architecte) : 1. PARITÉ (test pivot) : fake CLI/sh qui émet une ligne JSONL et tente d'écrire HORS grant (doit être bloqué kernel) et DANS grant (doit réussir). Cible le chemin structuré (run_turn_sandboxed via la factory réelle). 2. COMPANION NÉGATIF : même factory+enforcer mais sandbox==None ⇒ écriture hors-grant RÉUSSIT (prouve que le blocage vient du plan, pas d'une restriction ambiante). 3. FAIL-CLOSED : posture Deny sur kernel sans Landlock ⇒ enforce Err ⇒ run_turn renvoie erreur (Start) et AUCUN child ne tourne (marqueur de sortie absent). 4. NO-OP PAR DÉFAUT : eff==None ⇒ plan None ⇒ chemin async tokio actuel, comportement natif (vérifie que conformance/D0/D3 restent verts sans changement de comportement). 5. CONFINEMENT IRRÉVERSIBILITÉ : deux tours successifs (sessions différentes) avec grants DISJOINTS ⇒ chacun voit son propre périmètre, le tour précédent n'affecte pas le suivant (thread jetable mort). 6. TIMEOUT SOUS SANDBOX : fake CLI qui ne ferme jamais stdout ⇒ run_turn(timeout) tue le child et renvoie Timeout, sans zombie ni thread bloqué. 7. RESUME PRÉSERVÉ : le conversation_id/session_id est toujours capté correctement après un tour sandboxé (la restriction FS ne casse pas le parsing). Tout derrière #[cfg(target_os="linux")] + skip via landlock_is_enforced(). À la fin : exécute et donne la sortie BRUTE. NE committe PAS (figé par l'orchestrateur). Rends via idea_reply : fichier(s) de test ajoutés, mapping test→invariant (les 7), stratégie de skip, et sortie d'exécution réelle. Si un invariant ne passe pas, rapport d'erreur clair avec sortie brute → je renvoie à DevBackend.
  • Response: VERT — Tests bout-en-bout LP4-4 (enforcement Landlock sur le chemin STRUCTURÉ, sessions Claude/Codex JSON) ajoutés et passants. Les 7 invariants de l'Architecte couverts. ZÉRO token (aucun vrai claude/codex : sh qui émet une ligne JSONL + tente des écritures FS, ou le FakeCli scriptable existant). ## Fichiers - Ajouté : crates/infrastructure/src/session/sandbox_e2e.rs — 7 tests + helpers, derrière #[cfg(all(test, target_os = "linux"))]. - Modifié : crates/infrastructure/src/session/mod.rs — déclaration #[cfg(all(test, target_os = "linux"))] mod sandbox_e2e; (uniquement la ligne de module, suites existantes intactes). ## Mapping test → invariant (les 7) 1. PARITÉ (pivot)pty_structured_run_turn_enforces_plan_end_to_end : run_turn + plan RW(allowed) + enforcer Landlock. Asserte : la ligne JSONL est drainée (parsing intact sous sandbox), écriture in-grant réussit, écriture hors-grant bloquée noyau (fichier absent). Exact pendant du pivot PTY LP4-3, mais via process::run_turn_sandboxed. 2. COMPANION NÉGATIFstructured_run_turn_without_plan_does_not_sandbox : même enforcer câblé mais sandbox==None ⇒ écriture hors-grant RÉUSSIT ⇒ le blocage (1) vient du plan, pas d'une restriction ambiante. 3. FAIL-CLOSEDstructured_run_turn_fail_closed_no_child_on_enforce_err : enforcer double AlwaysFailEnforcer (renvoie SandboxError::KernelTooOld, simule fidèlement « Deny + kernel sans Landlock » de façon déterministe, indépendamment du kernel de CI). run_turnAgentSessionError::Start ET le marqueur que le child aurait écrit reste absent (aucun enfant lancé). 4. NO-OP PAR DÉFAUTstructured_run_turn_none_plan_is_native_path : plan==None (même avec enforcer fourni) ⇒ chemin async tokio historique, lignes drainées + écriture arbitraire réussit (aucune restriction). Non-régression confirmée par la suite session:: complète (cf. ci-dessous). 5. CONFINEMENT/IRRÉVERSIBILITÉstructured_two_turns_disjoint_grants_are_confined : deux run_turn successifs à grants DISJOINTS (A puis B). Chacun écrit dans son grant (ok) et dans l'autre (bloqué). Le tour B écrit bien dans B ⇒ la restriction du thread jetable de A n'a pas bavé. 6. TIMEOUT SOUS SANDBOXstructured_run_turn_timeout_under_sandbox : sleep 30 (stdout jamais fermé) via la branche sandboxée, run_turn(timeout 250ms)AgentSessionError::Timeout rapidement (asserté < 10s, pas d'attente des 30s) ⇒ killer oneshot + tokio::time::timeout OK, pas de thread bloqué. 7. RESUME PRÉSERVÉstructured_sandboxed_turn_preserves_conversation_id : via la fabrique réelle StructuredSessionFactory::new().with_sandbox_enforcer(default_enforcer()), profil Claude sur FakeCli (init+result), start(.., Some(&plan)). Sous enforcement actif, send() rend exactement un Final et conversation_id()Some("conv-sbx-1") ⇒ la restriction FS ne casse pas le parsing/la capture d'id. ## Stratégie de skip - Tout le module est #[cfg(all(test, target_os = "linux"))] (rien hors Linux). - Helper landlock_is_enforced() (calqué sur sandbox/landlock.rs et pty::sandbox_e2e_tests) : enforce d'un plan RW sur thread jetable (restriction irréversible ⇒ jamais sur le thread de test), skip propre (eprintln! + return) si SandboxStatus::Unsupported. - Gardés par landlock_is_enforced() (exigent un fencing réel) : invariants 1, 5, 7. Indépendants du kernel (déterministes sans Landlock) : 2 (contrôle natif), 3 (fake enforcer), 4 (plan None), 6 (timeout — branche sandboxée prise sous posture Ask, enforce Ok même sans Landlock). - Helpers réutilisés : fresh_dir, posture Ask, ligne JSONL result réaliste (format Claude vérifié), FakeCli de conformance. ## Sortie brute d'exécution $ cargo test -p infrastructure --lib session::sandbox_e2e -- --nocapture --test-threads=1 Finished `test` profile [unoptimized + debuginfo] target(s) in 5.01s Running unittests src/lib.rs (target/debug/deps/infrastructure-2cc30d2a1793b4e4) running 7 tests test session::sandbox_e2e::pty_structured_run_turn_enforces_plan_end_to_end ... ok test session::sandbox_e2e::structured_run_turn_fail_closed_no_child_on_enforce_err ... ok test session::sandbox_e2e::structured_run_turn_none_plan_is_native_path ... ok test session::sandbox_e2e::structured_run_turn_timeout_under_sandbox ... ok test session::sandbox_e2e::structured_run_turn_without_plan_does_not_sandbox ... ok test session::sandbox_e2e::structured_sandboxed_turn_preserves_conversation_id ... ok test session::sandbox_e2e::structured_two_turns_disjoint_grants_are_confined ... ok test result: ok. 7 passed; 0 failed; 0 ignored; 0 measured; 156 filtered out; finished in 0.27s Aucune ligne « skip » imprimée (run --nocapture) ⇒ les invariants gardés (1, 5, 7) ont réellement posé l'enforcement sur ce kernel (Landlock actif), pas été contournés. Non-régression (invariant 4 : conformance/D0/D3 + voisins) : === session === test result: ok. 56 passed; 0 failed; ... === sandbox === test result: ok. 7 passed; 0 failed; ... === pty === test result: ok. 5 passed; 0 failed; ... Pas de commit (figé par l'orchestrateur, comme demandé). Ajout de tests uniquement + 1 ligne de déclaration de module ; aucun code de prod touché.
  • Prompt: Tests unitaires LS1 — couche DOMAINE de la feature « Gestion des limites de session des agents ». Branche feature/agent-session-limits. DevBackend vient de livrer le code pur (compile, 0 warning). À toi d'écrire les tests unitaires et de les exécuter (cycle §3, étape test). Tests purs, dans les modules concernés (cohérent avec le style de tests existant du crate domain, ex. les #[cfg(test)] mod tests de readiness.rs). Périmètre exact à couvrir (issu du livrable DevBackend) : readiness.rs (ReadinessPolicy::classify) : - ReplyEvent::RateLimited { resets_at_ms }Some(ReadinessSignal::RateLimited { resets_at_ms }) (avec Some(t) ET None) ; - non-régression : Final→TurnEnded ; TextDelta/ToolActivity/Heartbeat→None inchangés ; - ReadinessSignal reste Copy (test de compilation : copie implicite). session_limit.rs (plan_resume, SessionLimit, ResumePlan) : - plan_resume avec resets_at_ms = Some(futur)ResumePlan::Scheduled { fire_at_ms = resets_at, conversation_id transmis } ; - clamp anti-passé : resets_at_ms = Some(passé < now)fire_at_ms == now_ms (jamais dans le passé) ; - resets_at_ms = NoneResumePlan::HumanFallback ; - SessionLimit::has_known_reset true/false selon Some/None ; - conversation_id Some/None correctement propagé dans Scheduled. profile.rs (RateLimitPattern + champ) : - RateLimitPattern::new rejette un pattern vide → DomainError::EmptyField ; - round-trip serde de AgentProfile : clé rateLimitPattern OMISE quand None ; présente et correcte quand Some ; JSON legacy (sans la clé) → désérialise en None (rétro-compat) ; - camelCase respecté sur les champs de RateLimitPattern (resetCapture, timeFormat). events.rs : constructibilité + égalité PartialEq des 5 variantes (AgentRateLimited, AgentResumeScheduled, AgentResumeCancelled, AgentResumed, AgentRateLimitSuspected) avec les bons types de champs. Exécute cargo test -p domain et rends-moi : le rapport complet (nombre de tests, pass/fail), et en cas d'échec un rapport d'erreurs CLAIR (test concerné, attendu vs obtenu, sortie réelle) pour que je le renvoie à DevBackend. Ne modifie PAS le code de production — seulement les tests ; si un test révèle un vrai bug, signale-le sans le corriger toi-même.
  • Response: LS1 — Tests unitaires DOMAINE « limites de session » : TOUT VERT. Commande : cargo test -p domain Résultat lib (cœur testé ici) : 165 passed; 0 failed; 0 ignored. Le reste du crate (tests d'intégration profile/structured_session/window + doctests) reste vert également — aucune régression. Je n'ai touché QUE des tests (aucune ligne de code de prod modifiée). 24 nouveaux tests ajoutés, tous au bon endroit (modules #[cfg(test)] du crate domain), dans le style existant : readiness.rs (5 tests) - rate_limited_with_known_reset_classifies_and_propagates_time — Some(t) propagé tel quel - rate_limited_without_reset_classifies_with_none — None propagé - rate_limited_is_not_classified_as_turn_ended — non terminal - non-régression : Final→TurnEnded déjà couvert + deltas_activities_and_heartbeats_are_non_terminal (TextDelta/ToolActivity/Heartbeat→None) inchangés et verts - readiness_signal_is_copy — test de compilation : copie implicite (move ⇒ ne compilerait plus si le Copy sautait) session_limit.rs (8 tests) - future_reset_schedules_at_reset_time_with_conversation_id — Scheduled{fire_at_ms = resets_at, conversation_id transmis} - past_reset_is_clamped_to_now_never_in_the_past — clamp anti-passé : fire_at_ms == now - reset_exactly_now_fires_at_now — borne - unknown_reset_falls_back_to_human — None ⇒ HumanFallback - conversation_id_{some,none}_is_propagated_into_scheduled — propagation Some/None - has_known_reset_is_true_with_some_and_false_with_none profile.rs (7 tests, dans mcp_tests) - rate_limit_pattern_new_rejects_empty_pattern — DomainError::EmptyField{ field == "rateLimitPattern.pattern" } - rate_limit_pattern_new_accepts_non_empty_pattern - profile_default_has_no_rate_limit_pattern / ..._omits_key_in_json — clé rateLimitPattern OMISE quand None (zéro régression) - legacy_json_without_rate_limit_pattern_deserialises_to_none — rétro-compat - with_rate_limit_pattern_sets_and_round_trips_camel_case — présente+correcte quand Some, camelCase resetCapture/timeFormat vérifié - rate_limit_pattern_omits_unset_optional_fields_in_json — reset_capture/time_format None ⇒ clés omises events.rs (6 tests, nouveau module de test) - constructibilité + égalité PartialEq des 5 variantes : AgentRateLimited, AgentResumeScheduled, AgentResumeCancelled, AgentResumed, AgentRateLimitSuspected (avec cas inégaux pour prouver la discrimination des champs) - distinct_session_limit_variants_are_not_equal — les variantes ne se confondent pas Aucun bug détecté dans le code de prod : tous les invariants du livrable DevBackend sont conformes (clamp max(t, now), omission serde, camelCase, EmptyField, Copy préservé). Feature validée côté tests domaine — prêt pour la suite du cycle (Git : merge éventuel feature/* → develop).
  • Prompt: Tests unitaires LS2 — adapter Claude niveau 1 (infra). Branche feature/agent-session-limits. DevBackend a livré (cargo build -p infrastructure OK, 0 warning). Le code est pur et testable sans process : fonction parse_event et nouvelle fonction pure parse_reset_ms (+ helpers privés) dans crates/infrastructure/src/session/claude.rs. Écris les tests et exécute-les, dans le style des tests existants de ce module (#[cfg(test)] mod tests de claude.rs). ATTENTION particulière : DevBackend a écrit un parseur ISO-8601/RFC3339 À LA MAIN (pas de chrono/time) + une heuristique secondes-vs-ms (seuil 10^12) + un algo jour-civil (days_from_civil). C'est du code délicat : teste-le rigoureusement, y compris les bords. Couverture à assurer : parse_reset_ms (époche-ms en sortie) : - noms de champ : resetsAt, resets_at, reset_at, resetAt, reset — chacun reconnu ; ordre de priorité si plusieurs présents (1er gagne) ; - epoch SECONDES (entier < 10^12) → ×1000 ; epoch MILLISECONDES (≥ 10^12) → tel quel ; le seuil exact (valeur juste sous / juste au-dessus de 10^12) ; - float epoch (secondes et ms) ; - chaîne contenant un entier/float (même heuristique) ; - ISO-8601 ...Z → ms attendus ; ISO avec offset ±hh:mm → converti en ms UTC corrects ; fraction de seconde .fff (tronquée/complétée à 3 chiffres) ; - robustesse : rate_limit_info absent / clé inconnue / valeur non numérique pourrie / chaîne ISO invalide → None (jamais de panique, jamais d'erreur) ; - days_from_civil & bissextiles : 1970-01-01T00:00:00Z → 0 ; une date avec 2024-02-29 (année bissextile) valide ; vérifie au moins une date post-2001 connue (calcule l'epoch-ms attendu à la main ou via une valeur de référence sûre). parse_event (mapping) : - ligne type:"rate_limit_event" avec rate_limit_info.resetsAt → ParsedLine contenant ReplyEvent::RateLimited { resets_at_ms: Some(...) } ; - rate_limit_event sans rate_limit_info exploitable → RateLimited { None } (et PAS Heartbeat) ; - non-régression : system/init → Heartbeat + capture session_id inchangés ; assistant multi-blocs inchangé ; result → Final inchangé ; type inconnu → ignoré ; ligne vide → défaut ; JSON illisible → Decode. Non-terminalité (T4) au niveau adapter : - un enchaînement de lignes rate_limit_event puis result → le flux émis par send (ou la séquence parsée) est [..., RateLimited, Final] : RateLimited n'interrompt PAS, seul Final clôt. Si testable au niveau send avec le fake CLI existant, fais-le ; sinon teste au niveau séquence de parse_event. Exécute cargo test -p infrastructure. Rends : rapport complet (nb tests, pass/fail), et en cas d'échec un rapport CLAIR (test, attendu vs obtenu, sortie réelle) à renvoyer à DevBackend. Ne modifie QUE des tests ; si tu détectes un vrai bug (ex. heuristique de seuil fausse, offset ISO mal converti, bissextile faux), signale-le précisément SANS corriger le code de prod.
  • Response: LS2 — Tests adapter Claude niveau 1 (infra) : VERT, mais ⚠️ j'ai trouvé 2 tests existants qui CASSAIENT au départ (régression de contrat assumée par le livrable §21 — détail plus bas). Commande : cargo test -p infrastructure Résultat lib (cœur testé) : 188 passed; 0 failed; 0 ignored. Tous les fichiers d'intégration + doctests également verts. Aucune régression résiduelle. J'ai ajouté 26 tests LS2 (tous via fonctions pures parse_reset_ms/parse_event, sauf 1 via send()+FakeCli — jamais le vrai claude). Tout est dans crates/infrastructure/src/session/mod.rs (#[cfg(test)] mod tests, là où vivent réellement les tests claude.rs). parse_reset_ms — noms de champ & priorité - recognises_every_field_name : resetsAt / resets_at / reset_at / resetAt / reset chacun reconnu - first_known_key_wins : resetsAt prime sur reset (1er de l'ordre gagne) parse_reset_ms — heuristique secondes/ms + SEUIL - integer_seconds_are_scaled_to_ms (×1000) / integer_millis_are_kept_as_is - threshold_boundary : 10^121 ⇒ secondes (×1000) ; 10^12 pile ⇒ ms (tel quel, borne inclusive côté ms) - float_seconds_preserve_fraction (1_700_000_000.5 ⇒ 1_700_000_000_500) / float_millis_kept_as_is - string_integer_* / string_float_* : même heuristique sur chaînes numériques parse_reset_ms — ISO-8601 / RFC3339 (parseur maison) - iso_utc_z : "2023-11-14T22:13:20Z" ⇒ 1_700_000_000_000 (recoupé contre l'epoch-secondes connu) - iso_positive_offset / iso_negative_offset / iso_compact_offset (+01:00, 01:00, +0100 = même instant UTC) - iso_fraction_padded_and_truncated : .5⇒500, .123456⇒123 (tronqué), .7⇒700 (complété) - robustesse : unknown_key_yields_none, non_numeric_garbage_yields_none (bool/null/array/objet/chaîne pourrie), invalid_iso_string_yields_none days_from_civil & bissextiles - unix_epoch_is_zero : 1970-01-01T00:00:00Z ⇒ 0 - leap_day_2024_02_29 : 2024-02-29T00:00:00Z ⇒ 1_709_164_800_000 (calculé à la main) - known_post_2001_date : 2021-01-01T00:00:00Z ⇒ 1_609_459_200_000 parse_event (mapping) + non-terminalité (T4) - parse_event_rate_limit_with_reset_yields_rate_limited_some : resetsAt exploitable ⇒ RateLimited{Some} - parse_event_rate_limit_without_info_is_rate_limited_none_not_heartbeat : absent / clé inconnue ⇒ RateLimited{None} (et PAS Heartbeat) - parse_event_sequence_rate_limited_then_final_is_not_interrupted : [RateLimited, Final] au niveau parse - send_emits_rate_limited_intercalated_only_final_closes (FakeCli) : flux complet = [Heartbeat, RateLimited, TextDelta, Final], le RateLimited n'interrompt PAS, un seul Final clôt — prouvé bout-en-bout ⚠️ DEUX TESTS EXISTANTS CASSAIENT au lancement initial (avant mes ajouts) — ce n'est PAS un bug de prod, c'est l'ancien contrat qui survivait : 1. claude_parse_rate_limit_event_is_heartbeat (mod.rs) — assertait l'ANCIEN comportement (rate_limit_event ⇒ Heartbeat). Le livrable §21 émet désormais RateLimited. → Je l'ai RENOMMÉ en claude_parse_rate_limit_event_without_reset_is_rate_limited_none et mis à jour l'assertion vers RateLimited{None}. 2. claude_session_respects_port_contract — le harnais de conformité assert_agent_session_contract (conformance.rs, module #[cfg(test)]) n'autorisait avant le Final que TextDelta/ToolActivity/Heartbeat. Le claude_script() contient une ligne rate_limit_event qui produit maintenant un RateLimited intercalé → rejeté. → J'ai ajouté ReplyEvent::RateLimited { .. } à la liste des événements non terminaux autorisés (conforme à §21-T4 : « s'intercale exactement comme un Heartbeat »). Ces deux modifs portent UNIQUEMENT sur du code de test (#[cfg(test)]), pas sur la prod. Je n'ai touché aucune ligne de claude.rs. Le code de prod de DevBackend est conforme au cadrage §21 sur tous les axes testés (noms de champ, priorité, seuil 10^12 exact, floats, offsets ISO signés, fractions ms, bissextile 2024, epoch Unix, robustesse zéro-panic, non-terminalité T4). Feature validée côté tests infra. Prêt pour la suite du cycle.
  • Prompt: Tests LS3 — adapter TokioScheduler (port Scheduler, réveil one-shot annulable), crates/infrastructure/src/scheduler/mod.rs. Branche feature/agent-session-limits. DevBackend a livré (build infra OK, 0 warning). Écris et exécute les tests, dans le style des tests async existants de l'infra (#[tokio::test], cherche des exemples dans le crate pour la convention de runtime). Construction : TokioScheduler::new(tx, clock) où tx est l'émetteur d'un tokio::sync::mpsc::unbounded_channel::<ScheduledTask>() et clock un Arc<dyn Clock> (utilise l'horloge système réelle ou un fake selon ce qui existe déjà ; les délais de test doivent rester COURTS pour ne pas ralentir la suite). Couverture : - arm tire APRÈS l'échéance : arm(now + ~50ms, task)rx.recv() (sous un timeout de sécurité, ex. 1s) rend exactement la ScheduledTask armée ; et vérifie qu'AUCUNE tâche n'arrive AVANT l'échéance (try_recv juste après arm = vide) ; - cancel empêche le tir : arm(now + grand délai, task) puis cancel(id) retourne true → après un court laps, rx reste vide ; - cancel d'un id inconnu → false (ScheduleId::new_random()) ; - cancel APRÈS tir → false (armer délai court, attendre réception via recv, puis cancel) ; - deadline déjà passée (arm(now - 1000, task)) → tir quasi-immédiat (task reçue sous timeout court) ; - (si pertinent et stable) plusieurs arm concurrents → chacun tire / s'annule indépendamment, pas de fuite. Attention aux tests temporels : utilise des timeouts généreux pour la réception (éviter le flaky) mais des délais d'armement courts ; ne te fie pas à des sleeps fixes fragiles pour prouver l'ABSENCE d'un événement — préfère try_recv après un petit yield. Évite tout test dépendant de l'ordonnancement fin qui rendrait la suite instable. Exécute cargo test -p infrastructure (vérifie que les tests LS1/LS2 restent verts aussi). Rends : rapport complet (nb tests, pass/fail), signalement de tout flakiness potentiel, et en cas d'échec un rapport CLAIR (attendu vs obtenu) pour DevBackend. Ne modifie QUE des tests ; signale tout vrai bug (ex. course sur cancel, fuite de handle) sans corriger la prod.
  • Response: LS3 — Tests TokioScheduler (port Scheduler, réveil one-shot annulable) : TOUT VERT, aucun flakiness observé. Commande : cargo test -p infrastructure Résultat lib : 195 passed; 0 failed; 0 ignored (188 LS1/LS2 + 7 nouveaux LS3). Tous les fichiers d'intégration + doctests verts. LS1/LS2 restent verts (aucune régression). Stabilité : suite scheduler relancée 3× d'affilée → 7/7 à chaque fois, ~0,20 s. Pas de flaky détecté. 7 tests ajoutés dans crates/infrastructure/src/scheduler/mod.rs (#[cfg(test)] mod tests, #[tokio::test] runtime current-thread par défaut comme le reste de l'infra). Construction conforme : TokioScheduler::new(tx, clock) avec tx = unbounded_channel::<ScheduledTask>() et clock = Arc<dyn Clock> (= SystemClock réel, horloge partagée pour des échéances cohérentes avec ce qu'arm relit). Délai d'armement court (50 ms), timeout de réception généreux (2 s) pour l'anti-flaky. - arm_fires_after_deadline_with_exact_task : arm(now+50ms) → la tâche EXACTE arrive sous timeout ; et try_recv juste après arm = Empty (rien AVANT l'échéance). - cancel_before_deadline_prevents_fire : arm(now+50ms) + cancel immédiat → true ; attente 4× l'échéance (200 ms) puis try_recv = Empty. Absence PROUVÉE (sans annulation, le délai court aurait tiré bien avant la fin de l'attente — pas un sleep fragile). Bonus : second cancel(id) = false (id retiré de la table). - cancel_unknown_id_is_false : cancel(ScheduleId::new_random())false. - cancel_after_fire_is_false : arm court, on attend la réception (tir effectif), on cède l'exécuteur (8× yield_now pour finaliser l'état du JoinHandle), puis cancelfalse. - past_deadline_fires_immediately : arm(now-1000) → tir quasi-immédiat (reçu sous 500 ms). - multiple_concurrent_arms_all_fire : 3 arms même échéance → les 3 tâches arrivent (comparées en ENSEMBLE trié, zéro dépendance à l'ordre) ; ids armés distincts. - cancel_one_among_many_leaves_others_firing : parmi 3 réveils, on annule le ciblé → seuls les 2 survivants arrivent, l'annulé jamais (try_recv final = Empty). Prouve l'indépendance des handles (pas d'annulation collatérale, pas de fuite). Note d'honnêteté sur le contrat « cancel APRÈS tir → false » : il repose sur JoinHandle::is_finished(). Entre l'envoi tx.send de la tâche de fond et la transition is_finished()==true, il existe une micro-fenêtre. En runtime current-thread (défaut #[tokio::test]), la tâche de fond est nécessairement pollée jusqu'à complétion AVANT que recv() ne rende la valeur, donc is_finished() est vrai au moment du cancel → déterministe. J'ai tout de même ajouté des yield_now() par robustesse. ⚠️ Si la prod était un jour exécutée sous un runtime multi-thread, ce contrat pourrait devenir une course (cancel d'une tâche tout juste tirée mais pas encore jointe → Some(handle) non fini → abort() no-op + retour true au lieu de false). Ce n'est PAS un bug aujourd'hui (le test est vert et stable), mais je le signale comme point de vigilance à surveiller si le drain LS4/LS7 venait à câbler le scheduler sous un runtime multi-thread. Je n'ai touché aucune ligne de prod. Feature LS3 validée côté tests infra. Prêt pour la suite du cycle.
  • Prompt: Tests LS4 — couche application de la feature « limites de session » : SessionLimitService (crates/application/src/agent/session_limit.rs) + réconciliation T4 dans crates/application/src/agent/structured.rs. Branche feature/agent-session-limits. DevBackend a livré (cargo build -p application OK, 0 warning ; tests compilent). Écris et exécute les tests, avec des FAKES des ports (Clock fixe, Scheduler, EventBus, AgentResumer), dans le style des tests applicatifs existants (regarde comment structured.rs et les autres use cases sont testés, réutilise les fakes existants si présents). SessionLimitService — (a) détection→planification : - on_rate_limited(agent, node, conv_id, Some(reset_futur)) → le fake Scheduler reçoit exactement 1 arm(fire_at_ms, ScheduledTask::ResumeAgent{agent_id, node_id, conversation_id}) avec fire_at_ms cohérent (= resets_at, ou now si passé via plan_resume) ; le fake EventBus reçoit AgentRateLimited{agent_id, resets_at_ms} PUIS AgentResumeScheduled{agent_id, fire_at_ms} DANS CET ORDRE ; - on_rate_limited(..., None) → AUCUN arm ; events AgentRateLimited{None} puis AgentRateLimitSuspected{None} ; - dédoublonnage (§21.10-4) : deux on_rate_limited successifs pour le MÊME agent → l'ancien ScheduleId est cancel-é sur le Scheduler (sans émettre d'AgentResumeCancelled pour le dédoublonnage interne). (b) exécution de la reprise : - execute_resume(ScheduledTask::ResumeAgent{...}) → le fake AgentResumer voit resume(agent_id, node_id, conversation_id, RESUME_PROMPT) (vérifie que le prompt passé == la const RESUME_PROMPT) ; EventBus reçoit AgentResumed{agent_id} ; l'entrée interne est retirée (un cancel_resume ultérieur → false) ; - AgentResumer qui retourne Err → l'erreur est propagée ET AgentResumed n'est PAS publié. (c) annulation : - cancel_resume(agent) après un on_rate_limited armé, fake Scheduler renvoyant true → retourne true, EventBus reçoit AgentResumeCancelled{agent_id} ; - cancel_resume(agent) sans armement préalable → false, aucun event ; - fake Scheduler renvoyant false (cas « déjà tiré ») → cancel_resume renvoie false ET n'émet PAS AgentResumeCancelled (la reprise suit son cours) — c'est le contrat anti-course important. Réconciliation T4 (drain_with_readiness_outcome / drain_to_final) : - flux [RateLimited{Some(t)}] sans Final → Ok(TurnOutcome::RateLimited{Some(t)}) (fin gracieuse, PAS d'Err) ; - flux [RateLimited{None}] sans Final → Ok(TurnOutcome::RateLimited{None}) ; - flux [..., RateLimited, Final]Ok(TurnOutcome::Completed(contenu)) (Final l'emporte) ; - flux [TextDelta] seul (ni Final ni RateLimited) → Err(Io) INCHANGÉ (non-régression critique : un vrai flux tronqué reste une erreur) ; - NON-RÉGRESSION : send_blocking et drain_with_readiness (signatures historiques) gardent leur comportement (RateLimited→Err(Io) côté send_blocking ; flux sans Final → Io). Vérifie que les tests existants (drain_with_readiness_lot1, send_blocking_d1, drain_marks_alive) restent verts. Exécute cargo test -p application. Rends : rapport complet (nb tests, pass/fail), confirmation explicite que les tests applicatifs préexistants restent verts (zéro régression), et en cas d'échec un rapport CLAIR (attendu vs obtenu) pour DevBackend. Ne modifie QUE des tests ; signale tout vrai bug sans corriger la prod.
  • Response: LS4 — Tests couche application « limites de session » : TOUT VERT, zéro régression. Commande : cargo test -p application Résultat : tous les binaires de test verts, 0 failed sur l'ensemble du crate. 16 nouveaux tests LS4 ajoutés, répartis en 2 fichiers d'intégration (style des tests applicatifs existants, 100 % fakes des ports). Confirmation explicite NON-RÉGRESSION : les suites préexistantes citées restent vertes — - drain_with_readiness_lot1.rs : 7/7 ✓ - send_blocking_d1.rs : 9/9 ✓ - (et tout le reste du crate : project/agent_lifecycle/orchestrator/structured_launch/etc., 0 échec). === Nouveau fichier tests/session_limit_service.rs (9 tests) — fakes Clock fixe / Scheduler enregistreur-contrôlable / EventBus espion / AgentResumer espion-contrôlable === (a) détection→planification : - on_rate_limited_future_arms_and_emits_in_order : Some(reset futur) ⇒ EXACTEMENT 1 arm(fire_at_ms==reset, ResumeAgent{agent,node,conv}) + events AgentRateLimited PUIS AgentResumeScheduled dans cet ordre. - on_rate_limited_past_reset_clamps_fire_at_to_now : reset passé ⇒ fire_at_ms==now (clamp anti-passé) ; l'event RateLimited garde l'heure brute passée, ResumeScheduled porte le now clampé. - on_rate_limited_without_reset_is_human_fallback_no_arm : None ⇒ AUCUN arm + events AgentRateLimited{None} puis AgentRateLimitSuspected{None}. - on_rate_limited_twice_same_agent_dedups_cancelling_previous : 2 signaux même agent ⇒ l'ancien ScheduleId est cancel-é avant réarmement, et AUCUN AgentResumeCancelled émis (dédoublonnage interne silencieux, §21.10-4). (b) exécution : - execute_resume_calls_resumer_with_prompt_and_emits_resumed : resume(agent,node,conv, prompt==RESUME_PROMPT vérifié) + AgentResumed publié + entrée retirée (cancel_resume ultérieur ⇒ false). - execute_resume_propagates_error_without_emitting_resumed : Resumer Err ⇒ erreur propagée ET AgentResumed NON publié. (c) annulation : - cancel_resume_after_arm_returns_true_and_emits_cancelled : cancel renvoyant true ⇒ true + bon ScheduleId passé + AgentResumeCancelled publié. - cancel_resume_without_arm_is_false_no_event : sans armement ⇒ false, Scheduler non sollicité, aucun event. - cancel_resume_when_scheduler_already_fired_is_false_no_event : Scheduler renvoyant false (« déjà tiré ») ⇒ false ET PAS d'AgentResumeCancelled (contrat anti-course important). === Nouveau fichier tests/session_limit_t4.rs (7 tests) — réconciliation §21.2-T4, fakes AgentSession + InputMediator === - outcome_rate_limited_some_without_final_is_graceful : [RateLimited{Some(t)}]Ok(TurnOutcome::RateLimited{Some(t)}) (PAS d'Err) + vérifie qu'un RateLimited ne marque PAS Idle. - outcome_rate_limited_none_without_final_is_graceful : [RateLimited{None}]Ok(RateLimited{None}). - outcome_rate_limited_then_final_is_completed : [.., RateLimited, Final]Completed(contenu) (Final l'emporte) + Idle marqué. - outcome_truncated_stream_without_final_or_ratelimit_is_io_error : [TextDelta] seul ⇒ Err(Io) INCHANGÉ (non-régression critique du flux tronqué). - non-régression signatures historiques : drain_with_readiness_rate_limited_is_io_error, send_blocking_rate_limited_is_io_error (limite ⇒ Io), drain_with_readiness_nominal_still_completes (cas nominal Final intact). Aucun bug détecté : le code DevBackend est conforme au cadrage §21.5 sur tous les axes (ordre des events, clamp via plan_resume, dédoublonnage silencieux, prompt constant, propagation d'erreur sans event, contrat anti-course du cancel). Je n'ai modifié AUCUNE ligne de prod (uniquement ajouté 2 fichiers de tests). Feature LS4 validée côté tests application. Prêt pour la suite du cycle (Git : merge éventuel).
  • Prompt: Tests LS5 — parser regex niveau 2 (infra) : RateLimitParser (crates/infrastructure/src/ratelimit/mod.rs) + le module d'heure partagé timeparse (crates/infrastructure/src/timeparse.rs). Branche feature/agent-session-limits. DevBackend a livré (cargo build -p infrastructure OK, 0 warning ; tests LS2 toujours verts). Tout est pur (now_ms injecté), testable sans I/O. Écris et exécute les tests dans le style existant. RateLimitParser (new + detect + applies) : - new sur regex INVALIDE → None (jamais de panique) ; - pattern qui matche SANS reset_capture → Some(SessionLimit{ resets_at_ms: None, source: Pattern, detected_at_ms == now_ms }) ; - capture nommée (?P<reset>...) + time_format ABSOLU : epoch_s (secondes→×1000), epoch_ms (tel quel), iso8601/rfc3339 (...Z → ms attendus) → resets_at_ms corrects ; - time_format RELATIF (relative_s, capture « 600 », now=T) → Some(resets_at_ms == T + 600_000) ; - time_format MURAL (wall, capture « 3pm ») : now correspondant à 10h du jour → 15h AUJOURD'HUI (même jour UTC) ; now correspondant à 16h → 15h DEMAIN (passage de minuit, +24h). Choisis des now_ms calculés proprement (epoch connu) et calcule l'attendu à la main ; - pattern NE matche PAS → detect → None ; - pattern matche mais capture absente/valeur pourrie/non parsable → Some(SessionLimit{ resets_at_ms: None }) (détection utile sans heure) ; - vérifie que source == RateLimitSource::Pattern dans tous les cas détectés ; - compilation du regex faite une seule fois (à new) — au minimum vérifie que detect peut être appelé plusieurs fois sans souci. applies(profile) : - profil avec structured_adapter (structuré) → false (même s'il a un rate_limit_pattern) ; - profil PTY (sans structured_adapter) AVEC rate_limit_pattern → true ; - profil PTY SANS rate_limit_pattern → false. timeparse (fonctions réexportées) : - days_from_civil : 1970-01-01 → 0 ; une année bissextile (2024-02-29) cohérente ; - parse_wall_clock : « 3pm » → (15,0,0) ; « 15:00:30 » → (15,0,30) ; format invalide → None ; - wall_clock_to_ms : même jour vs lendemain selon now ; - parse_absolute_ms : epoch s/ms/ISO cohérents (recoupe avec LS2 si utile). - NON-RÉGRESSION : confirme que les tests LS2 (parse_reset_ms/parse_event dans session/mod.rs) restent verts après le refactor qui a déplacé les helpers vers timeparse. Exécute cargo test -p infrastructure. Rends : rapport complet (nb tests, pass/fail), confirmation explicite que LS2/LS3 restent verts, et en cas d'échec rapport CLAIR (attendu vs obtenu) pour DevBackend. EN PLUS (petit nettoyage que DevBackend t'a signalé) : tes tests scheduler LS3 (crates/infrastructure/src/scheduler/mod.rs, ~lignes 253/285) déclenchent 2 warnings irrefutable if let car ScheduledTask est mono-variante aujourd'hui. Corrige ces 2 warnings dans le code de TEST (ex. déstructuration directe au lieu de if let) pour garder la suite sans warning — c'est du test, tu peux le modifier. Ne touche à aucun code de prod ; signale tout vrai bug sans le corriger.
  • Response: LS5 — Tests parser regex niveau 2 (RateLimitParser) + module timeparse : TOUT VERT, 0 warning, zéro régression. Commande : cargo test -p infrastructure Résultat lib : 221 passed; 0 failed; 0 ignored (195 antérieurs + 26 nouveaux LS5). Tous les fichiers d'intégration + doctests verts. Build de test : 0 warning (vérifié --no-run | grep -c warning = 0). Confirmation explicite NON-RÉGRESSION : - LS2 (session::tests::parse_reset_ms* / parse_event* / send_emits_rate*) : tous verts APRÈS le refactor qui a déplacé les helpers vers timeparse ✓ - LS3 (scheduler::tests::*) : 7/7 verts ✓ (+ les 2 warnings irrefutable if let corrigés — voir plus bas) === ratelimit/mod.rs — 15 tests (in-file #[cfg(test)] mod tests) === new + detect : - new_returns_none_on_invalid_regex : regex invalide "rate limit (" ⇒ None (jamais de panique). - detect_returns_none_when_pattern_does_not_match : pas de match ⇒ None. - detect_match_without_reset_capture_has_no_time : match sans reset_capture ⇒ SessionLimit{resets_at_ms:None, source:Pattern, detected_at_ms==now}. - formats ABSOLUS : detect_epoch_seconds_format (×1000), detect_epoch_millis_format (tel quel), detect_iso8601_format (2023-11-14T22:13:20Z→1_700_000_000_000). - format RELATIF : detect_relative_seconds_format_uses_now (capture « 600 », now=T ⇒ T+600_000). - format MURAL (passage de minuit, math calculée à la main sur DAY_START=1_699_920_000_000 = 2023-11-14T00:00Z) : detect_wall_clock_same_day_when_future (now=10h, « 3pm » ⇒ 15h même jour) ; detect_wall_clock_next_day_when_past (now=16h ⇒ 15h DEMAIN, +24h). - capture inexploitable ⇒ détection sans heure : detect_match_with_missing_capture_group_has_no_time, detect_match_with_unparsable_value_has_no_time (⇒ resets_at_ms:None). - detect_can_be_called_multiple_times : regex compilé une seule fois, detect appelé 3× sans souci. - source == RateLimitSource::Pattern asserté dans tous les cas détectés. applies(profile) : - applies_false_for_structured_profile_even_with_pattern (structuré ⇒ false, niveau 1 prend la main) ; - applies_true_for_pty_profile_with_pattern (PTY + pattern ⇒ true) ; - applies_false_for_pty_profile_without_pattern (PTY sans pattern ⇒ false). === timeparse.rs — 11 tests === - days_from_civil_epoch_is_zero (1970-01-01→0) ; days_from_civil_leap_day_2024 (2024-02-29→19782, + cohérence avec le 1ᵉʳ mars). - parse_wall_clock_variants (« 3pm »→(15,0,0), « 15:00:30 »→(15,0,30), « 3:30 pm »→(15,30,0), 12am→0h, 12pm→12h) ; parse_wall_clock_rejects_invalid (texte, 25:00, 10:75, trop de composantes ⇒ None). - wall_clock_to_ms_* : même jour / lendemain / cas-limite « cible == now » (⇒ lendemain, jamais le présent). - parse_absolute_ms_* : epoch s/ms, ISO8601, garbage→None (recoupe LS2). - parse_rfc3339_offsets_convert_to_utc : +01:00 et 01:00 ⇒ même instant UTC. === Nettoyage LS3 demandé === Les 2 warnings irrefutable if let (scheduler/mod.rs ~253/285, ScheduledTask mono-variante) sont corrigés : remplacés par une déstructuration directe let ScheduledTask::ResumeAgent { conversation_id, .. } = task; (code de TEST uniquement). Suite désormais 0 warning. Aucun bug détecté : le code DevBackend est conforme au cadrage §21 niveau 2 sur tous les axes (robustesse regex, stratégies de format absolu/relatif/mural, passage de minuit UTC, règle anti-double-détection applies, factorisation timeparse sans casser LS2). Je n'ai modifié AUCUN code de prod (uniquement ajouté des tests + corrigé mes propres tests LS3). Feature LS5 validée côté tests infra. Prêt pour la suite du cycle.
  • Prompt: Tâche QA — tests unitaires de la slice LS7-front (UI limites de session des agents), branche feature/agent-session-limits. DevFrontend vient d'implémenter (typecheck tsc --noEmit propre, 39 tests agents existants toujours verts, non commité). Périmètre à couvrir (frontend/) : 1. src/features/agents/useAgents.ts — nouvel état limitByAgent: Record<string, AgentLimitState> ({ limitedUntil?, resumeFireAt?, suspected? }) peuplé depuis 5 DOMAIN_EVENT dans l'abonnement onDomainEvent. À tester (via le mock gateway qui émet des events) : - agentRateLimited {agentId, resetsAtMs} → entrée { limitedUntil: resetsAtMs, suspected: false }. - agentResumeScheduled {agentId, fireAtMs} → arme resumeFireAt. - agentResumeCancelled {agentId} → retire resumeFireAt, reste limité. - agentResumed {agentId} → supprime l'entrée (état effacé). - agentRateLimitSuspected {agentId, resetsAtMs?} → { ..., suspected: true }, y compris le cas SANS resetsAtMs (heure inconnue). - séquence réaliste : rateLimited → resumeScheduled → cancelResume (action) → vérifier retrait optimiste + appel input.cancelResume (mock cancelledResumes / cancelResumeResult). - Action cancelResume(agentId) exposée par le hook : retrait optimiste + verdict backend renvoyé (teste les deux verdicts via cancelResumeResult). 2. src/features/agents/AgentLimitBadge.tsx — helpers purs exportés formatResetTime(epochMs) (→ HH:MM) et formatCountdown(remainingMs) (→ « Xm Ys »/« Ys », clampé à 0 pour négatif). Teste ces helpers purs directement (cas limites : 0, négatif, < 60 s, > 60 s, minuit). Teste le rendu du badge (limité jusqu'à HH:MM vs « limité » sans heure vs « heure inconnue » pour suspected sans resetsAtMs) et que le bouton « Annuler la reprise » appelle l'action quand resumeFireAt est armé. 3. Adapter mock src/adapters/mock/index.tsMockInputGateway.cancelResume (cancelledResumes, cancelResumeResult) : couvre via les tests du hook. Suis le style des tests existants (cf. singletonAgent.test.tsx, mock.test.ts, les tests agents). Exécute la suite (npm test ou l'outil du repo) et RENDS-MOI via idea_reply : la commande exacte lancée, le résultat réel (nb tests passés/échoués), et le rapport d'erreurs complet si rouge. Si un test révèle un bug d'implémentation, décris-le précisément (fichier:ligne, attendu vs obtenu) — je relaierai à DevFrontend pour correction. Ne commit rien.
  • Response: QA LS7-front — VERT. Aucun bug d'implémentation trouvé. Rien commité. ## Fichiers de test ajoutés (2) - frontend/src/features/agents/useAgentsLimits.test.tsx (13 tests) — état limitByAgent du hook via le MockSystemGateway qui émet les events + MockInputGateway. - frontend/src/features/agents/AgentLimitBadge.test.tsx (11 tests) — helpers purs + rendu du badge. ## Commandes exactes lancées (cwd = frontend/) 1. npx vitest run src/features/agents/useAgentsLimits.test.tsx src/features/agents/AgentLimitBadge.test.tsxTest Files 2 passed (2) · Tests 24 passed (24) 2. npx vitest run src/features/agents/ (suite agents complète, régression) → Test Files 4 passed (4) · Tests 63 passed (63) (39 existants + 24 nouveaux, 0 régression) ## Couverture livrée useAgents (§21), tous via events réels : - agentRateLimited{resetsAtMs}{limitedUntil, resumeFireAt:undefined, suspected:false} ✓ - agentResumeScheduled{fireAtMs} → arme resumeFireAt par-dessus l'état limité ✓ - agentResumeCancelled → retire resumeFireAt, reste limité ✓ ; + no-op sur agent inconnu (aucune entrée créée) ✓ - agentResumed → entrée supprimée (agentId in map === false) ✓ - agentRateLimitSuspected AVEC et SANS resetsAtMssuspected:true, limitedUntil undefined dans le cas sans heure ✓ - séquence réaliste rateLimited→scheduled→cancelResume(action) : retrait optimiste du countdown + agent toujours limité + input.cancelledResumes === [agentId] + verdict ✓ - action cancelResume : verdict true ET false (via cancelResumeResult), + appel du port même sans reprise armée ✓ - deux agents foldés indépendamment ✓ AgentLimitBadge : - formatCountdown : 0→"0s", négatif→"0s" (clamp), <60s→"Xs", ceil partiel (4.2s→"5s"), ≥60s→"Xm Ys" (60000→"1m 0s", 125000→"2m 5s") ✓ - formatResetTime : HH:MM sans secondes (même minute +30s = même libellé), minuit stable ✓ - rendu : "limité jusqu'à HH:MM" (heure connue) / "limité" (sans heure) / note "heure inconnue — reprise à préciser" (suspected sans heure) + absence de la note quand suspected A une heure ✓ - bouton "Annuler la reprise" : appelle onCancelResume au clic quand resumeFireAt armé, countdown rendu, bouton désactivé si busy ✓ MockInputGateway.cancelResume (cancelledResumes/cancelResumeResult) : couvert via les tests du hook. ## Note hors-périmètre (PRÉEXISTANTE, pas causée par LS7-front) src/adapters/mock/mock.test.ts:16 échoue (1 test) : il assert « thirteen gateways » mais le permission gateway porte le compte à 14. Vérifié : la gateway permission existe déjà dans HEAD committé (eca2ba9) — l'assertion de comptage est simplement périmée, sans rapport avec les limites de session. Mes deux fichiers ne touchent pas ce test. Aucune action de ma part (hors scope) ; à relayer pour une mise à jour du compteur si tu veux la suite adapters/mock 100% verte.
  • Prompt: Tâche QA — tests unitaires LS8-backend (filet humain niveau 3), branche feature/agent-session-limits. DevBackend a implémenté (build 0 erreur/0 warning, suites domain/application/app-tauri déjà vertes, non commité). À COUVRIR — nouvelle méthode SessionLimitService::confirm_human_resume(agent_id, node_id, conversation_id, resets_at_ms: i64) dans crates/application/src/agent/session_limit.rs. Teste via les fakes existants (Clock/Scheduler/EventBus) du fichier crates/application/tests/session_limit_service.rs (suis ce style). Cas (cadrage Architect) : (a) heure FUTURE ⇒ publie AgentRateLimited{Some(resets_at_ms)} puis AgentResumeScheduled{fire_at_ms == resets_at_ms} + un ScheduleId armé sur le scheduler. (b) heure PASSÉE (resets_at_ms < now) ⇒ clamp : fire_at_ms == now (reprise quasi-immédiate). Vérifie l'événement AgentResumeScheduled avec fire_at_ms == now. (c) DÉDOUBLONNAGE croisé : confirm_human_resume APRÈS un on_rate_limited déjà armé pour le même agent ⇒ le précédent réveil est désarmé, une seule entrée armée (et l'inverse : on_rate_limited après confirm_human_resume). Vérifie qu'il n'y a qu'un seul armement actif. (d) ANNULABILITÉ : cancel_resume(agent_id) après confirm_human_resume ⇒ publie AgentResumeCancelled et renvoie true (l'armement humain s'annule par la même voie). (e) PARITÉ auto/humain : même séquence d'événements qu'on_rate_limited cas Scheduled (à reset identique). À COUVRIR — commande set_resume_at (app-tauri) si testable au niveau intégration comme session_limit_wiring.rs : au minimum le cas node_for_agent == None ⇒ erreur NOT_FOUND (pas d'armement orphelin). Si le câblage rend ce test trop lourd (besoin d'une cellule vivante), note-le et concentre-toi sur la couverture application (ae) qui est l'essentiel. Exécute (cargo test -p application + -p app-tauri si pertinent). RENDS-MOI via idea_reply : commande exacte, résultat réel (passés/échoués), rapport d'erreurs complet si rouge. Si un test révèle un bug (notamment sur le dédoublonnage croisé ou le clamp passé), décris-le précisément (attendu vs obtenu) — je relaierai à DevBackend. Ne commit rien.
  • Response: QA LS8-backend (filet humain niveau 3) — VERT. Aucun bug d'implémentation trouvé (ni sur le dédoublonnage croisé, ni sur le clamp passé). Rien commité. ## Fichiers de test modifiés (2 — ajouts only, fakes existants réutilisés) - crates/application/tests/session_limit_service.rs : +6 tests confirm_human_resume (style des fakes Clock/Scheduler/EventBus existants). - crates/app-tauri/tests/session_limit_wiring.rs : +2 tests (précondition NOT_FOUND + parité runtime humaine sur le vrai bus). ## Commandes exactes lancées + résultats réels 1. cargo test -p application --test session_limit_service15 passed; 0 failed (10 existants + 5 nouveaux). 2. cargo test -p app-tauri --test session_limit_wiring4 passed; 0 failed (2 existants + 2 nouveaux). 3. cargo test -p application (régression complète) → toutes suites vertes, 0 failed. 4. cargo test -p app-tauri (régression complète) → toutes suites vertes, 0 failed. ## Couverture application (ae) — l'essentiel - (a) heure FUTURE confirm_human_resume_future_arms_and_emits_in_order : exactement 1 arm(fire_at_ms==resets_at_ms, ResumeAgent{agent,node,conv}), 1 ScheduleId émis, events AgentRateLimited{Some(reset)} PUIS AgentResumeScheduled{fire_at_ms==reset} dans cet ordre. ✓ - (b) heure PASSÉE confirm_human_resume_past_reset_clamps_fire_at_to_now : fire_at_ms == now (clamp anti-passé) ; AgentRateLimited garde l'heure brute passée, AgentResumeScheduled porte le now clampé. ✓ - (c) dédoublonnage CROISÉ (les deux sens) : - confirm_human_resume_after_auto_dedups_single_active_arm : confirm APRÈS on_rate_limited → l'ancien ScheduleId auto est cancel-é (cancels == [issued[0]]), pas d'AgentResumeCancelled (silencieux). Unicité prouvée : 1er cancel_resume→true, 2e→false. ✓ - auto_after_confirm_human_resume_dedups_single_active_arm : on_rate_limited APRÈS confirm → symétrique, même invariant d'unicité. ✓ - (d) ANNULABILITÉ cancel_resume_after_confirm_human_resume_returns_true_and_emits_cancelled : renvoie true + publie AgentResumeCancelled (même voie que l'auto). ✓ - (e) PARITÉ auto/humain confirm_human_resume_is_event_for_event_identical_to_auto_scheduled : à reset futur identique, bus.events() ET scheduler.armed() strictement égaux entre confirm_human_resume et on_rate_limited. La source Human vs Structured n'a aucun effet observable. ✓ ## Couverture app-tauri set_resume_at - NOT_FOUND set_resume_at_resolves_no_cell_for_an_agent_without_a_live_session : ✓ couvert au niveau précondition. NOTE : la commande #[tauri::command] set_resume_at exige State<AppState>, non constructible hors runtime Tauri — le harnais wiring existant n'invoque jamais les commandes, seulement le service. J'ai donc testé la précondition exacte qui produit le NOT_FOUND : sur un AppState::build neuf, structured_sessions.node_for_agent(unknown) ET terminal_sessions.node_for_agent(unknown) renvoient None → la branche ok_or_else(NotFound) (commands.rs:1420-1428) est prise → aucun armement orphelin. C'est la couverture maximale réaliste sans faire tourner Tauri. - Parité runtime confirm_human_resume_arms_a_cancellable_resume_over_the_real_bus : sur le vrai TokioBroadcastEventBus, confirm_human_resume publie AgentRateLimited puis AgentResumeScheduled et l'armement est annulable (cancel_resume→true), exactement comme la branche auto déjà testée. ✓ ## Observation (non bloquante, pas un bug) confirm_human_resume est total et défensif : le cas ResumePlan::HumanFallback y est inatteignable (resets_at_ms toujours Some) → traité en no-op via if let Scheduled. Conforme au cadrage Architect ; pas de test dédié (branche morte par construction). Build app-tauri OK (0 erreur). Rien n'a été commité.
  • Prompt: Tâche QA — validation LS8-front (formulaire de saisie d'heure, filet humain niveau 3) sur feature/agent-session-limits. DevFrontend a implémenté ET ajusté/ajouté des tests (typecheck propre, npx vitest run src/features/agents/ src/adapters/mock/ = 109 tests verts). Ton rôle : valider la couverture, exécuter, et signaler tout trou ou bug. Points à vérifier/exécuter : 1. Helper pur timeInputToEpochMs(value, now) (AgentLimitBadge.tsx) : cas couverts ? « HH:MM » valide → aujourd'hui à cette heure ; instant passé dans la journée → epoch < now (le backend clampe, pas le front) ; malformé/vide/25:00/12:60null. Ajoute les cas manquants si besoin. 2. Soumission du formulaire (état suspected SANS heure) ⇒ onSetResumeAt / action hook setResumeAt appelée avec l'epoch attendu ; bouton désactivé si saisie vide ou busy ; saisie malformée (null) ne déclenche PAS d'appel. 3. Action hook useAgents.setResumeAt(agentId, resetsAtMs) : délègue bien au port input.setResumeAt, aucune mutation optimiste (vérifie que l'état ne bascule QUE sur réception de agentResumeScheduled, pas avant). Mock instrumenté resumeArmings. 4. Bascule complète : agent suspected sans heure → submit form → émettre agentResumeScheduled (via mock) → le badge passe à l'état nominal (countdown + bouton Annuler), le formulaire disparaît. 5. Non-régression : tout src/features/agents/ + src/adapters/mock/ vert. Confirme le compteur gateways corrigé (14) dans mock.test.ts. Exécute npx vitest run src/features/agents/ src/adapters/mock/ (+ tsc --noEmit si tu veux). RENDS-MOI via idea_reply : commande exacte, résultat réel (passés/échoués), trous comblés (fichiers/tests ajoutés), et tout bug trouvé (attendu vs obtenu, fichier:ligne) — je relaierai à DevFrontend. Ne commit rien.