//! LOT R0b (cadrage orchestration v5 §3.3, Trou B) — `list_live_agents` lit //! l'agrégateur `LiveSessions` (PTY **+** structuré), plus seulement le registre //! PTY. Un agent chat (structuré) vivant doit apparaître dans la liste, comme un //! agent PTY ; les deux ensemble sans doublon ; aucune session ⇒ liste vide. //! //! Ces tests reproduisent **exactement** ce que fait la commande //! `list_live_agents` : construire un `LiveSessions` à partir des deux registres //! partagés et passer `live_agents()` à `LiveAgentListDto::from_pairs`. Ils //! exercent donc le câblage de l'agrégateur **et** la dé-duplication par agent //! portée par le DTO (contrat de liveness pour l'UI). 100 % fakes, sans process. use std::sync::Arc; use async_trait::async_trait; use app_tauri_lib::dto::LiveAgentListDto; use application::{LiveSessions, StructuredSessions, TerminalSessions}; use domain::ports::{AgentSession, AgentSessionError, PtyHandle, ReplyStream}; use domain::{AgentId, NodeId, ProjectPath, PtySize, SessionId, SessionKind, TerminalSession}; use uuid::Uuid; // --- petits constructeurs déterministes ------------------------------------ fn sid(n: u128) -> SessionId { SessionId::from_uuid(Uuid::from_u128(n)) } fn aid(n: u128) -> AgentId { AgentId::from_uuid(Uuid::from_u128(n)) } fn nid(n: u128) -> NodeId { NodeId::from_uuid(Uuid::from_u128(n)) } /// Fake minimal d'`AgentSession` : porte juste l'id (ce que le registre clé). struct FakeSession { id: SessionId, } #[async_trait] impl AgentSession for FakeSession { fn id(&self) -> SessionId { self.id } fn conversation_id(&self) -> Option { None } async fn send(&self, _prompt: &str) -> Result { Ok(Box::new(std::iter::empty())) } async fn shutdown(&self) -> Result<(), AgentSessionError> { Ok(()) } } fn fake(id: SessionId) -> Arc { Arc::new(FakeSession { id }) } /// Insère un agent PTY dans `TerminalSessions` (jumeau du helper de D1). fn insert_pty(pty: &TerminalSessions, s: SessionId, a: AgentId, n: NodeId) { let session = TerminalSession::starting( s, n, ProjectPath::new("/p").unwrap(), SessionKind::Agent { agent_id: a }, PtySize::new(24, 80).unwrap(), ); pty.insert(PtyHandle { session_id: s }, session); } /// Reproduit le corps de la commande `list_live_agents` (hors validation d'id). fn dto_for(pty: &Arc, structured: &Arc) -> LiveAgentListDto { let live = LiveSessions::new(Arc::clone(pty), Arc::clone(structured)); LiveAgentListDto::from_pairs(live.live_agents()) } // =========================================================================== // 1. Un agent PTY vivant apparaît. // =========================================================================== #[test] fn pty_live_agent_is_listed() { let pty = Arc::new(TerminalSessions::new()); let structured = Arc::new(StructuredSessions::new()); let a = aid(10); insert_pty(&pty, sid(1), a, nid(100)); let dto = dto_for(&pty, &structured); assert_eq!(dto.0.len(), 1); assert_eq!(dto.0[0].agent_id, a.to_string()); assert_eq!(dto.0[0].node_id, nid(100).to_string()); assert_eq!(dto.0[0].session_id, sid(1).to_string()); } // =========================================================================== // 2. Un agent structuré/chat vivant apparaît (le cas qui régressait — Trou B). // =========================================================================== #[test] fn structured_live_agent_is_listed() { let pty = Arc::new(TerminalSessions::new()); let structured = Arc::new(StructuredSessions::new()); let a = aid(20); structured.insert(fake(sid(2)), a, nid(200)); let dto = dto_for(&pty, &structured); assert_eq!( dto.0.len(), 1, "un agent chat vivant doit apparaître (il était invisible avant R0b)" ); assert_eq!(dto.0[0].agent_id, a.to_string()); assert_eq!(dto.0[0].node_id, nid(200).to_string()); assert_eq!(dto.0[0].session_id, sid(2).to_string()); } // =========================================================================== // 3. Les deux types vivants en même temps ⇒ présents, sans doublon. // =========================================================================== #[test] fn both_kinds_live_listed_without_duplicates() { let pty = Arc::new(TerminalSessions::new()); let structured = Arc::new(StructuredSessions::new()); let pty_agent = aid(10); let chat_agent = aid(20); insert_pty(&pty, sid(1), pty_agent, nid(100)); structured.insert(fake(sid(2)), chat_agent, nid(200)); let dto = dto_for(&pty, &structured); assert_eq!(dto.0.len(), 2, "les deux registres contribuent"); let mut ids: Vec<&str> = dto.0.iter().map(|d| d.agent_id.as_str()).collect(); ids.sort_unstable(); let mut expected = vec![pty_agent.to_string(), chat_agent.to_string()]; expected.sort_unstable(); assert_eq!(ids, expected); // Aucun agent répété (contrat de liveness pour l'UI). let mut uniq = ids.clone(); uniq.dedup(); assert_eq!(uniq.len(), dto.0.len(), "aucun doublon d'agent"); } // =========================================================================== // 3bis. Même agent présent dans les DEUX registres ⇒ une seule entrée. // // L'invariant « 1 session vivante/agent » l'interdit normalement, mais le DTO // doit rester dup-free par défense en profondeur : un seul `agent_id` sur le fil. // =========================================================================== #[test] fn same_agent_in_both_registries_is_deduplicated() { let pty = Arc::new(TerminalSessions::new()); let structured = Arc::new(StructuredSessions::new()); let a = aid(30); insert_pty(&pty, sid(1), a, nid(100)); structured.insert(fake(sid(2)), a, nid(200)); let dto = dto_for(&pty, &structured); assert_eq!( dto.0.len(), 1, "un même agent ne doit apparaître qu'une fois" ); assert_eq!(dto.0[0].agent_id, a.to_string()); // On garde la première occurrence (PTY, listé en premier par l'agrégateur). assert_eq!(dto.0[0].node_id, nid(100).to_string()); assert_eq!(dto.0[0].session_id, sid(1).to_string()); } // =========================================================================== // 4. Aucune session ⇒ liste vide. // =========================================================================== #[test] fn no_sessions_yields_empty_list() { let pty = Arc::new(TerminalSessions::new()); let structured = Arc::new(StructuredSessions::new()); let dto = dto_for(&pty, &structured); assert!(dto.0.is_empty(), "aucune session ⇒ liste vide"); }