Files
IdeA/crates/app-tauri/tests/session_limit_wiring.rs
Blomios 9df592389c feat(session-limits): LS7 — câblage backend app-tauri (taps niveaux 1&2 + reprise annulable)
Branche le SessionLimitService dans l'application Tauri et expose la
surface de reprise/annulation au front.

- application/agent/lifecycle.rs : LaunchAgentOutput.profile exposé
  (None sur réattache/idempotent, Some sur lancement effectif).
- application/terminal/registry.rs : StructuredSessions::meta_for_session()
  (lookup agent/node par SessionId pour le tap niveau 1).
- app-tauri/state.rs : ResumeContext(s), AppAgentResumer (impl du port
  AgentResumer au-dessus de LaunchAgent), instanciation + câblage du
  SessionLimitService (TokioScheduler + drain des réveils) dans AppState::build.
- app-tauri/commands.rs : taps niveau 1 (agent_send) et niveau 2
  (launch_agent, parser regex confiné), alimentation de resume_contexts,
  commande cancel_resume.
- app-tauri/lib.rs : enregistrement de cancel_resume dans le handler.
- app-tauri/Cargo.toml : dépendance async-trait.

Tests : session_limit_wiring.rs (2 tests de composition) + meta_for_session
dans structured_registry_d1.rs ; fixtures dto_agents/dto_chat ajustées
(profile: None). Tout vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 07:56:08 +02:00

99 lines
3.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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 LS1LS4 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");
}