//! Integration test for the **session-limit** wiring in the composition root //! (ARCHITECTURE §21, LS7). //! //! Proves that [`AppState`] actually *constructs and wires* the //! `SessionLimitService` (the gap LS7 closes: the LS1–LS4 domain/application //! machinery existed but was never instantiated at runtime). The detect→plan→ //! resume→cancel logic itself is covered by the application/infrastructure unit //! tests (LS3/LS4); here we assert the **composition** the commands rely on: //! the service is present, `on_rate_limited` arms a cancellable resume that emits //! the domain events on the real bus, and `cancel_resume` is a clean no-op for an //! unknown agent (never a panic). use std::path::PathBuf; use std::time::Duration; use app_tauri_lib::state::AppState; use domain::events::DomainEvent; use domain::ids::NodeId; use domain::AgentId; use infrastructure::UuidGenerator; use domain::ports::IdGenerator; fn temp_path(tag: &str) -> PathBuf { let ids = UuidGenerator::new(); std::env::temp_dir().join(format!("idea-sl-test-{tag}-{}", ids.new_uuid())) } fn agent(n: u128) -> AgentId { AgentId::from_uuid(uuid::Uuid::from_u128(n)) } fn node(n: u128) -> NodeId { NodeId::from_uuid(uuid::Uuid::from_u128(n)) } /// `cancel_resume` on an agent with no armed resume returns `false` (and never /// panics): the service is wired and behaves as a clean no-op. #[tokio::test] async fn cancel_resume_is_a_clean_noop_for_unknown_agent() { let state = AppState::build(temp_path("appdata")); assert!( !state.session_limit_service.cancel_resume(agent(1)), "no armed resume ⇒ cancel returns false" ); } /// A structured rate-limit signal with a **future** reset arms a cancellable /// resume: the wired service publishes `AgentRateLimited` then /// `AgentResumeScheduled` on the **real** event bus, and `cancel_resume` then /// publishes `AgentResumeCancelled` and returns `true`. #[tokio::test] async fn on_rate_limited_arms_a_cancellable_resume_over_the_real_bus() { let state = AppState::build(temp_path("appdata")); let mut rx = state.event_bus.raw_receiver(); // Reset far in the future so the scheduler does NOT fire before we cancel. let resets_at_ms = i64::MAX; state .session_limit_service .on_rate_limited(agent(7), node(70), None, Some(resets_at_ms)); // The two arming events, in order, on the bus. let mut saw_rate_limited = false; let mut saw_scheduled = false; for _ in 0..32 { match tokio::time::timeout(Duration::from_secs(2), rx.recv()).await { Ok(Ok(DomainEvent::AgentRateLimited { agent_id, .. })) if agent_id == agent(7) => { saw_rate_limited = true; } Ok(Ok(DomainEvent::AgentResumeScheduled { agent_id, .. })) if agent_id == agent(7) => { saw_scheduled = true; break; } Ok(Ok(_)) => continue, _ => break, } } assert!(saw_rate_limited, "AgentRateLimited relayed on the bus"); assert!(saw_scheduled, "AgentResumeScheduled relayed on the bus"); // The window is cancellable: cancel succeeds and emits AgentResumeCancelled. assert!( state.session_limit_service.cancel_resume(agent(7)), "an armed resume is cancellable" ); let mut saw_cancelled = false; for _ in 0..32 { match tokio::time::timeout(Duration::from_secs(2), rx.recv()).await { Ok(Ok(DomainEvent::AgentResumeCancelled { agent_id })) if agent_id == agent(7) => { saw_cancelled = true; break; } Ok(Ok(_)) => continue, _ => break, } } assert!(saw_cancelled, "AgentResumeCancelled relayed on the bus"); } /// `set_resume_at` (filet humain niveau 3) résout l'agent→cellule dans les registries /// des sessions vivantes ; **sans cellule vivante**, la résolution échoue ⇒ la commande /// renvoie `NOT_FOUND` sans armer quoi que ce soit (pas d'armement orphelin). /// /// La commande `#[tauri::command]` exige une `State` non constructible hors /// runtime Tauri ; on couvre donc ici sa **précondition exacte** : sur un `AppState` /// neuf (aucune session vivante), aucune registry ne résout l'agent — c'est précisément /// le `None` qui produit le `NOT_FOUND` dans `set_resume_at` (cf. commands.rs:1420-1428). #[tokio::test] async fn set_resume_at_resolves_no_cell_for_an_agent_without_a_live_session() { let state = AppState::build(temp_path("appdata")); let unknown = agent(99); assert!( state.structured_sessions.node_for_agent(&unknown).is_none(), "aucune session structurée vivante ⇒ pas de cellule" ); assert!( state.terminal_sessions.node_for_agent(&unknown).is_none(), "aucune session terminal vivante ⇒ pas de cellule" ); // Conjonction = la branche `ok_or_else(NotFound)` de set_resume_at est prise : // la saisie d'heure n'a pas de cible ⇒ aucun armement orphelin. let resolved = state .structured_sessions .node_for_agent(&unknown) .or_else(|| state.terminal_sessions.node_for_agent(&unknown)); assert!(resolved.is_none(), "set_resume_at ⇒ NOT_FOUND (aucun armement orphelin)"); } /// Parité runtime du filet humain : `confirm_human_resume` (la délégation de /// `set_resume_at`) arme une reprise annulable sur le **vrai** bus — `AgentRateLimited` /// puis `AgentResumeScheduled` — exactement comme la branche auto `on_rate_limited`. #[tokio::test] async fn confirm_human_resume_arms_a_cancellable_resume_over_the_real_bus() { let state = AppState::build(temp_path("appdata")); let mut rx = state.event_bus.raw_receiver(); // Reset très loin dans le futur : le scheduler ne tire pas avant l'annulation. let resets_at_ms = i64::MAX; state .session_limit_service .confirm_human_resume(agent(8), node(80), None, resets_at_ms); let mut saw_rate_limited = false; let mut saw_scheduled = false; for _ in 0..32 { match tokio::time::timeout(Duration::from_secs(2), rx.recv()).await { Ok(Ok(DomainEvent::AgentRateLimited { agent_id, .. })) if agent_id == agent(8) => { saw_rate_limited = true; } Ok(Ok(DomainEvent::AgentResumeScheduled { agent_id, .. })) if agent_id == agent(8) => { saw_scheduled = true; break; } Ok(Ok(_)) => continue, _ => break, } } assert!(saw_rate_limited, "AgentRateLimited relayed on the bus (humain)"); assert!(saw_scheduled, "AgentResumeScheduled relayed on the bus (humain)"); // Annulable par la même voie que l'auto. assert!( state.session_limit_service.cancel_resume(agent(8)), "an armed human resume is cancellable" ); }